tester.nim 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  1. # Tester for nimsuggest.
  2. # Every test file can have a #[!]# comment that is deleted from the input
  3. # before 'nimsuggest' is invoked to ensure this token doesn't make a
  4. # crucial difference for Nim's parser.
  5. # When debugging, to run a single test, use for e.g.:
  6. # `nim r nimsuggest/tester.nim nimsuggest/tests/tsug_accquote.nim`
  7. import os, osproc, strutils, streams, re, sexp, net
  8. from sequtils import toSeq
  9. type
  10. Test = object
  11. filename, cmd, dest: string
  12. startup: seq[string]
  13. script: seq[(string, string)]
  14. disabled: bool
  15. const
  16. DummyEof = "!EOF!"
  17. tpath = "nimsuggest/tests"
  18. # we could also use `stdtest/specialpaths`
  19. import std/compilesettings
  20. proc parseTest(filename: string; epcMode=false): Test =
  21. const cursorMarker = "#[!]#"
  22. let nimsug = "bin" / addFileExt("nimsuggest_testing", ExeExt)
  23. doAssert nimsug.fileExists, nimsug
  24. const libpath = querySetting(libPath)
  25. result.filename = filename
  26. result.dest = getTempDir() / extractFilename(filename)
  27. result.cmd = nimsug & " --tester " & result.dest
  28. result.script = @[]
  29. result.startup = @[]
  30. var tmp = open(result.dest, fmWrite)
  31. var specSection = 0
  32. var markers = newSeq[string]()
  33. var i = 1
  34. for x in lines(filename):
  35. let marker = x.find(cursorMarker)
  36. if marker >= 0:
  37. if epcMode:
  38. markers.add "(\"" & filename & "\" " & $i & " " & $marker & " \"" & result.dest & "\")"
  39. else:
  40. markers.add "\"" & filename & "\";\"" & result.dest & "\":" & $i & ":" & $marker
  41. tmp.writeLine x.replace(cursorMarker, "")
  42. else:
  43. tmp.writeLine x
  44. if x.contains("""""""""):
  45. inc specSection
  46. elif specSection == 1:
  47. if x.startsWith("disabled:"):
  48. if x.startsWith("disabled:true"):
  49. result.disabled = true
  50. else:
  51. # be strict about format
  52. doAssert x.startsWith("disabled:false")
  53. result.disabled = false
  54. elif x.startsWith("$nimsuggest"):
  55. result.cmd = x % ["nimsuggest", nimsug, "file", filename, "lib", libpath]
  56. elif x.startsWith("!"):
  57. if result.cmd.len == 0:
  58. result.startup.add x
  59. else:
  60. result.script.add((x, ""))
  61. elif x.startsWith(">"):
  62. # since 'markers' here are not complete yet, we do the $substitutions
  63. # afterwards
  64. result.script.add((x.substr(1).replaceWord("$path", tpath), ""))
  65. elif x.len > 0:
  66. # expected output line:
  67. let x = x % ["file", filename, "lib", libpath]
  68. result.script[^1][1].add x.replace(";;", "\t") & '\L'
  69. # else: ignore empty lines for better readability of the specs
  70. inc i
  71. tmp.close()
  72. # now that we know the markers, substitute them:
  73. for a in mitems(result.script):
  74. a[0] = a[0] % markers
  75. proc parseCmd(c: string): seq[string] =
  76. # we don't support double quotes for now so that
  77. # we can later support them properly with escapes and stuff.
  78. result = @[]
  79. var i = 0
  80. var a = ""
  81. while i < c.len:
  82. setLen(a, 0)
  83. # eat all delimiting whitespace
  84. while i < c.len and c[i] in {' ', '\t', '\l', '\r'}: inc(i)
  85. if i >= c.len: break
  86. case c[i]
  87. of '"': raise newException(ValueError, "double quotes not yet supported: " & c)
  88. of '\'':
  89. var delim = c[i]
  90. inc(i) # skip ' or "
  91. while i < c.len and c[i] != delim:
  92. add a, c[i]
  93. inc(i)
  94. if i < c.len: inc(i)
  95. else:
  96. while i < c.len and c[i] > ' ':
  97. add(a, c[i])
  98. inc(i)
  99. add(result, a)
  100. proc edit(tmpfile: string; x: seq[string]) =
  101. if x.len != 3 and x.len != 4:
  102. quit "!edit takes two or three arguments"
  103. let f = if x.len >= 4: tpath / x[3] else: tmpfile
  104. try:
  105. let content = readFile(f)
  106. let newcontent = content.replace(x[1], x[2])
  107. if content == newcontent:
  108. quit "wrong test case: edit had no effect"
  109. writeFile(f, newcontent)
  110. except IOError:
  111. quit "cannot edit file " & tmpfile
  112. proc exec(x: seq[string]) =
  113. if x.len != 2: quit "!exec takes one argument"
  114. if execShellCmd(x[1]) != 0:
  115. quit "External program failed " & x[1]
  116. proc copy(x: seq[string]) =
  117. if x.len != 3: quit "!copy takes two arguments"
  118. let rel = tpath
  119. copyFile(rel / x[1], rel / x[2])
  120. proc del(x: seq[string]) =
  121. if x.len != 2: quit "!del takes one argument"
  122. removeFile(tpath / x[1])
  123. proc runCmd(cmd, dest: string): bool =
  124. result = cmd[0] == '!'
  125. if not result: return
  126. let x = cmd.parseCmd()
  127. case x[0]
  128. of "!edit":
  129. edit(dest, x)
  130. of "!exec":
  131. exec(x)
  132. of "!copy":
  133. copy(x)
  134. of "!del":
  135. del(x)
  136. else:
  137. quit "unknown command: " & cmd
  138. proc smartCompare(pattern, x: string): bool =
  139. if pattern.contains('*'):
  140. result = match(x, re(escapeRe(pattern).replace("\\x2A","(.*)"), {}))
  141. proc sendEpcStr(socket: Socket; cmd: string) =
  142. let s = cmd.find(' ')
  143. doAssert s > 0
  144. var args = cmd.substr(s+1)
  145. if not args.startsWith("("): args = escapeJson(args)
  146. let c = "(call 567 " & cmd.substr(0, s) & args & ")"
  147. socket.send toHex(c.len, 6)
  148. socket.send c
  149. proc recvEpc(socket: Socket): string =
  150. var L = newStringOfCap(6)
  151. if socket.recv(L, 6) != 6:
  152. raise newException(ValueError, "recv A failed #" & L & "#")
  153. let x = parseHexInt(L)
  154. result = newString(x)
  155. if socket.recv(result, x) != x:
  156. raise newException(ValueError, "recv B failed")
  157. proc sexpToAnswer(s: SexpNode): string =
  158. result = ""
  159. doAssert s.kind == SList
  160. doAssert s.len >= 3
  161. let m = s[2]
  162. if m.kind != SList:
  163. echo s
  164. doAssert m.kind == SList
  165. for a in m:
  166. doAssert a.kind == SList
  167. #s.section,
  168. #s.symkind,
  169. #s.qualifiedPath.map(newSString),
  170. #s.filePath,
  171. #s.forth,
  172. #s.line,
  173. #s.column,
  174. #s.doc
  175. if a.len >= 9:
  176. let section = a[0].getStr
  177. let symk = a[1].getStr
  178. let qp = a[2]
  179. let file = a[3].getStr
  180. let typ = a[4].getStr
  181. let line = a[5].getNum
  182. let col = a[6].getNum
  183. let doc = a[7].getStr.escape
  184. result.add section
  185. result.add '\t'
  186. result.add symk
  187. result.add '\t'
  188. var i = 0
  189. if qp.kind == SList:
  190. for aa in qp:
  191. if i > 0: result.add '.'
  192. result.add aa.getStr
  193. inc i
  194. result.add '\t'
  195. result.add typ
  196. result.add '\t'
  197. result.add file
  198. result.add '\t'
  199. result.addInt line
  200. result.add '\t'
  201. result.addInt col
  202. result.add '\t'
  203. result.add doc
  204. result.add '\t'
  205. result.addInt a[8].getNum
  206. if a.len >= 10:
  207. result.add '\t'
  208. result.add a[9].getStr
  209. result.add '\L'
  210. proc doReport(filename, answer, resp: string; report: var string) =
  211. if resp != answer and not smartCompare(resp, answer):
  212. report.add "\nTest failed: " & filename
  213. var hasDiff = false
  214. for i in 0..min(resp.len-1, answer.len-1):
  215. if resp[i] != answer[i]:
  216. report.add "\n Expected: " & resp.substr(i, i+200)
  217. report.add "\n But got: " & answer.substr(i, i+200)
  218. hasDiff = true
  219. break
  220. if not hasDiff:
  221. report.add "\n Expected: " & resp
  222. report.add "\n But got: " & answer
  223. proc skipDisabledTest(test: Test): bool =
  224. if test.disabled:
  225. echo "disabled: " & test.filename
  226. result = test.disabled
  227. proc runEpcTest(filename: string): int =
  228. let s = parseTest(filename, true)
  229. if s.skipDisabledTest: return 0
  230. for req, _ in items(s.script):
  231. if req.startsWith("highlight"):
  232. echo "disabled epc: " & s.filename
  233. return 0
  234. for cmd in s.startup:
  235. if not runCmd(cmd, s.dest):
  236. quit "invalid command: " & cmd
  237. let epccmd = if s.cmd.contains("--v3"):
  238. s.cmd.replace("--tester", "--epc --log")
  239. else:
  240. s.cmd.replace("--tester", "--epc --v2 --log")
  241. let cl = parseCmdLine(epccmd)
  242. var p = startProcess(command=cl[0], args=cl[1 .. ^1],
  243. options={poStdErrToStdOut, poUsePath,
  244. poInteractive, poDaemon})
  245. let outp = p.outputStream
  246. var report = ""
  247. var socket = newSocket()
  248. try:
  249. # read the port number:
  250. when defined(posix):
  251. var a = newStringOfCap(120)
  252. discard outp.readLine(a)
  253. else:
  254. var i = 0
  255. while not osproc.hasData(p) and i < 100:
  256. os.sleep(50)
  257. inc i
  258. let a = outp.readAll().strip()
  259. let port = parseInt(a)
  260. socket.connect("localhost", Port(port))
  261. for req, resp in items(s.script):
  262. if not runCmd(req, s.dest):
  263. socket.sendEpcStr(req)
  264. let sx = parseSexp(socket.recvEpc())
  265. if not req.startsWith("mod "):
  266. let answer = if sx[2].kind == SNil: "" else: sexpToAnswer(sx)
  267. doReport(filename, answer, resp, report)
  268. socket.sendEpcStr "return arg"
  269. # bugfix: this was in `finally` block, causing the original error to be
  270. # potentially masked by another one in case `socket.sendEpcStr` raises
  271. # (e.g. if socket couldn't connect in the 1st place)
  272. finally:
  273. close(p)
  274. if report.len > 0:
  275. echo "==== EPC ========================================"
  276. echo report
  277. result = report.len
  278. proc runTest(filename: string): int =
  279. let s = parseTest filename
  280. if s.skipDisabledTest: return 0
  281. for cmd in s.startup:
  282. if not runCmd(cmd, s.dest):
  283. quit "invalid command: " & cmd
  284. let cl = parseCmdLine(s.cmd)
  285. var p = startProcess(command=cl[0], args=cl[1 .. ^1],
  286. options={poStdErrToStdOut, poUsePath,
  287. poInteractive, poDaemon})
  288. let outp = p.outputStream
  289. let inp = p.inputStream
  290. var report = ""
  291. var a = newStringOfCap(120)
  292. try:
  293. # read and ignore anything nimsuggest says at startup:
  294. while outp.readLine(a):
  295. if a == DummyEof: break
  296. for req, resp in items(s.script):
  297. if not runCmd(req, s.dest):
  298. inp.writeLine(req)
  299. inp.flush()
  300. var answer = ""
  301. while outp.readLine(a):
  302. if a == DummyEof: break
  303. answer.add a
  304. answer.add '\L'
  305. doReport(filename, answer, resp, report)
  306. finally:
  307. try:
  308. inp.writeLine("quit")
  309. inp.flush()
  310. except IOError, OSError:
  311. # assume it's SIGPIPE, ie, the child already died
  312. discard
  313. close(p)
  314. if report.len > 0:
  315. echo "==== STDIN ======================================"
  316. echo report
  317. result = report.len
  318. proc main() =
  319. var failures = 0
  320. if os.paramCount() > 0:
  321. let x = os.paramStr(1)
  322. let xx = expandFilename x
  323. failures += runTest(xx)
  324. failures += runEpcTest(xx)
  325. else:
  326. let files = toSeq(walkFiles(tpath / "t*.nim"))
  327. for i, x in files:
  328. echo "$#/$# test: $#" % [$i, $files.len, x]
  329. when defined(i386):
  330. if x == "nimsuggest/tests/tmacro_highlight.nim":
  331. echo "skipping" # workaround bug #17945
  332. continue
  333. let xx = expandFilename x
  334. when not defined(windows):
  335. # XXX Windows IO redirection seems bonkers:
  336. failures += runTest(xx)
  337. failures += runEpcTest(xx)
  338. if failures > 0:
  339. quit 1
  340. main()