testament.nim 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823
  1. #
  2. #
  3. # Nim Testament
  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. strutils, pegs, os, osproc, streams, json,
  12. backend, parseopt, specs, htmlgen, browsers, terminal,
  13. algorithm, times, md5, sequtils, azure, intsets
  14. from std/sugar import dup
  15. import compiler/nodejs
  16. var useColors = true
  17. var backendLogging = true
  18. var simulate = false
  19. const
  20. failString* = "FAIL: " # ensures all failures can be searched with 1 keyword in CI logs
  21. testsDir = "tests" & DirSep
  22. resultsFile = "testresults.html"
  23. #jsonFile = "testresults.json" # not used
  24. Usage = """Usage:
  25. testament [options] command [arguments]
  26. Command:
  27. p|pat|pattern <glob> run all the tests matching the given pattern
  28. all run all tests
  29. c|cat|category <category> run all the tests of a certain category
  30. r|run <test> run single test file
  31. html generate $1 from the database
  32. stats generate statistics about test cases
  33. Arguments:
  34. arguments are passed to the compiler
  35. Options:
  36. --print also print results to the console
  37. --simulate see what tests would be run but don't run them (for debugging)
  38. --failing only show failing/ignored tests
  39. --targets:"c c++ js objc" run tests for specified targets (default: all)
  40. --nim:path use a particular nim executable (default: $$PATH/nim)
  41. --directory:dir Change to directory dir before reading the tests or doing anything else.
  42. --colors:on|off Turn messages coloring on|off.
  43. --backendLogging:on|off Disable or enable backend logging. By default turned on.
  44. --megatest:on|off Enable or disable megatest. Default is on.
  45. --skipFrom:file Read tests to skip from `file` - one test per line, # comments ignored
  46. On Azure Pipelines, testament will also publish test results via Azure Pipelines' Test Management API
  47. provided that System.AccessToken is made available via the environment variable SYSTEM_ACCESSTOKEN.
  48. """ % resultsFile
  49. type
  50. Category = distinct string
  51. TResults = object
  52. total, passed, skipped: int
  53. data: string
  54. TTest = object
  55. name: string
  56. cat: Category
  57. options: string
  58. args: seq[string]
  59. spec: TSpec
  60. startTime: float
  61. # ----------------------------------------------------------------------------
  62. let
  63. pegLineError =
  64. peg"{[^(]*} '(' {\d+} ', ' {\d+} ') ' ('Error') ':' \s* {.*}"
  65. pegLineTemplate =
  66. peg"""
  67. {[^(]*} '(' {\d+} ', ' {\d+} ') '
  68. 'template/generic instantiation' ( ' of `' [^`]+ '`' )? ' from here' .*
  69. """
  70. pegOtherError = peg"'Error:' \s* {.*}"
  71. pegOfInterest = pegLineError / pegOtherError
  72. var gTargets = {low(TTarget)..high(TTarget)}
  73. proc isSuccess(input: string): bool =
  74. # not clear how to do the equivalent of pkg/regex's: re"FOO(.*?)BAR" in pegs
  75. input.startsWith("Hint: ") and input.endsWith("[SuccessX]")
  76. proc normalizeMsg(s: string): string =
  77. result = newStringOfCap(s.len+1)
  78. for x in splitLines(s):
  79. if result.len > 0: result.add '\L'
  80. result.add x.strip
  81. proc getFileDir(filename: string): string =
  82. result = filename.splitFile().dir
  83. if not result.isAbsolute():
  84. result = getCurrentDir() / result
  85. proc execCmdEx2(command: string, args: openArray[string]; workingDir, input: string = ""): tuple[
  86. cmdLine: string,
  87. output: TaintedString,
  88. exitCode: int] {.tags:
  89. [ExecIOEffect, ReadIOEffect, RootEffect], gcsafe.} =
  90. result.cmdLine.add quoteShell(command)
  91. for arg in args:
  92. result.cmdLine.add ' '
  93. result.cmdLine.add quoteShell(arg)
  94. var p = startProcess(command, workingDir=workingDir, args=args,
  95. options={poStdErrToStdOut, poUsePath})
  96. var outp = outputStream(p)
  97. # There is no way to provide input for the child process
  98. # anymore. Closing it will create EOF on stdin instead of eternal
  99. # blocking.
  100. let instream = inputStream(p)
  101. instream.write(input)
  102. close instream
  103. result.exitCode = -1
  104. var line = newStringOfCap(120).TaintedString
  105. while true:
  106. if outp.readLine(line):
  107. result.output.string.add(line.string)
  108. result.output.string.add("\n")
  109. else:
  110. result.exitCode = peekExitCode(p)
  111. if result.exitCode != -1: break
  112. close(p)
  113. proc nimcacheDir(filename, options: string, target: TTarget): string =
  114. ## Give each test a private nimcache dir so they don't clobber each other's.
  115. let hashInput = options & $target
  116. result = "nimcache" / (filename & '_' & hashInput.getMD5)
  117. proc prepareTestArgs(cmdTemplate, filename, options, nimcache: string,
  118. target: TTarget, extraOptions = ""): seq[string] =
  119. var options = target.defaultOptions & " " & options
  120. # improve pending https://github.com/nim-lang/Nim/issues/14343
  121. if nimcache.len > 0: options.add " " & ("--nimCache:" & nimcache).quoteShell
  122. options.add " " & extraOptions
  123. result = parseCmdLine(cmdTemplate % ["target", targetToCmd[target],
  124. "options", options, "file", filename.quoteShell,
  125. "filedir", filename.getFileDir()])
  126. proc callCompiler(cmdTemplate, filename, options, nimcache: string,
  127. target: TTarget, extraOptions = ""): TSpec =
  128. let c = prepareTestArgs(cmdTemplate, filename, options, nimcache, target,
  129. extraOptions)
  130. result.cmd = quoteShellCommand(c)
  131. var p = startProcess(command=c[0], args=c[1 .. ^1],
  132. options={poStdErrToStdOut, poUsePath})
  133. let outp = p.outputStream
  134. var suc = ""
  135. var err = ""
  136. var tmpl = ""
  137. var x = newStringOfCap(120)
  138. result.nimout = ""
  139. while true:
  140. if outp.readLine(x.TaintedString):
  141. result.nimout.add(x & "\n")
  142. if x =~ pegOfInterest:
  143. # `err` should contain the last error/warning message
  144. err = x
  145. elif x =~ pegLineTemplate and err == "":
  146. # `tmpl` contains the last template expansion before the error
  147. tmpl = x
  148. elif x.isSuccess:
  149. suc = x
  150. elif not running(p):
  151. break
  152. close(p)
  153. result.msg = ""
  154. result.file = ""
  155. result.output = ""
  156. result.line = 0
  157. result.column = 0
  158. result.tfile = ""
  159. result.tline = 0
  160. result.tcolumn = 0
  161. result.err = reNimcCrash
  162. if tmpl =~ pegLineTemplate:
  163. result.tfile = extractFilename(matches[0])
  164. result.tline = parseInt(matches[1])
  165. result.tcolumn = parseInt(matches[2])
  166. if err =~ pegLineError:
  167. result.file = extractFilename(matches[0])
  168. result.line = parseInt(matches[1])
  169. result.column = parseInt(matches[2])
  170. result.msg = matches[3]
  171. elif err =~ pegOtherError:
  172. result.msg = matches[0]
  173. elif suc.isSuccess:
  174. result.err = reSuccess
  175. proc callCCompiler(cmdTemplate, filename, options: string,
  176. target: TTarget): TSpec =
  177. let c = prepareTestArgs(cmdTemplate, filename, options, nimcache = "", target)
  178. var p = startProcess(command="gcc", args=c[5 .. ^1],
  179. options={poStdErrToStdOut, poUsePath})
  180. let outp = p.outputStream
  181. var x = newStringOfCap(120)
  182. result.nimout = ""
  183. result.msg = ""
  184. result.file = ""
  185. result.output = ""
  186. result.line = -1
  187. while true:
  188. if outp.readLine(x.TaintedString):
  189. result.nimout.add(x & "\n")
  190. elif not running(p):
  191. break
  192. close(p)
  193. if p.peekExitCode == 0:
  194. result.err = reSuccess
  195. proc initResults: TResults =
  196. result.total = 0
  197. result.passed = 0
  198. result.skipped = 0
  199. result.data = ""
  200. import macros
  201. macro ignoreStyleEcho(args: varargs[typed]): untyped =
  202. let typForegroundColor = bindSym"ForegroundColor".getType
  203. let typBackgroundColor = bindSym"BackgroundColor".getType
  204. let typStyle = bindSym"Style".getType
  205. let typTerminalCmd = bindSym"TerminalCmd".getType
  206. result = newCall(bindSym"echo")
  207. for arg in children(args):
  208. if arg.kind == nnkNilLit: continue
  209. let typ = arg.getType
  210. if typ.kind != nnkEnumTy or
  211. typ != typForegroundColor and
  212. typ != typBackgroundColor and
  213. typ != typStyle and
  214. typ != typTerminalCmd:
  215. result.add(arg)
  216. template maybeStyledEcho(args: varargs[untyped]): untyped =
  217. if useColors:
  218. styledEcho(args)
  219. else:
  220. ignoreStyleEcho(args)
  221. proc `$`(x: TResults): string =
  222. result = ("Tests passed: $1 / $3 <br />\n" &
  223. "Tests skipped: $2 / $3 <br />\n") %
  224. [$x.passed, $x.skipped, $x.total]
  225. proc addResult(r: var TResults, test: TTest, target: TTarget,
  226. expected, given: string, successOrig: TResultEnum) =
  227. # test.name is easier to find than test.name.extractFilename
  228. # A bit hacky but simple and works with tests/testament/tshould_not_work.nim
  229. var name = test.name.replace(DirSep, '/')
  230. name.add " " & $target
  231. if test.options.len > 0: name.add " " & test.options
  232. let duration = epochTime() - test.startTime
  233. let success = if test.spec.timeout > 0.0 and duration > test.spec.timeout: reTimeout
  234. else: successOrig
  235. let durationStr = duration.formatFloat(ffDecimal, precision = 2).align(5)
  236. if backendLogging:
  237. backend.writeTestResult(name = name,
  238. category = test.cat.string,
  239. target = $target,
  240. action = $test.spec.action,
  241. result = $success,
  242. expected = expected,
  243. given = given)
  244. r.data.addf("$#\t$#\t$#\t$#", name, expected, given, $success)
  245. template disp(msg) =
  246. maybeStyledEcho styleDim, fgYellow, msg & " ", styleBright, fgCyan, name
  247. if success == reSuccess:
  248. maybeStyledEcho fgGreen, "PASS: ", fgCyan, alignLeft(name, 60), fgBlue, " (", durationStr, " sec)"
  249. elif success == reDisabled:
  250. if test.spec.inCurrentBatch: disp("SKIP:")
  251. else: disp("NOTINBATCH:")
  252. elif success == reJoined: disp("JOINED:")
  253. else:
  254. maybeStyledEcho styleBright, fgRed, failString, fgCyan, name
  255. maybeStyledEcho styleBright, fgCyan, "Test \"", test.name, "\"", " in category \"", test.cat.string, "\""
  256. maybeStyledEcho styleBright, fgRed, "Failure: ", $success
  257. if success in {reBuildFailed, reNimcCrash, reInstallFailed}:
  258. # expected is empty, no reason to print it.
  259. echo given
  260. else:
  261. maybeStyledEcho fgYellow, "Expected:"
  262. maybeStyledEcho styleBright, expected, "\n"
  263. maybeStyledEcho fgYellow, "Gotten:"
  264. maybeStyledEcho styleBright, given, "\n"
  265. if backendLogging and (isAppVeyor or isAzure):
  266. let (outcome, msg) =
  267. case success
  268. of reSuccess:
  269. ("Passed", "")
  270. of reDisabled, reJoined:
  271. ("Skipped", "")
  272. of reBuildFailed, reNimcCrash, reInstallFailed:
  273. ("Failed", "Failure: " & $success & "\n" & given)
  274. else:
  275. ("Failed", "Failure: " & $success & "\nExpected:\n" & expected & "\n\n" & "Gotten:\n" & given)
  276. if isAzure:
  277. azure.addTestResult(name, test.cat.string, int(duration * 1000), msg, success)
  278. else:
  279. var p = startProcess("appveyor", args=["AddTest", test.name.replace("\\", "/") & test.options,
  280. "-Framework", "nim-testament", "-FileName",
  281. test.cat.string,
  282. "-Outcome", outcome, "-ErrorMessage", msg,
  283. "-Duration", $(duration*1000).int],
  284. options={poStdErrToStdOut, poUsePath, poParentStreams})
  285. discard waitForExit(p)
  286. close(p)
  287. proc checkForInlineErrors(r: var TResults, expected, given: TSpec, test: TTest, target: TTarget) =
  288. let pegLine = peg"{[^(]*} '(' {\d+} ', ' {\d+} ') ' {[^:]*} ':' \s* {.*}"
  289. var covered = initIntSet()
  290. for line in splitLines(given.nimout):
  291. if line =~ pegLine:
  292. let file = extractFilename(matches[0])
  293. let line = try: parseInt(matches[1]) except: -1
  294. let col = try: parseInt(matches[2]) except: -1
  295. let kind = matches[3]
  296. let msg = matches[4]
  297. if file == extractFilename test.name:
  298. var i = 0
  299. for x in expected.inlineErrors:
  300. if x.line == line and (x.col == col or x.col < 0) and
  301. x.kind == kind and x.msg in msg:
  302. covered.incl i
  303. inc i
  304. block coverCheck:
  305. for j in 0..high(expected.inlineErrors):
  306. if j notin covered:
  307. var e = test.name
  308. e.add "("
  309. e.addInt expected.inlineErrors[j].line
  310. if expected.inlineErrors[j].col > 0:
  311. e.add ", "
  312. e.addInt expected.inlineErrors[j].col
  313. e.add ") "
  314. e.add expected.inlineErrors[j].kind
  315. e.add ": "
  316. e.add expected.inlineErrors[j].msg
  317. r.addResult(test, target, e, given.nimout, reMsgsDiffer)
  318. break coverCheck
  319. r.addResult(test, target, "", given.msg, reSuccess)
  320. inc(r.passed)
  321. proc cmpMsgs(r: var TResults, expected, given: TSpec, test: TTest, target: TTarget) =
  322. if expected.inlineErrors.len > 0:
  323. checkForInlineErrors(r, expected, given, test, target)
  324. elif strip(expected.msg) notin strip(given.msg):
  325. r.addResult(test, target, expected.msg, given.msg, reMsgsDiffer)
  326. elif expected.nimout.len > 0 and expected.nimout.normalizeMsg notin given.nimout.normalizeMsg:
  327. r.addResult(test, target, expected.nimout, given.nimout, reMsgsDiffer)
  328. elif expected.tfile == "" and extractFilename(expected.file) != extractFilename(given.file) and
  329. "internal error:" notin expected.msg:
  330. r.addResult(test, target, expected.file, given.file, reFilesDiffer)
  331. elif expected.line != given.line and expected.line != 0 or
  332. expected.column != given.column and expected.column != 0:
  333. r.addResult(test, target, $expected.line & ':' & $expected.column,
  334. $given.line & ':' & $given.column,
  335. reLinesDiffer)
  336. elif expected.tfile != "" and extractFilename(expected.tfile) != extractFilename(given.tfile) and
  337. "internal error:" notin expected.msg:
  338. r.addResult(test, target, expected.tfile, given.tfile, reFilesDiffer)
  339. elif expected.tline != given.tline and expected.tline != 0 or
  340. expected.tcolumn != given.tcolumn and expected.tcolumn != 0:
  341. r.addResult(test, target, $expected.tline & ':' & $expected.tcolumn,
  342. $given.tline & ':' & $given.tcolumn,
  343. reLinesDiffer)
  344. else:
  345. r.addResult(test, target, expected.msg, given.msg, reSuccess)
  346. inc(r.passed)
  347. proc generatedFile(test: TTest, target: TTarget): string =
  348. if target == targetJS:
  349. result = test.name.changeFileExt("js")
  350. else:
  351. let (_, name, _) = test.name.splitFile
  352. let ext = targetToExt[target]
  353. result = nimcacheDir(test.name, test.options, target) / "@m" & name.changeFileExt(ext)
  354. proc needsCodegenCheck(spec: TSpec): bool =
  355. result = spec.maxCodeSize > 0 or spec.ccodeCheck.len > 0
  356. proc codegenCheck(test: TTest, target: TTarget, spec: TSpec, expectedMsg: var string,
  357. given: var TSpec) =
  358. try:
  359. let genFile = generatedFile(test, target)
  360. let contents = readFile(genFile).string
  361. let check = spec.ccodeCheck
  362. if check.len > 0:
  363. if check[0] == '\\':
  364. # little hack to get 'match' support:
  365. if not contents.match(check.peg):
  366. given.err = reCodegenFailure
  367. elif contents.find(check.peg) < 0:
  368. given.err = reCodegenFailure
  369. expectedMsg = check
  370. if spec.maxCodeSize > 0 and contents.len > spec.maxCodeSize:
  371. given.err = reCodegenFailure
  372. given.msg = "generated code size: " & $contents.len
  373. expectedMsg = "max allowed size: " & $spec.maxCodeSize
  374. except ValueError:
  375. given.err = reInvalidPeg
  376. echo getCurrentExceptionMsg()
  377. except IOError:
  378. given.err = reCodeNotFound
  379. echo getCurrentExceptionMsg()
  380. proc nimoutCheck(test: TTest; expectedNimout: string; given: var TSpec) =
  381. let giv = given.nimout.strip
  382. var currentPos = 0
  383. # Only check that nimout contains all expected lines in that order.
  384. # There may be more output in nimout. It is ignored here.
  385. for line in expectedNimout.strip.splitLines:
  386. currentPos = giv.find(line.strip, currentPos)
  387. if currentPos < 0:
  388. given.err = reMsgsDiffer
  389. break
  390. proc compilerOutputTests(test: TTest, target: TTarget, given: var TSpec,
  391. expected: TSpec; r: var TResults) =
  392. var expectedmsg: string = ""
  393. var givenmsg: string = ""
  394. if given.err == reSuccess:
  395. if expected.needsCodegenCheck:
  396. codegenCheck(test, target, expected, expectedmsg, given)
  397. givenmsg = given.msg
  398. if expected.nimout.len > 0:
  399. expectedmsg = expected.nimout
  400. givenmsg = given.nimout.strip
  401. nimoutCheck(test, expectedmsg, given)
  402. else:
  403. givenmsg = "$ " & given.cmd & "\n" & given.nimout
  404. if given.err == reSuccess: inc(r.passed)
  405. r.addResult(test, target, expectedmsg, givenmsg, given.err)
  406. proc getTestSpecTarget(): TTarget =
  407. if getEnv("NIM_COMPILE_TO_CPP", "false").string == "true":
  408. result = targetCpp
  409. else:
  410. result = targetC
  411. proc checkDisabled(r: var TResults, test: TTest): bool =
  412. if test.spec.err in {reDisabled, reJoined}:
  413. # targetC is a lie, but parameter is required
  414. r.addResult(test, targetC, "", "", test.spec.err)
  415. inc(r.skipped)
  416. inc(r.total)
  417. result = false
  418. else:
  419. result = true
  420. var count = 0
  421. proc testSpecHelper(r: var TResults, test: var TTest, expected: TSpec,
  422. target: TTarget, nimcache: string, extraOptions = "") =
  423. test.startTime = epochTime()
  424. case expected.action
  425. of actionCompile:
  426. var given = callCompiler(expected.getCmd, test.name, test.options, nimcache, target,
  427. extraOptions = " --stdout --hint[Path]:off --hint[Processing]:off")
  428. compilerOutputTests(test, target, given, expected, r)
  429. of actionRun:
  430. var given = callCompiler(expected.getCmd, test.name, test.options,
  431. nimcache, target, extraOptions)
  432. if given.err != reSuccess:
  433. r.addResult(test, target, "", "$ " & given.cmd & "\n" & given.nimout, given.err)
  434. else:
  435. let isJsTarget = target == targetJS
  436. var exeFile = changeFileExt(test.name, if isJsTarget: "js" else: ExeExt)
  437. if not fileExists(exeFile):
  438. r.addResult(test, target, expected.output,
  439. "executable not found: " & exeFile, reExeNotFound)
  440. else:
  441. let nodejs = if isJsTarget: findNodeJs() else: ""
  442. if isJsTarget and nodejs == "":
  443. r.addResult(test, target, expected.output, "nodejs binary not in PATH",
  444. reExeNotFound)
  445. else:
  446. var exeCmd: string
  447. var args = test.args
  448. if isJsTarget:
  449. exeCmd = nodejs
  450. args = concat(@[exeFile], args)
  451. else:
  452. exeCmd = exeFile.dup(normalizeExe)
  453. if expected.useValgrind:
  454. args = @["--error-exitcode=1"] & exeCmd & args
  455. exeCmd = "valgrind"
  456. var (_, buf, exitCode) = execCmdEx2(exeCmd, args, input = expected.input)
  457. # Treat all failure codes from nodejs as 1. Older versions of nodejs used
  458. # to return other codes, but for us it is sufficient to know that it's not 0.
  459. if exitCode != 0: exitCode = 1
  460. let bufB =
  461. if expected.sortoutput:
  462. var x = splitLines(strip(buf.string))
  463. sort(x, system.cmp)
  464. join(x, "\n")
  465. else:
  466. strip(buf.string)
  467. if exitCode != expected.exitCode:
  468. r.addResult(test, target, "exitcode: " & $expected.exitCode,
  469. "exitcode: " & $exitCode & "\n\nOutput:\n" &
  470. bufB, reExitcodesDiffer)
  471. elif (expected.outputCheck == ocEqual and expected.output != bufB) or
  472. (expected.outputCheck == ocSubstr and expected.output notin bufB):
  473. given.err = reOutputsDiffer
  474. r.addResult(test, target, expected.output, bufB, reOutputsDiffer)
  475. else:
  476. compilerOutputTests(test, target, given, expected, r)
  477. of actionReject:
  478. var given = callCompiler(expected.getCmd, test.name, test.options,
  479. nimcache, target)
  480. cmpMsgs(r, expected, given, test, target)
  481. proc targetHelper(r: var TResults, test: TTest, expected: TSpec, extraOptions = "") =
  482. for target in expected.targets:
  483. inc(r.total)
  484. if target notin gTargets:
  485. r.addResult(test, target, "", "", reDisabled)
  486. inc(r.skipped)
  487. elif simulate:
  488. inc count
  489. echo "testSpec count: ", count, " expected: ", expected
  490. else:
  491. let nimcache = nimcacheDir(test.name, test.options, target)
  492. var testClone = test
  493. testSpecHelper(r, testClone, expected, target, nimcache, extraOptions)
  494. proc testSpec(r: var TResults, test: TTest, targets: set[TTarget] = {}) =
  495. var expected = test.spec
  496. if expected.parseErrors.len > 0:
  497. # targetC is a lie, but a parameter is required
  498. r.addResult(test, targetC, "", expected.parseErrors, reInvalidSpec)
  499. inc(r.total)
  500. return
  501. if not checkDisabled(r, test): return
  502. expected.targets.incl targets
  503. # still no target specified at all
  504. if expected.targets == {}:
  505. expected.targets = {getTestSpecTarget()}
  506. if test.spec.matrix.len > 0:
  507. for m in test.spec.matrix:
  508. targetHelper(r, test, expected, m)
  509. else:
  510. targetHelper(r, test, expected)
  511. proc testSpecWithNimcache(r: var TResults, test: TTest; nimcache: string) =
  512. if not checkDisabled(r, test): return
  513. for target in test.spec.targets:
  514. inc(r.total)
  515. var testClone = test
  516. testSpecHelper(r, testClone, test.spec, target, nimcache)
  517. proc testC(r: var TResults, test: TTest, action: TTestAction) =
  518. # runs C code. Doesn't support any specs, just goes by exit code.
  519. if not checkDisabled(r, test): return
  520. let tname = test.name.addFileExt(".c")
  521. inc(r.total)
  522. maybeStyledEcho "Processing ", fgCyan, extractFilename(tname)
  523. var given = callCCompiler(getCmd(TSpec()), test.name & ".c", test.options, targetC)
  524. if given.err != reSuccess:
  525. r.addResult(test, targetC, "", given.msg, given.err)
  526. elif action == actionRun:
  527. let exeFile = changeFileExt(test.name, ExeExt)
  528. var (_, exitCode) = execCmdEx(exeFile, options = {poStdErrToStdOut, poUsePath})
  529. if exitCode != 0: given.err = reExitcodesDiffer
  530. if given.err == reSuccess: inc(r.passed)
  531. proc testExec(r: var TResults, test: TTest) =
  532. # runs executable or script, just goes by exit code
  533. if not checkDisabled(r, test): return
  534. inc(r.total)
  535. let (outp, errC) = execCmdEx(test.options.strip())
  536. var given: TSpec
  537. if errC == 0:
  538. given.err = reSuccess
  539. else:
  540. given.err = reExitcodesDiffer
  541. given.msg = outp.string
  542. if given.err == reSuccess: inc(r.passed)
  543. r.addResult(test, targetC, "", given.msg, given.err)
  544. proc makeTest(test, options: string, cat: Category): TTest =
  545. result.cat = cat
  546. result.name = test
  547. result.options = options
  548. result.spec = parseSpec(addFileExt(test, ".nim"))
  549. result.startTime = epochTime()
  550. proc makeRawTest(test, options: string, cat: Category): TTest =
  551. result.cat = cat
  552. result.name = test
  553. result.options = options
  554. result.spec = initSpec(addFileExt(test, ".nim"))
  555. result.startTime = epochTime()
  556. result.spec.action = actionCompile
  557. result.spec.targets = {getTestSpecTarget()}
  558. # TODO: fix these files
  559. const disabledFilesDefault = @[
  560. "LockFreeHash.nim",
  561. "sharedstrings.nim",
  562. "tableimpl.nim",
  563. "setimpl.nim",
  564. "hashcommon.nim",
  565. # Requires compiling with '--threads:on`
  566. "sharedlist.nim",
  567. "sharedtables.nim",
  568. # Error: undeclared identifier: 'hasThreadSupport'
  569. "ioselectors_epoll.nim",
  570. "ioselectors_kqueue.nim",
  571. "ioselectors_poll.nim",
  572. # Error: undeclared identifier: 'Timeval'
  573. "ioselectors_select.nim",
  574. ]
  575. when defined(windows):
  576. const
  577. # array of modules disabled from compilation test of stdlib.
  578. disabledFiles = disabledFilesDefault & @["coro.nim"]
  579. else:
  580. const
  581. # array of modules disabled from compilation test of stdlib.
  582. disabledFiles = disabledFilesDefault
  583. include categories
  584. proc loadSkipFrom(name: string): seq[string] =
  585. if name.len == 0: return
  586. # One skip per line, comments start with #
  587. # used by `nlvm` (at least)
  588. for line in lines(name):
  589. let sline = line.strip()
  590. if sline.len > 0 and not sline.startsWith("#"):
  591. result.add sline
  592. proc main() =
  593. os.putEnv "NIMTEST_COLOR", "never"
  594. os.putEnv "NIMTEST_OUTPUT_LVL", "PRINT_FAILURES"
  595. azure.init()
  596. backend.open()
  597. var optPrintResults = false
  598. var optFailing = false
  599. var targetsStr = ""
  600. var isMainProcess = true
  601. var skipFrom = ""
  602. var useMegatest = true
  603. var p = initOptParser()
  604. p.next()
  605. while p.kind in {cmdLongOption, cmdShortOption}:
  606. case p.key.string.normalize
  607. of "print", "verbose": optPrintResults = true
  608. of "failing": optFailing = true
  609. of "pedantic": discard "now always enabled"
  610. of "targets":
  611. targetsStr = p.val.string
  612. gTargets = parseTargets(targetsStr)
  613. of "nim":
  614. compilerPrefix = addFileExt(p.val.string, ExeExt)
  615. of "directory":
  616. setCurrentDir(p.val.string)
  617. of "colors":
  618. case p.val.string:
  619. of "on":
  620. useColors = true
  621. of "off":
  622. useColors = false
  623. else:
  624. quit Usage
  625. of "batch":
  626. testamentData0.batchArg = p.val
  627. if p.val != "_":
  628. let s = p.val.split("_")
  629. doAssert s.len == 2, $(p.val, s)
  630. testamentData0.testamentBatch = s[0].parseInt
  631. testamentData0.testamentNumBatch = s[1].parseInt
  632. doAssert testamentData0.testamentNumBatch > 0
  633. doAssert testamentData0.testamentBatch >= 0 and testamentData0.testamentBatch < testamentData0.testamentNumBatch
  634. of "simulate":
  635. simulate = true
  636. of "megatest":
  637. case p.val.string:
  638. of "on":
  639. useMegatest = true
  640. of "off":
  641. useMegatest = false
  642. else:
  643. quit Usage
  644. of "backendlogging":
  645. case p.val.string:
  646. of "on":
  647. backendLogging = true
  648. of "off":
  649. backendLogging = false
  650. else:
  651. quit Usage
  652. of "skipfrom":
  653. skipFrom = p.val.string
  654. else:
  655. quit Usage
  656. p.next()
  657. if p.kind != cmdArgument:
  658. quit Usage
  659. var action = p.key.string.normalize
  660. p.next()
  661. var r = initResults()
  662. case action
  663. of "all":
  664. #processCategory(r, Category"megatest", p.cmdLineRest.string, testsDir, runJoinableTests = false)
  665. var myself = quoteShell(getAppFilename())
  666. if targetsStr.len > 0:
  667. myself &= " " & quoteShell("--targets:" & targetsStr)
  668. myself &= " " & quoteShell("--nim:" & compilerPrefix)
  669. if testamentData0.batchArg.len > 0:
  670. myself &= " --batch:" & testamentData0.batchArg
  671. if skipFrom.len > 0:
  672. myself &= " " & quoteShell("--skipFrom:" & skipFrom)
  673. var cats: seq[string]
  674. let rest = if p.cmdLineRest.string.len > 0: " " & p.cmdLineRest.string else: ""
  675. for kind, dir in walkDir(testsDir):
  676. assert testsDir.startsWith(testsDir)
  677. let cat = dir[testsDir.len .. ^1]
  678. if kind == pcDir and cat notin ["testdata", "nimcache"]:
  679. cats.add cat
  680. cats.add AdditionalCategories
  681. if useMegatest: cats.add MegaTestCat
  682. var cmds: seq[string]
  683. for cat in cats:
  684. let runtype = if useMegatest: " pcat " else: " cat "
  685. cmds.add(myself & runtype & quoteShell(cat) & rest)
  686. proc progressStatus(idx: int) =
  687. echo "progress[all]: $1/$2 starting: cat: $3" % [$idx, $cats.len, cats[idx]]
  688. if simulate:
  689. skips = loadSkipFrom(skipFrom)
  690. for i, cati in cats:
  691. progressStatus(i)
  692. processCategory(r, Category(cati), p.cmdLineRest.string, testsDir, runJoinableTests = false)
  693. else:
  694. addQuitProc azure.finalize
  695. quit osproc.execProcesses(cmds, {poEchoCmd, poStdErrToStdOut, poUsePath, poParentStreams}, beforeRunEvent = progressStatus)
  696. of "c", "cat", "category":
  697. skips = loadSkipFrom(skipFrom)
  698. var cat = Category(p.key)
  699. processCategory(r, cat, p.cmdLineRest.string, testsDir, runJoinableTests = true)
  700. of "pcat":
  701. skips = loadSkipFrom(skipFrom)
  702. # 'pcat' is used for running a category in parallel. Currently the only
  703. # difference is that we don't want to run joinable tests here as they
  704. # are covered by the 'megatest' category.
  705. isMainProcess = false
  706. var cat = Category(p.key)
  707. p.next
  708. processCategory(r, cat, p.cmdLineRest.string, testsDir, runJoinableTests = false)
  709. of "p", "pat", "pattern":
  710. skips = loadSkipFrom(skipFrom)
  711. let pattern = p.key
  712. p.next
  713. processPattern(r, pattern, p.cmdLineRest.string, simulate)
  714. of "r", "run":
  715. var subPath = p.key.string
  716. if subPath.isAbsolute: subPath = subPath.relativePath(getCurrentDir())
  717. # at least one directory is required in the path, to use as a category name
  718. let pathParts = split(subPath, {DirSep, AltSep})
  719. # "stdlib/nre/captures.nim" -> "stdlib" + "nre/captures.nim"
  720. let cat = Category(pathParts[0])
  721. subPath = joinPath(pathParts[1..^1])
  722. processSingleTest(r, cat, p.cmdLineRest.string, subPath)
  723. of "html":
  724. generateHtml(resultsFile, optFailing)
  725. else:
  726. quit Usage
  727. if optPrintResults:
  728. if action == "html": openDefaultBrowser(resultsFile)
  729. else: echo r, r.data
  730. azure.finalize()
  731. backend.close()
  732. var failed = r.total - r.passed - r.skipped
  733. if failed != 0:
  734. echo "FAILURE! total: ", r.total, " passed: ", r.passed, " skipped: ",
  735. r.skipped, " failed: ", failed
  736. quit(QuitFailure)
  737. if isMainProcess:
  738. echo "Used ", compilerPrefix, " to run the tests. Use --nim to override."
  739. if paramCount() == 0:
  740. quit Usage
  741. main()