tester.nim 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375
  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).replaceWord("$file", filename), ""))
  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 >= 11:
  207. result.add '\t'
  208. result.addInt a[9].getNum
  209. result.add '\t'
  210. result.addInt a[10].getNum
  211. elif a.len >= 10:
  212. result.add '\t'
  213. result.add a[9].getStr
  214. result.add '\L'
  215. proc doReport(filename, answer, resp: string; report: var string) =
  216. if resp != answer and not smartCompare(resp, answer):
  217. report.add "\nTest failed: " & filename
  218. var hasDiff = false
  219. for i in 0..min(resp.len-1, answer.len-1):
  220. if resp[i] != answer[i]:
  221. report.add "\n Expected:\n" & resp
  222. report.add "\n But got:\n" & answer
  223. hasDiff = true
  224. break
  225. if not hasDiff:
  226. report.add "\n Expected: " & resp
  227. report.add "\n But got: " & answer
  228. proc skipDisabledTest(test: Test): bool =
  229. if test.disabled:
  230. echo "disabled: " & test.filename
  231. result = test.disabled
  232. proc runEpcTest(filename: string): int =
  233. let s = parseTest(filename, true)
  234. if s.skipDisabledTest: return 0
  235. for req, _ in items(s.script):
  236. if req.startsWith("highlight"):
  237. echo "disabled epc: " & s.filename
  238. return 0
  239. for cmd in s.startup:
  240. if not runCmd(cmd, s.dest):
  241. quit "invalid command: " & cmd
  242. let epccmd = if s.cmd.contains("--v3"):
  243. s.cmd.replace("--tester", "--epc --log")
  244. else:
  245. s.cmd.replace("--tester", "--epc --v2 --log")
  246. let cl = parseCmdLine(epccmd)
  247. var p = startProcess(command=cl[0], args=cl[1 .. ^1],
  248. options={poStdErrToStdOut, poUsePath,
  249. poInteractive, poDaemon})
  250. let outp = p.outputStream
  251. var report = ""
  252. var socket = newSocket()
  253. try:
  254. # read the port number:
  255. when defined(posix):
  256. var a = newStringOfCap(120)
  257. discard outp.readLine(a)
  258. else:
  259. var i = 0
  260. while not osproc.hasData(p) and i < 100:
  261. os.sleep(50)
  262. inc i
  263. let a = outp.readAll().strip()
  264. var port: int
  265. try:
  266. port = parseInt(a)
  267. except ValueError:
  268. echo "Error parsing port number: " & a
  269. echo outp.readAll()
  270. quit 1
  271. socket.connect("localhost", Port(port))
  272. for req, resp in items(s.script):
  273. if not runCmd(req, s.dest):
  274. socket.sendEpcStr(req)
  275. let sx = parseSexp(socket.recvEpc())
  276. if not req.startsWith("mod "):
  277. let answer = if sx[2].kind == SNil: "" else: sexpToAnswer(sx)
  278. doReport(filename, answer, resp, report)
  279. socket.sendEpcStr "return arg"
  280. # bugfix: this was in `finally` block, causing the original error to be
  281. # potentially masked by another one in case `socket.sendEpcStr` raises
  282. # (e.g. if socket couldn't connect in the 1st place)
  283. finally:
  284. close(p)
  285. if report.len > 0:
  286. echo "==== EPC ========================================"
  287. echo report
  288. result = report.len
  289. proc runTest(filename: string): int =
  290. let s = parseTest filename
  291. if s.skipDisabledTest: return 0
  292. for cmd in s.startup:
  293. if not runCmd(cmd, s.dest):
  294. quit "invalid command: " & cmd
  295. let cl = parseCmdLine(s.cmd)
  296. var p = startProcess(command=cl[0], args=cl[1 .. ^1],
  297. options={poStdErrToStdOut, poUsePath,
  298. poInteractive, poDaemon})
  299. let outp = p.outputStream
  300. let inp = p.inputStream
  301. var report = ""
  302. var a = newStringOfCap(120)
  303. try:
  304. # read and ignore anything nimsuggest says at startup:
  305. while outp.readLine(a):
  306. if a == DummyEof: break
  307. for req, resp in items(s.script):
  308. if not runCmd(req, s.dest):
  309. inp.writeLine(req)
  310. inp.flush()
  311. var answer = ""
  312. while outp.readLine(a):
  313. if a == DummyEof: break
  314. answer.add a
  315. answer.add '\L'
  316. doReport(filename, answer, resp, report)
  317. finally:
  318. try:
  319. inp.writeLine("quit")
  320. inp.flush()
  321. except IOError, OSError:
  322. # assume it's SIGPIPE, ie, the child already died
  323. discard
  324. close(p)
  325. if report.len > 0:
  326. echo "==== STDIN ======================================"
  327. echo report
  328. result = report.len
  329. proc main() =
  330. var failures = 0
  331. if os.paramCount() > 0:
  332. let x = os.paramStr(1)
  333. let xx = expandFilename x
  334. # run only stdio when running single test
  335. failures += runTest(xx)
  336. else:
  337. let files = toSeq(walkFiles(tpath / "t*.nim"))
  338. for i, x in files:
  339. echo "$#/$# test: $#" % [$i, $files.len, x]
  340. when defined(i386):
  341. if x == "nimsuggest/tests/tmacro_highlight.nim":
  342. echo "skipping" # workaround bug #17945
  343. continue
  344. let xx = expandFilename x
  345. when not defined(windows):
  346. # XXX Windows IO redirection seems bonkers:
  347. failures += runTest(xx)
  348. failures += runEpcTest(xx)
  349. if failures > 0:
  350. quit 1
  351. main()