diff options
Diffstat (limited to 'testament/testament.nim')
-rw-r--r-- | testament/testament.nim | 733 |
1 files changed, 733 insertions, 0 deletions
diff --git a/testament/testament.nim b/testament/testament.nim new file mode 100644 index 000000000..5d0213ce5 --- /dev/null +++ b/testament/testament.nim @@ -0,0 +1,733 @@ +# +# +# Nim Testament +# (c) Copyright 2017 Andreas Rumpf +# +# See the file "copying.txt", included in this +# distribution, for details about the copyright. +# + +## This program verifies Nim against the testcases. + +import + strutils, pegs, os, osproc, streams, json, + backend, parseopt, specs, htmlgen, browsers, terminal, + algorithm, times, md5, sequtils + +include compiler/nodejs + +var useColors = true +var backendLogging = true +var simulate = false + +const + testsDir = "tests" & DirSep + resultsFile = "testresults.html" + #jsonFile = "testresults.json" # not used + Usage = """Usage: + testament [options] command [arguments] + +Command: + p|pat|pattern <glob> run all the tests matching the given pattern + all run all tests + c|cat|category <category> run all the tests of a certain category + r|run <test> run single test file + html generate $1 from the database + stats generate statistics about test cases +Arguments: + arguments are passed to the compiler +Options: + --print also print results to the console + --simulate see what tests would be run but don't run them (for debugging) + --failing only show failing/ignored tests + --targets:"c c++ js objc" run tests for specified targets (default: all) + --nim:path use a particular nim executable (default: $$PATH/nim) + --directory:dir Change to directory dir before reading the tests or doing anything else. + --colors:on|off Turn messagescoloring on|off. + --backendLogging:on|off Disable or enable backend logging. By default turned on. + --megatest:on|off Enable or disable megatest. Default is on. + --skipFrom:file Read tests to skip from `file` - one test per line, # comments ignored +""" % resultsFile + +type + Category = distinct string + TResults = object + total, passed, skipped: int + data: string + + TTest = object + name: string + cat: Category + options: string + args: seq[string] + spec: TSpec + startTime: float + +# ---------------------------------------------------------------------------- + +let + pegLineError = + peg"{[^(]*} '(' {\d+} ', ' {\d+} ') ' ('Error') ':' \s* {.*}" + pegLineTemplate = + peg""" + {[^(]*} '(' {\d+} ', ' {\d+} ') ' + 'template/generic instantiation' ( ' of `' [^`]+ '`' )? ' from here' .* + """ + pegOtherError = peg"'Error:' \s* {.*}" + pegSuccess = peg"'Hint: operation successful'.*" + pegOfInterest = pegLineError / pegOtherError + +var gTargets = {low(TTarget)..high(TTarget)} + +proc normalizeMsg(s: string): string = + result = newStringOfCap(s.len+1) + for x in splitLines(s): + if result.len > 0: result.add '\L' + result.add x.strip + +proc getFileDir(filename: string): string = + result = filename.splitFile().dir + if not result.isAbsolute(): + result = getCurrentDir() / result + +proc execCmdEx2(command: string, args: openarray[string]; workingDir, input: string = ""): tuple[ + cmdLine: string, + output: TaintedString, + exitCode: int] {.tags: + [ExecIOEffect, ReadIOEffect, RootEffect], gcsafe.} = + + result.cmdLine.add quoteShell(command) + for arg in args: + result.cmdLine.add ' ' + result.cmdLine.add quoteShell(arg) + + var p = startProcess(command, workingDir=workingDir, args=args, options={poStdErrToStdOut, poUsePath}) + var outp = outputStream(p) + + # There is no way to provide input for the child process + # anymore. Closing it will create EOF on stdin instead of eternal + # blocking. + let instream = inputStream(p) + instream.write(input) + close instream + + result.exitCode = -1 + var line = newStringOfCap(120).TaintedString + while true: + if outp.readLine(line): + result.output.string.add(line.string) + result.output.string.add("\n") + else: + result.exitCode = peekExitCode(p) + if result.exitCode != -1: break + close(p) + +proc nimcacheDir(filename, options: string, target: TTarget): string = + ## Give each test a private nimcache dir so they don't clobber each other's. + let hashInput = options & $target + result = "nimcache" / (filename & '_' & hashInput.getMD5) + +proc prepareTestArgs(cmdTemplate, filename, options, nimcache: string, + target: TTarget, extraOptions=""): seq[string] = + let options = options & " " & quoteShell("--nimCache:" & nimcache) & extraOptions + result = parseCmdLine(cmdTemplate % ["target", targetToCmd[target], + "options", options, "file", filename.quoteShell, + "filedir", filename.getFileDir()]) + +proc callCompiler(cmdTemplate, filename, options, nimcache: string, + target: TTarget, + extraOptions=""): TSpec = + let c = prepareTestArgs(cmdTemplate, filename, options, nimcache, target, + extraOptions) + result.cmd = quoteShellCommand(c) + var p = startProcess(command=c[0], args=c[1 .. ^1], + options={poStdErrToStdOut, poUsePath}) + let outp = p.outputStream + var suc = "" + var err = "" + var tmpl = "" + var x = newStringOfCap(120) + result.nimout = "" + while true: + if outp.readLine(x.TaintedString): + result.nimout.add(x & "\n") + if x =~ pegOfInterest: + # `err` should contain the last error/warning message + err = x + elif x =~ pegLineTemplate and err == "": + # `tmpl` contains the last template expansion before the error + tmpl = x + elif x =~ pegSuccess: + suc = x + elif not running(p): + break + close(p) + result.msg = "" + result.file = "" + result.output = "" + result.line = 0 + result.column = 0 + result.tfile = "" + result.tline = 0 + result.tcolumn = 0 + if tmpl =~ pegLineTemplate: + result.tfile = extractFilename(matches[0]) + result.tline = parseInt(matches[1]) + result.tcolumn = parseInt(matches[2]) + if err =~ pegLineError: + result.file = extractFilename(matches[0]) + result.line = parseInt(matches[1]) + result.column = parseInt(matches[2]) + result.msg = matches[3] + elif err =~ pegOtherError: + result.msg = matches[0] + elif suc =~ pegSuccess: + result.err = reSuccess + +proc callCCompiler(cmdTemplate, filename, options: string, + target: TTarget): TSpec = + let c = parseCmdLine(cmdTemplate % ["target", targetToCmd[target], + "options", options, "file", filename.quoteShell, + "filedir", filename.getFileDir()]) + var p = startProcess(command="gcc", args=c[5 .. ^1], + options={poStdErrToStdOut, poUsePath}) + let outp = p.outputStream + var x = newStringOfCap(120) + result.nimout = "" + result.msg = "" + result.file = "" + result.output = "" + result.line = -1 + while true: + if outp.readLine(x.TaintedString): + result.nimout.add(x & "\n") + elif not running(p): + break + close(p) + if p.peekExitCode == 0: + result.err = reSuccess + +proc initResults: TResults = + result.total = 0 + result.passed = 0 + result.skipped = 0 + result.data = "" + +import macros + +macro ignoreStyleEcho(args: varargs[typed]): untyped = + let typForegroundColor = bindSym"ForegroundColor".getType + let typBackgroundColor = bindSym"BackgroundColor".getType + let typStyle = bindSym"Style".getType + let typTerminalCmd = bindSym"TerminalCmd".getType + result = newCall(bindSym"echo") + for arg in children(args): + if arg.kind == nnkNilLit: continue + let typ = arg.getType + if typ.kind != nnkEnumTy or + typ != typForegroundColor and + typ != typBackgroundColor and + typ != typStyle and + typ != typTerminalCmd: + result.add(arg) + +template maybeStyledEcho(args: varargs[untyped]): untyped = + if useColors: + styledEcho(args) + else: + ignoreStyleEcho(args) + + +proc `$`(x: TResults): string = + result = ("Tests passed: $1 / $3 <br />\n" & + "Tests skipped: $2 / $3 <br />\n") % + [$x.passed, $x.skipped, $x.total] + +proc addResult(r: var TResults, test: TTest, target: TTarget, + expected, given: string, success: TResultEnum) = + # test.name is easier to find than test.name.extractFilename + # A bit hacky but simple and works with tests/testament/tshouldfail.nim + var name = test.name.replace(DirSep, '/') + name.add " " & $target & test.options + + let duration = epochTime() - test.startTime + let durationStr = duration.formatFloat(ffDecimal, precision = 8).align(11) + if backendLogging: + backend.writeTestResult(name = name, + category = test.cat.string, + target = $target, + action = $test.spec.action, + result = $success, + expected = expected, + given = given) + r.data.addf("$#\t$#\t$#\t$#", name, expected, given, $success) + if success == reSuccess: + maybeStyledEcho fgGreen, "PASS: ", fgCyan, alignLeft(name, 60), fgBlue, " (", durationStr, " secs)" + elif success == reDisabled: + maybeStyledEcho styleDim, fgYellow, "SKIP: ", styleBright, fgCyan, name + elif success == reJoined: + maybeStyledEcho styleDim, fgYellow, "JOINED: ", styleBright, fgCyan, name + else: + maybeStyledEcho styleBright, fgRed, "FAIL: ", fgCyan, name + maybeStyledEcho styleBright, fgCyan, "Test \"", test.name, "\"", " in category \"", test.cat.string, "\"" + maybeStyledEcho styleBright, fgRed, "Failure: ", $success + if success in {reBuildFailed, reNimcCrash, reInstallFailed}: + # expected is empty, no reason to print it. + echo given + else: + maybeStyledEcho fgYellow, "Expected:" + maybeStyledEcho styleBright, expected, "\n" + maybeStyledEcho fgYellow, "Gotten:" + maybeStyledEcho styleBright, given, "\n" + + + if backendLogging and existsEnv("APPVEYOR"): + let (outcome, msg) = + case success + of reSuccess: + ("Passed", "") + of reDisabled, reJoined: + ("Skipped", "") + of reBuildFailed, reNimcCrash, reInstallFailed: + ("Failed", "Failure: " & $success & "\n" & given) + else: + ("Failed", "Failure: " & $success & "\nExpected:\n" & expected & "\n\n" & "Gotten:\n" & given) + 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}) + discard waitForExit(p) + close(p) + +proc cmpMsgs(r: var TResults, expected, given: TSpec, test: TTest, target: TTarget) = + if strip(expected.msg) notin strip(given.msg): + r.addResult(test, target, expected.msg, given.msg, reMsgsDiffer) + elif expected.nimout.len > 0 and expected.nimout.normalizeMsg notin given.nimout.normalizeMsg: + r.addResult(test, target, expected.nimout, given.nimout, reMsgsDiffer) + elif expected.tfile == "" and extractFilename(expected.file) != extractFilename(given.file) and + "internal error:" notin expected.msg: + r.addResult(test, target, expected.file, given.file, reFilesDiffer) + elif expected.line != given.line and expected.line != 0 or + expected.column != given.column and expected.column != 0: + r.addResult(test, target, $expected.line & ':' & $expected.column, + $given.line & ':' & $given.column, + reLinesDiffer) + elif expected.tfile != "" and extractFilename(expected.tfile) != extractFilename(given.tfile) and + "internal error:" notin expected.msg: + r.addResult(test, target, expected.tfile, given.tfile, reFilesDiffer) + elif expected.tline != given.tline and expected.tline != 0 or + expected.tcolumn != given.tcolumn and expected.tcolumn != 0: + r.addResult(test, target, $expected.tline & ':' & $expected.tcolumn, + $given.tline & ':' & $given.tcolumn, + reLinesDiffer) + else: + r.addResult(test, target, expected.msg, given.msg, reSuccess) + inc(r.passed) + +proc generatedFile(test: TTest, target: TTarget): string = + if target == targetJS: + result = test.name.changeFileExt("js") + else: + let (_, name, _) = test.name.splitFile + let ext = targetToExt[target] + result = nimcacheDir(test.name, test.options, target) / + name.replace("_", "__").changeFileExt(ext) + +proc needsCodegenCheck(spec: TSpec): bool = + result = spec.maxCodeSize > 0 or spec.ccodeCheck.len > 0 + +proc codegenCheck(test: TTest, target: TTarget, spec: TSpec, expectedMsg: var string, + given: var TSpec) = + try: + let genFile = generatedFile(test, target) + let contents = readFile(genFile).string + let check = spec.ccodeCheck + if check.len > 0: + if check[0] == '\\': + # little hack to get 'match' support: + if not contents.match(check.peg): + given.err = reCodegenFailure + elif contents.find(check.peg) < 0: + given.err = reCodegenFailure + expectedMsg = check + if spec.maxCodeSize > 0 and contents.len > spec.maxCodeSize: + given.err = reCodegenFailure + given.msg = "generated code size: " & $contents.len + expectedMsg = "max allowed size: " & $spec.maxCodeSize + except ValueError: + given.err = reInvalidPeg + echo getCurrentExceptionMsg() + except IOError: + given.err = reCodeNotFound + echo getCurrentExceptionMsg() + +proc nimoutCheck(test: TTest; expectedNimout: string; given: var TSpec) = + let giv = given.nimout.strip + var currentPos = 0 + # Only check that nimout contains all expected lines in that order. + # There may be more output in nimout. It is ignored here. + for line in expectedNimout.strip.splitLines: + currentPos = giv.find(line.strip, currentPos) + if currentPos < 0: + given.err = reMsgsDiffer + break + +proc compilerOutputTests(test: TTest, target: TTarget, given: var TSpec, + expected: TSpec; r: var TResults) = + var expectedmsg: string = "" + var givenmsg: string = "" + if given.err == reSuccess: + if expected.needsCodegenCheck: + codegenCheck(test, target, expected, expectedmsg, given) + givenmsg = given.msg + if expected.nimout.len > 0: + expectedmsg = expected.nimout + givenmsg = given.nimout.strip + nimoutCheck(test, expectedmsg, given) + else: + givenmsg = "$ " & given.cmd & "\n" & given.nimout + if given.err == reSuccess: inc(r.passed) + r.addResult(test, target, expectedmsg, givenmsg, given.err) + +proc getTestSpecTarget(): TTarget = + if getEnv("NIM_COMPILE_TO_CPP", "false").string == "true": + result = targetCpp + else: + result = targetC + +proc checkDisabled(r: var TResults, test: TTest): bool = + if test.spec.err in {reDisabled, reJoined}: + # targetC is a lie, but parameter is required + r.addResult(test, targetC, "", "", test.spec.err) + inc(r.skipped) + inc(r.total) + result = false + else: + result = true + +var count = 0 + +proc testSpecHelper(r: var TResults, test: TTest, expected: TSpec, target: TTarget, nimcache: string) = + case expected.action + of actionCompile: + var given = callCompiler(expected.getCmd, test.name, test.options, nimcache, target, + extraOptions = " --stdout --hint[Path]:off --hint[Processing]:off") + compilerOutputTests(test, target, given, expected, r) + of actionRun: + var given = callCompiler(expected.getCmd, test.name, test.options, nimcache, target) + if given.err != reSuccess: + r.addResult(test, target, "", "$ " & given.cmd & "\n" & given.nimout, given.err) + else: + let isJsTarget = target == targetJS + var exeFile = changeFileExt(test.name, if isJsTarget: "js" else: ExeExt) + if not existsFile(exeFile): + r.addResult(test, target, expected.output, + "executable not found: " & exeFile, reExeNotFound) + else: + let nodejs = if isJsTarget: findNodeJs() else: "" + if isJsTarget and nodejs == "": + r.addResult(test, target, expected.output, "nodejs binary not in PATH", + reExeNotFound) + else: + var exeCmd: string + var args = test.args + if isJsTarget: + exeCmd = nodejs + args = concat(@[exeFile], args) + elif defined(posix) and not exeFile.contains('/'): + # "security" in Posix is actually just a euphemism + # for "unproductive arbitrary shit" + exeCmd = "./" & exeFile + else: + exeCmd = exeFile + var (_, buf, exitCode) = execCmdEx2(exeCmd, args, input = expected.input) + # Treat all failure codes from nodejs as 1. Older versions of nodejs used + # to return other codes, but for us it is sufficient to know that it's not 0. + if exitCode != 0: exitCode = 1 + let bufB = + if expected.sortoutput: + var x = splitLines(strip(buf.string)) + sort(x, system.cmp) + join(x, "\n") + else: + strip(buf.string) + if exitCode != expected.exitCode: + r.addResult(test, target, "exitcode: " & $expected.exitCode, + "exitcode: " & $exitCode & "\n\nOutput:\n" & + bufB, reExitCodesDiffer) + elif (expected.outputCheck == ocEqual and expected.output != bufB) or + (expected.outputCheck == ocSubstr and expected.output notin bufB): + given.err = reOutputsDiffer + r.addResult(test, target, expected.output, bufB, reOutputsDiffer) + else: + compilerOutputTests(test, target, given, expected, r) + of actionReject: + var given = callCompiler(expected.getCmd, test.name, test.options, + nimcache, target) + cmpMsgs(r, expected, given, test, target) + + +proc testSpec(r: var TResults, test: TTest, targets: set[TTarget] = {}) = + var expected = test.spec + if expected.parseErrors.len > 0: + # targetC is a lie, but a parameter is required + r.addResult(test, targetC, "", expected.parseErrors, reInvalidSpec) + inc(r.total) + return + if not checkDisabled(r, test): return + + expected.targets.incl targets + # still no target specified at all + if expected.targets == {}: + expected.targets = {getTestSpecTarget()} + for target in expected.targets: + inc(r.total) + if target notin gTargets: + r.addResult(test, target, "", "", reDisabled) + inc(r.skipped) + elif simulate: + inc count + echo "testSpec count: ", count, " expected: ", expected + else: + let nimcache = nimcacheDir(test.name, test.options, target) + testSpecHelper(r, test, expected, target, nimcache) + +proc testSpecWithNimcache(r: var TResults, test: TTest; nimcache: string) = + if not checkDisabled(r, test): return + for target in test.spec.targets: + inc(r.total) + testSpecHelper(r, test, test.spec, target, nimcache) + +proc testC(r: var TResults, test: TTest, action: TTestAction) = + # runs C code. Doesn't support any specs, just goes by exit code. + if not checkDisabled(r, test): return + + let tname = test.name.addFileExt(".c") + inc(r.total) + maybeStyledEcho "Processing ", fgCyan, extractFilename(tname) + var given = callCCompiler(getCmd(TSpec()), test.name & ".c", test.options, targetC) + if given.err != reSuccess: + r.addResult(test, targetC, "", given.msg, given.err) + elif action == actionRun: + let exeFile = changeFileExt(test.name, ExeExt) + var (_, exitCode) = execCmdEx(exeFile, options = {poStdErrToStdOut, poUsePath}) + if exitCode != 0: given.err = reExitCodesDiffer + if given.err == reSuccess: inc(r.passed) + +proc testExec(r: var TResults, test: TTest) = + # runs executable or script, just goes by exit code + if not checkDisabled(r, test): return + + inc(r.total) + let (outp, errC) = execCmdEx(test.options.strip()) + var given: TSpec + if errC == 0: + given.err = reSuccess + else: + given.err = reExitCodesDiffer + given.msg = outp.string + + if given.err == reSuccess: inc(r.passed) + r.addResult(test, targetC, "", given.msg, given.err) + +proc makeTest(test, options: string, cat: Category): TTest = + result.cat = cat + result.name = test + result.options = options + result.spec = parseSpec(addFileExt(test, ".nim")) + result.startTime = epochTime() + +proc makeRawTest(test, options: string, cat: Category): TTest = + result.cat = cat + result.name = test + result.options = options + result.spec = initSpec(addFileExt(test, ".nim")) + result.startTime = epochTime() + result.spec.action = actionCompile + result.spec.targets = {getTestSpecTarget()} + +# TODO: fix these files +const disabledFilesDefault = @[ + "LockFreeHash.nim", + "sharedstrings.nim", + "tableimpl.nim", + "setimpl.nim", + "hashcommon.nim", + + # Error: undeclared identifier: 'hasThreadSupport' + "ioselectors_epoll.nim", + "ioselectors_kqueue.nim", + "ioselectors_poll.nim", + + # Error: undeclared identifier: 'Timeval' + "ioselectors_select.nim", +] + +when defined(windows): + const + # array of modules disabled from compilation test of stdlib. + disabledFiles = disabledFilesDefault & @["coro.nim"] +else: + const + # array of modules disabled from compilation test of stdlib. + disabledFiles = disabledFilesDefault + +include categories + +proc loadSkipFrom(name: string): seq[string] = + if name.len == 0: return + # One skip per line, comments start with # + # used by `nlvm` (at least) + for line in lines(name): + let sline = line.strip() + if sline.len > 0 and not sline.startsWith("#"): + result.add sline + +proc main() = + os.putenv "NIMTEST_COLOR", "never" + os.putenv "NIMTEST_OUTPUT_LVL", "PRINT_FAILURES" + + backend.open() + var optPrintResults = false + var optFailing = false + var targetsStr = "" + var isMainProcess = true + var skipFrom = "" + var useMegatest = true + + var p = initOptParser() + p.next() + while p.kind in {cmdLongoption, cmdShortOption}: + case p.key.string.normalize + of "print", "verbose": optPrintResults = true + of "failing": optFailing = true + of "pedantic": discard "now always enabled" + of "targets": + targetsStr = p.val.string + gTargets = parseTargets(targetsStr) + of "nim": + compilerPrefix = addFileExt(p.val.string, ExeExt) + of "directory": + setCurrentDir(p.val.string) + of "colors": + case p.val.string: + of "on": + useColors = true + of "off": + useColors = false + else: + quit Usage + of "simulate": + simulate = true + of "megatest": + case p.val.string: + of "on": + useMegatest = true + of "off": + useMegatest = false + else: + quit Usage + of "backendlogging": + case p.val.string: + of "on": + backendLogging = true + of "off": + backendLogging = false + else: + quit Usage + of "skipfrom": + skipFrom = p.val.string + else: + quit Usage + p.next() + if p.kind != cmdArgument: + quit Usage + var action = p.key.string.normalize + p.next() + var r = initResults() + case action + of "all": + #processCategory(r, Category"megatest", p.cmdLineRest.string, testsDir, runJoinableTests = false) + + var myself = quoteShell(findExe("testament" / "testament")) + if targetsStr.len > 0: + myself &= " " & quoteShell("--targets:" & targetsStr) + + myself &= " " & quoteShell("--nim:" & compilerPrefix) + + if skipFrom.len > 0: + myself &= " " & quoteShell("--skipFrom:" & skipFrom) + + var cats: seq[string] + let rest = if p.cmdLineRest.string.len > 0: " " & p.cmdLineRest.string else: "" + for kind, dir in walkDir(testsDir): + assert testsDir.startsWith(testsDir) + let cat = dir[testsDir.len .. ^1] + if kind == pcDir and cat notin ["testdata", "nimcache"]: + cats.add cat + cats.add AdditionalCategories + if useMegatest: cats.add MegaTestCat + + var cmds: seq[string] + for cat in cats: + let runtype = if useMegatest: " pcat " else: " cat " + cmds.add(myself & runtype & quoteShell(cat) & rest) + + proc progressStatus(idx: int) = + echo "progress[all]: i: " & $idx & " / " & $cats.len & " cat: " & cats[idx] + + if simulate: + skips = loadSkipFrom(skipFrom) + for i, cati in cats: + progressStatus(i) + processCategory(r, Category(cati), p.cmdLineRest.string, testsDir, runJoinableTests = false) + else: + quit osproc.execProcesses(cmds, {poEchoCmd, poStdErrToStdOut, poUsePath, poParentStreams}, beforeRunEvent = progressStatus) + of "c", "cat", "category": + skips = loadSkipFrom(skipFrom) + var cat = Category(p.key) + p.next + processCategory(r, cat, p.cmdLineRest.string, testsDir, runJoinableTests = true) + of "pcat": + skips = loadSkipFrom(skipFrom) + # 'pcat' is used for running a category in parallel. Currently the only + # difference is that we don't want to run joinable tests here as they + # are covered by the 'megatest' category. + isMainProcess = false + var cat = Category(p.key) + p.next + processCategory(r, cat, p.cmdLineRest.string, testsDir, runJoinableTests = false) + of "p", "pat", "pattern": + skips = loadSkipFrom(skipFrom) + let pattern = p.key + p.next + processPattern(r, pattern, p.cmdLineRest.string, simulate) + of "r", "run": + # at least one directory is required in the path, to use as a category name + let pathParts = split(p.key.string, {DirSep, AltSep}) + # "stdlib/nre/captures.nim" -> "stdlib" + "nre/captures.nim" + let cat = Category(pathParts[0]) + let subPath = joinPath(pathParts[1..^1]) + processSingleTest(r, cat, p.cmdLineRest.string, subPath) + of "html": + generateHtml(resultsFile, optFailing) + else: + quit Usage + + if optPrintResults: + if action == "html": openDefaultBrowser(resultsFile) + else: echo r, r.data + backend.close() + var failed = r.total - r.passed - r.skipped + if failed != 0: + echo "FAILURE! total: ", r.total, " passed: ", r.passed, " skipped: ", + r.skipped, " failed: ", failed + quit(QuitFailure) + if isMainProcess: + echo "Used ", compilerPrefix, " to run the tests. Use --nim to override." + +if paramCount() == 0: + quit Usage +main() |