tester.nim 9.8 KB

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