tester.nim 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496
  1. #
  2. #
  3. # Nim Tester
  4. # (c) Copyright 2015 Andreas Rumpf
  5. #
  6. # See the file "copying.txt", included in this
  7. # distribution, for details about the copyright.
  8. #
  9. ## This program verifies Nim against the testcases.
  10. import
  11. parseutils, strutils, pegs, os, osproc, streams, parsecfg, json,
  12. marshal, backend, parseopt, specs, htmlgen, browsers, terminal,
  13. algorithm, compiler/nodejs, re, times, sets
  14. const
  15. resultsFile = "testresults.html"
  16. jsonFile = "testresults.json"
  17. Usage = """Usage:
  18. tester [options] command [arguments]
  19. Command:
  20. all run all tests
  21. c|category <category> run all the tests of a certain category
  22. r|run <test> run single test file
  23. html [commit] generate $1 from the database; uses the latest
  24. commit or a specific one (use -1 for the commit
  25. before latest etc)
  26. Arguments:
  27. arguments are passed to the compiler
  28. Options:
  29. --print also print results to the console
  30. --failing only show failing/ignored tests
  31. --pedantic return non-zero status code if there are failures
  32. --targets:"c c++ js objc" run tests for specified targets (default: all)
  33. --nim:path use a particular nim executable (default: compiler/nim)
  34. """ % resultsFile
  35. type
  36. Category = distinct string
  37. TResults = object
  38. total, passed, skipped: int
  39. data: string
  40. TTest = object
  41. name: string
  42. cat: Category
  43. options: string
  44. target: TTarget
  45. action: TTestAction
  46. startTime: float
  47. # ----------------------------------------------------------------------------
  48. let
  49. pegLineError =
  50. peg"{[^(]*} '(' {\d+} ', ' {\d+} ') ' ('Error') ':' \s* {.*}"
  51. pegLineTemplate =
  52. peg"{[^(]*} '(' {\d+} ', ' {\d+} ') ' 'template/generic instantiation from here'.*"
  53. pegOtherError = peg"'Error:' \s* {.*}"
  54. pegSuccess = peg"'Hint: operation successful'.*"
  55. pegOfInterest = pegLineError / pegOtherError
  56. var targets = {low(TTarget)..high(TTarget)}
  57. proc normalizeMsg(s: string): string =
  58. result = newStringOfCap(s.len+1)
  59. for x in splitLines(s):
  60. if result.len > 0: result.add '\L'
  61. result.add x.strip
  62. proc getFileDir(filename: string): string =
  63. result = filename.splitFile().dir
  64. if not result.isAbsolute():
  65. result = getCurrentDir() / result
  66. proc callCompiler(cmdTemplate, filename, options: string,
  67. target: TTarget): TSpec =
  68. let c = parseCmdLine(cmdTemplate % ["target", targetToCmd[target],
  69. "options", options, "file", filename.quoteShell,
  70. "filedir", filename.getFileDir()])
  71. var p = startProcess(command=c[0], args=c[1.. ^1],
  72. options={poStdErrToStdOut, poUsePath})
  73. let outp = p.outputStream
  74. var suc = ""
  75. var err = ""
  76. var tmpl = ""
  77. var x = newStringOfCap(120)
  78. result.nimout = ""
  79. while outp.readLine(x.TaintedString) or running(p):
  80. result.nimout.add(x & "\n")
  81. if x =~ pegOfInterest:
  82. # `err` should contain the last error/warning message
  83. err = x
  84. elif x =~ pegLineTemplate and err == "":
  85. # `tmpl` contains the last template expansion before the error
  86. tmpl = x
  87. elif x =~ pegSuccess:
  88. suc = x
  89. close(p)
  90. result.msg = ""
  91. result.file = ""
  92. result.outp = ""
  93. result.line = 0
  94. result.column = 0
  95. result.tfile = ""
  96. result.tline = 0
  97. result.tcolumn = 0
  98. if tmpl =~ pegLineTemplate:
  99. result.tfile = extractFilename(matches[0])
  100. result.tline = parseInt(matches[1])
  101. result.tcolumn = parseInt(matches[2])
  102. if err =~ pegLineError:
  103. result.file = extractFilename(matches[0])
  104. result.line = parseInt(matches[1])
  105. result.column = parseInt(matches[2])
  106. result.msg = matches[3]
  107. elif err =~ pegOtherError:
  108. result.msg = matches[0]
  109. elif suc =~ pegSuccess:
  110. result.err = reSuccess
  111. proc callCCompiler(cmdTemplate, filename, options: string,
  112. target: TTarget): TSpec =
  113. let c = parseCmdLine(cmdTemplate % ["target", targetToCmd[target],
  114. "options", options, "file", filename.quoteShell,
  115. "filedir", filename.getFileDir()])
  116. var p = startProcess(command="gcc", args=c[5.. ^1],
  117. options={poStdErrToStdOut, poUsePath})
  118. let outp = p.outputStream
  119. var x = newStringOfCap(120)
  120. result.nimout = ""
  121. result.msg = ""
  122. result.file = ""
  123. result.outp = ""
  124. result.line = -1
  125. while outp.readLine(x.TaintedString) or running(p):
  126. result.nimout.add(x & "\n")
  127. close(p)
  128. if p.peekExitCode == 0:
  129. result.err = reSuccess
  130. proc initResults: TResults =
  131. result.total = 0
  132. result.passed = 0
  133. result.skipped = 0
  134. result.data = ""
  135. proc readResults(filename: string): TResults =
  136. result = marshal.to[TResults](readFile(filename).string)
  137. proc writeResults(filename: string, r: TResults) =
  138. writeFile(filename, $$r)
  139. proc `$`(x: TResults): string =
  140. result = ("Tests passed: $1 / $3 <br />\n" &
  141. "Tests skipped: $2 / $3 <br />\n") %
  142. [$x.passed, $x.skipped, $x.total]
  143. proc addResult(r: var TResults, test: TTest,
  144. expected, given: string, success: TResultEnum) =
  145. let name = test.name.extractFilename & test.options
  146. let duration = epochTime() - test.startTime
  147. backend.writeTestResult(name = name,
  148. category = test.cat.string,
  149. target = $test.target,
  150. action = $test.action,
  151. result = $success,
  152. expected = expected,
  153. given = given)
  154. r.data.addf("$#\t$#\t$#\t$#", name, expected, given, $success)
  155. if success == reSuccess:
  156. styledEcho fgGreen, "PASS: ", fgCyan, name
  157. elif success == reIgnored:
  158. styledEcho styleDim, fgYellow, "SKIP: ", styleBright, fgCyan, name
  159. else:
  160. styledEcho styleBright, fgRed, "FAIL: ", fgCyan, name
  161. styledEcho styleBright, fgCyan, "Test \"", test.name, "\"", " in category \"", test.cat.string, "\""
  162. styledEcho styleBright, fgRed, "Failure: ", $success
  163. styledEcho fgYellow, "Expected:"
  164. styledEcho styleBright, expected, "\n"
  165. styledEcho fgYellow, "Gotten:"
  166. styledEcho styleBright, given, "\n"
  167. if existsEnv("APPVEYOR"):
  168. let (outcome, msg) =
  169. if success == reSuccess:
  170. ("Passed", "")
  171. elif success == reIgnored:
  172. ("Skipped", "")
  173. else:
  174. ("Failed", "Failure: " & $success & "\nExpected:\n" & expected & "\n\n" & "Gotten:\n" & given)
  175. var p = startProcess("appveyor", args=["AddTest", test.name.replace("\\", "/") & test.options, "-Framework", "nim-testament", "-FileName", test.cat.string, "-Outcome", outcome, "-ErrorMessage", msg, "-Duration", $(duration*1000).int], options={poStdErrToStdOut, poUsePath, poParentStreams})
  176. discard waitForExit(p)
  177. close(p)
  178. proc cmpMsgs(r: var TResults, expected, given: TSpec, test: TTest) =
  179. if strip(expected.msg) notin strip(given.msg):
  180. r.addResult(test, expected.msg, given.msg, reMsgsDiffer)
  181. elif expected.nimout.len > 0 and expected.nimout.normalizeMsg notin given.nimout.normalizeMsg:
  182. r.addResult(test, expected.nimout, given.nimout, reMsgsDiffer)
  183. elif expected.tfile == "" and extractFilename(expected.file) != extractFilename(given.file) and
  184. "internal error:" notin expected.msg:
  185. r.addResult(test, expected.file, given.file, reFilesDiffer)
  186. elif expected.line != given.line and expected.line != 0 or
  187. expected.column != given.column and expected.column != 0:
  188. r.addResult(test, $expected.line & ':' & $expected.column,
  189. $given.line & ':' & $given.column,
  190. reLinesDiffer)
  191. elif expected.tfile != "" and extractFilename(expected.tfile) != extractFilename(given.tfile) and
  192. "internal error:" notin expected.msg:
  193. r.addResult(test, expected.tfile, given.tfile, reFilesDiffer)
  194. elif expected.tline != given.tline and expected.tline != 0 or
  195. expected.tcolumn != given.tcolumn and expected.tcolumn != 0:
  196. r.addResult(test, $expected.tline & ':' & $expected.tcolumn,
  197. $given.tline & ':' & $given.tcolumn,
  198. reLinesDiffer)
  199. else:
  200. r.addResult(test, expected.msg, given.msg, reSuccess)
  201. inc(r.passed)
  202. proc generatedFile(path, name: string, target: TTarget): string =
  203. let ext = targetToExt[target]
  204. result = path / "nimcache" /
  205. (if target == targetJS: "" else: "compiler_") &
  206. name.changeFileExt(ext)
  207. proc needsCodegenCheck(spec: TSpec): bool =
  208. result = spec.maxCodeSize > 0 or spec.ccodeCheck.len > 0
  209. proc codegenCheck(test: TTest, spec: TSpec, expectedMsg: var string,
  210. given: var TSpec) =
  211. try:
  212. let (path, name, _) = test.name.splitFile
  213. let genFile = generatedFile(path, name, test.target)
  214. let contents = readFile(genFile).string
  215. let check = spec.ccodeCheck
  216. if check.len > 0:
  217. if check[0] == '\\':
  218. # little hack to get 'match' support:
  219. if not contents.match(check.peg):
  220. given.err = reCodegenFailure
  221. elif contents.find(check.peg) < 0:
  222. given.err = reCodegenFailure
  223. expectedMsg = check
  224. if spec.maxCodeSize > 0 and contents.len > spec.maxCodeSize:
  225. given.err = reCodegenFailure
  226. given.msg = "generated code size: " & $contents.len
  227. expectedMsg = "max allowed size: " & $spec.maxCodeSize
  228. except ValueError:
  229. given.err = reInvalidPeg
  230. echo getCurrentExceptionMsg()
  231. except IOError:
  232. given.err = reCodeNotFound
  233. proc nimoutCheck(test: TTest; expectedNimout: string; given: var TSpec) =
  234. let exp = expectedNimout.strip.replace("\C\L", "\L")
  235. let giv = given.nimout.strip.replace("\C\L", "\L")
  236. if exp notin giv:
  237. given.err = reMsgsDiffer
  238. proc makeDeterministic(s: string): string =
  239. var x = splitLines(s)
  240. sort(x, system.cmp)
  241. result = join(x, "\n")
  242. proc compilerOutputTests(test: TTest, given: var TSpec, expected: TSpec;
  243. r: var TResults) =
  244. var expectedmsg: string = ""
  245. var givenmsg: string = ""
  246. if given.err == reSuccess:
  247. if expected.needsCodegenCheck:
  248. codegenCheck(test, expected, expectedmsg, given)
  249. givenmsg = given.msg
  250. if expected.nimout.len > 0:
  251. expectedmsg = expected.nimout
  252. givenmsg = given.nimout.strip
  253. nimoutCheck(test, expectedmsg, given)
  254. else:
  255. givenmsg = given.nimout.strip
  256. if given.err == reSuccess: inc(r.passed)
  257. r.addResult(test, expectedmsg, givenmsg, given.err)
  258. proc analyzeAndConsolidateOutput(s: string): string =
  259. result = ""
  260. let rows = s.splitLines
  261. for i in 0 ..< rows.len:
  262. if (let pos = find(rows[i], "Traceback (most recent call last)"); pos != -1):
  263. result = substr(rows[i], pos) & "\n"
  264. for i in i+1 ..< rows.len:
  265. result.add rows[i] & "\n"
  266. if not (rows[i] =~ re"^[^(]+\(\d+\)\s+"):
  267. return
  268. elif (let pos = find(rows[i], "SIGSEGV: Illegal storage access."); pos != -1):
  269. result = substr(rows[i], pos)
  270. return
  271. proc testSpec(r: var TResults, test: TTest) =
  272. # major entry point for a single test
  273. if test.target notin targets:
  274. r.addResult(test, "", "", reIgnored)
  275. inc(r.skipped)
  276. return
  277. let tname = test.name.addFileExt(".nim")
  278. #echo "TESTING ", tname
  279. inc(r.total)
  280. var expected: TSpec
  281. if test.action != actionRunNoSpec:
  282. expected = parseSpec(tname)
  283. else:
  284. specDefaults expected
  285. expected.action = actionRunNoSpec
  286. if expected.err == reIgnored:
  287. r.addResult(test, "", "", reIgnored)
  288. inc(r.skipped)
  289. return
  290. case expected.action
  291. of actionCompile:
  292. var given = callCompiler(expected.cmd, test.name,
  293. test.options & " --stdout --hint[Path]:off --hint[Processing]:off",
  294. test.target)
  295. compilerOutputTests(test, given, expected, r)
  296. of actionRun, actionRunNoSpec:
  297. # In this branch of code "early return" pattern is clearer than deep
  298. # nested conditionals - the empty rows in between to clarify the "danger"
  299. var given = callCompiler(expected.cmd, test.name, test.options,
  300. test.target)
  301. if given.err != reSuccess:
  302. r.addResult(test, "", given.msg, given.err)
  303. return
  304. let isJsTarget = test.target == targetJS
  305. var exeFile: string
  306. if isJsTarget:
  307. let (dir, file, _) = splitFile(tname)
  308. exeFile = dir / "nimcache" / file & ".js" # *TODO* hardcoded "nimcache"
  309. else:
  310. exeFile = changeFileExt(tname, ExeExt)
  311. if not existsFile(exeFile):
  312. r.addResult(test, expected.outp, "executable not found", reExeNotFound)
  313. return
  314. let nodejs = if isJsTarget: findNodeJs() else: ""
  315. if isJsTarget and nodejs == "":
  316. r.addResult(test, expected.outp, "nodejs binary not in PATH",
  317. reExeNotFound)
  318. return
  319. let exeCmd = (if isJsTarget: nodejs & " " else: "") & exeFile
  320. var (buf, exitCode) = execCmdEx(exeCmd, options = {poStdErrToStdOut})
  321. # Treat all failure codes from nodejs as 1. Older versions of nodejs used
  322. # to return other codes, but for us it is sufficient to know that it's not 0.
  323. if exitCode != 0: exitCode = 1
  324. let bufB = if expected.sortoutput: makeDeterministic(strip(buf.string))
  325. else: strip(buf.string)
  326. let expectedOut = strip(expected.outp)
  327. if exitCode != expected.exitCode:
  328. r.addResult(test, "exitcode: " & $expected.exitCode,
  329. "exitcode: " & $exitCode & "\n\nOutput:\n" &
  330. analyzeAndConsolidateOutput(bufB),
  331. reExitCodesDiffer)
  332. return
  333. if bufB != expectedOut and expected.action != actionRunNoSpec:
  334. if not (expected.substr and expectedOut in bufB):
  335. given.err = reOutputsDiffer
  336. r.addResult(test, expected.outp, bufB, reOutputsDiffer)
  337. return
  338. compilerOutputTests(test, given, expected, r)
  339. return
  340. of actionReject:
  341. var given = callCompiler(expected.cmd, test.name, test.options,
  342. test.target)
  343. cmpMsgs(r, expected, given, test)
  344. return
  345. proc testNoSpec(r: var TResults, test: TTest) =
  346. # does not extract the spec because the file is not supposed to have any
  347. #let tname = test.name.addFileExt(".nim")
  348. inc(r.total)
  349. let given = callCompiler(cmdTemplate(), test.name, test.options, test.target)
  350. r.addResult(test, "", given.msg, given.err)
  351. if given.err == reSuccess: inc(r.passed)
  352. proc testC(r: var TResults, test: TTest) =
  353. # runs C code. Doesn't support any specs, just goes by exit code.
  354. let tname = test.name.addFileExt(".c")
  355. inc(r.total)
  356. styledEcho "Processing ", fgCyan, extractFilename(tname)
  357. var given = callCCompiler(cmdTemplate(), test.name & ".c", test.options, test.target)
  358. if given.err != reSuccess:
  359. r.addResult(test, "", given.msg, given.err)
  360. elif test.action == actionRun:
  361. let exeFile = changeFileExt(test.name, ExeExt)
  362. var (_, exitCode) = execCmdEx(exeFile, options = {poStdErrToStdOut, poUsePath})
  363. if exitCode != 0: given.err = reExitCodesDiffer
  364. if given.err == reSuccess: inc(r.passed)
  365. proc makeTest(test, options: string, cat: Category, action = actionCompile,
  366. target = targetC, env: string = ""): TTest =
  367. # start with 'actionCompile', will be overwritten in the spec:
  368. result = TTest(cat: cat, name: test, options: options,
  369. target: target, action: action, startTime: epochTime())
  370. when defined(windows):
  371. const
  372. # array of modules disabled from compilation test of stdlib.
  373. disabledFiles = ["coro.nim", "fsmonitor.nim"]
  374. else:
  375. const
  376. # array of modules disabled from compilation test of stdlib.
  377. disabledFiles = ["-"]
  378. include categories
  379. # proc runCaasTests(r: var TResults) =
  380. # for test, output, status, mode in caasTestsRunner():
  381. # r.addResult(test, "", output & "-> " & $mode,
  382. # if status: reSuccess else: reOutputsDiffer)
  383. proc main() =
  384. os.putenv "NIMTEST_NO_COLOR", "1"
  385. os.putenv "NIMTEST_OUTPUT_LVL", "PRINT_FAILURES"
  386. backend.open()
  387. var optPrintResults = false
  388. var optFailing = false
  389. var optPedantic = false
  390. var p = initOptParser()
  391. p.next()
  392. while p.kind == cmdLongoption:
  393. case p.key.string.normalize
  394. of "print", "verbose": optPrintResults = true
  395. of "failing": optFailing = true
  396. of "pedantic": optPedantic = true
  397. of "targets": targets = parseTargets(p.val.string)
  398. of "nim": compilerPrefix = p.val.string
  399. else: quit Usage
  400. p.next()
  401. if p.kind != cmdArgument: quit Usage
  402. var action = p.key.string.normalize
  403. p.next()
  404. var r = initResults()
  405. case action
  406. of "all":
  407. let testsDir = "tests" & DirSep
  408. for kind, dir in walkDir(testsDir):
  409. assert testsDir.startsWith(testsDir)
  410. let cat = dir[testsDir.len .. ^1]
  411. if kind == pcDir and cat notin ["testament", "testdata", "nimcache"]:
  412. processCategory(r, Category(cat), p.cmdLineRest.string)
  413. for a in AdditionalCategories:
  414. processCategory(r, Category(a), p.cmdLineRest.string)
  415. of "c", "cat", "category":
  416. var cat = Category(p.key)
  417. p.next
  418. processCategory(r, cat, p.cmdLineRest.string)
  419. of "r", "run":
  420. let (dir, file) = splitPath(p.key.string)
  421. let (_, subdir) = splitPath(dir)
  422. var cat = Category(subdir)
  423. processSingleTest(r, cat, p.cmdLineRest.string, file)
  424. of "html":
  425. var commit = 0
  426. discard parseInt(p.cmdLineRest.string, commit)
  427. generateHtml(resultsFile, commit, optFailing)
  428. generateJson(jsonFile, commit)
  429. else:
  430. quit Usage
  431. if optPrintResults:
  432. if action == "html": openDefaultBrowser(resultsFile)
  433. else: echo r, r.data
  434. backend.close()
  435. if optPedantic:
  436. var failed = r.total - r.passed - r.skipped
  437. if failed > 0:
  438. echo "FAILURE! total: ", r.total, " passed: ", r.passed, " skipped: ", r.skipped
  439. quit(QuitFailure)
  440. if paramCount() == 0:
  441. quit Usage
  442. main()