tester.nim 19 KB

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