diff options
Diffstat (limited to 'lib/pure/unittest.nim')
-rw-r--r-- | lib/pure/unittest.nim | 830 |
1 files changed, 691 insertions, 139 deletions
diff --git a/lib/pure/unittest.nim b/lib/pure/unittest.nim index 3bf4724b9..cfb762258 100644 --- a/lib/pure/unittest.nim +++ b/lib/pure/unittest.nim @@ -9,13 +9,76 @@ ## :Author: Zahary Karadjov ## -## This module implements boilerplate to make testing easy. +## This module implements boilerplate to make unit testing easy. ## -## Example: +## The test status and name is printed after any output or traceback. ## -## .. code:: nim +## Tests can be nested, however failure of a nested test will not mark the +## parent test as failed. Setup and teardown are inherited. Setup can be +## overridden locally. ## +## Compiled test files as well as `nim c -r <testfile.nim>` +## exit with 0 for success (no failed tests) or 1 for failure. +## +## Testament +## ========= +## +## Instead of `unittest`, please consider using +## `the Testament tool <testament.html>`_ which offers process isolation for your tests. +## +## Alternatively using `when isMainModule: doAssert conditionHere` is usually a +## much simpler solution for testing purposes. +## +## Running a single test +## ===================== +## +## Specify the test name as a command line argument. +## +## ```cmd +## nim c -r test "my test name" "another test" +## ``` +## +## Multiple arguments can be used. +## +## Running a single test suite +## =========================== +## +## Specify the suite name delimited by `"::"`. +## +## ```cmd +## nim c -r test "my test name::" +## ``` +## +## Selecting tests by pattern +## ========================== +## +## A single ``"*"`` can be used for globbing. +## +## Delimit the end of a suite name with `"::"`. +## +## Tests matching **any** of the arguments are executed. +## +## ```cmd +## nim c -r test fast_suite::mytest1 fast_suite::mytest2 +## nim c -r test "fast_suite::mytest*" +## nim c -r test "auth*::" "crypto::hashing*" +## # Run suites starting with 'bug #' and standalone tests starting with '#' +## nim c -r test 'bug #*::' '::#*' +## ``` +## +## Examples +## ======== +## +## ```nim ## suite "description for this stuff": +## echo "suite setup: run once before the tests" +## +## setup: +## echo "run before each test" +## +## teardown: +## echo "run after each test" +## ## test "essential truths": ## # give up and stop if this fails ## require(true) @@ -28,210 +91,699 @@ ## ## test "out of bounds error is thrown on bad access": ## let v = @[1, 2, 3] # you can do initialization here -## expect(IndexError): +## expect(IndexDefect): ## discard v[4] +## +## echo "suite teardown: run once after the tests" +## ``` +## +## Limitations/Bugs +## ================ +## Since `check` will rewrite some expressions for supporting checkpoints +## (namely assigns expressions to variables), some type conversions are not supported. +## For example `check 4.0 == 2 + 2` won't work. But `doAssert 4.0 == 2 + 2` works. +## Make sure both sides of the operator (such as `==`, `>=` and so on) have the same type. +## + +import std/private/since +import std/exitprocs -import - macros +when defined(nimPreviewSlimSystem): + import std/assertions + +import std/[macros, strutils, streams, times, sets, sequtils] when declared(stdout): - import os + import std/os -when not defined(ECMAScript): - import terminal - system.addQuitProc(resetAttributes) +const useTerminal = not defined(js) -type - TestStatus* = enum OK, FAILED - OutputLevel* = enum PRINT_ALL, PRINT_FAILURES, PRINT_NONE +when useTerminal: + import std/terminal -{.deprecated: [TTestStatus: TestStatus, TOutputLevel: OutputLevel]} +type + TestStatus* = enum ## The status of a test when it is done. + OK, + FAILED, + SKIPPED + + OutputLevel* = enum ## The output verbosity of the tests. + PRINT_ALL, ## Print as much as possible. + PRINT_FAILURES, ## Print only the failed tests. + PRINT_NONE ## Print nothing. + + TestResult* = object + suiteName*: string + ## Name of the test suite that contains this test case. + ## Can be ``nil`` if the test case is not in a suite. + testName*: string + ## Name of the test case + status*: TestStatus + + OutputFormatter* = ref object of RootObj + + ConsoleOutputFormatter* = ref object of OutputFormatter + colorOutput: bool + ## Have test results printed in color. + ## Default is `auto` depending on `isatty(stdout)`, or override it with + ## `-d:nimUnittestColor:auto|on|off`. + ## + ## Deprecated: Setting the environment variable `NIMTEST_COLOR` to `always` + ## or `never` changes the default for the non-js target to true or false respectively. + ## Deprecated: the environment variable `NIMTEST_NO_COLOR`, when set, changes the + ## default to true, if `NIMTEST_COLOR` is undefined. + outputLevel: OutputLevel + ## Set the verbosity of test results. + ## Default is `PRINT_ALL`, or override with: + ## `-d:nimUnittestOutputLevel:PRINT_ALL|PRINT_FAILURES|PRINT_NONE`. + ## + ## Deprecated: the `NIMTEST_OUTPUT_LVL` environment variable is set for the non-js target. + isInSuite: bool + isInTest: bool + + JUnitOutputFormatter* = ref object of OutputFormatter + stream: Stream + testErrors: seq[string] + testStartTime: float + testStackTrace: string var - abortOnError* {.threadvar.}: bool - outputLevel* {.threadvar.}: OutputLevel - colorOutput* {.threadvar.}: bool + abortOnError* {.threadvar.}: bool ## Set to true in order to quit + ## immediately on fail. Default is false, + ## or override with `-d:nimUnittestAbortOnError:on|off`. + ## + ## Deprecated: can also override depending on whether + ## `NIMTEST_ABORT_ON_ERROR` environment variable is set. checkpoints {.threadvar.}: seq[string] + formatters {.threadvar.}: seq[OutputFormatter] + testsFilters {.threadvar.}: HashSet[string] + disabledParamFiltering {.threadvar.}: bool -checkpoints = @[] - -template testSetupIMPL*: stmt {.immediate, dirty.} = discard -template testTeardownIMPL*: stmt {.immediate, dirty.} = discard - -proc shouldRun(testName: string): bool = - result = true - -template suite*(name: expr, body: stmt): stmt {.immediate, dirty.} = - block: - template setup*(setupBody: stmt): stmt {.immediate, dirty.} = - template testSetupIMPL: stmt {.immediate, dirty.} = setupBody - - template teardown*(teardownBody: stmt): stmt {.immediate, dirty.} = - template testTeardownIMPL: stmt {.immediate, dirty.} = teardownBody +const + outputLevelDefault = PRINT_ALL + nimUnittestOutputLevel {.strdefine.} = $outputLevelDefault + nimUnittestColor {.strdefine.} = "auto" ## auto|on|off + nimUnittestAbortOnError {.booldefine.} = false - body +template deprecateEnvVarHere() = + # xxx issue a runtime warning to deprecate this envvar. + discard -proc testDone(name: string, s: TestStatus) = - if s == FAILED: - programResult += 1 - - if outputLevel != PRINT_NONE and (outputLevel == PRINT_ALL or s == FAILED): - template rawPrint() = echo("[", $s, "] ", name) - when not defined(ECMAScript): - if colorOutput and not defined(ECMAScript): - var color = (if s == OK: fgGreen else: fgRed) - styledEcho styleBright, color, "[", $s, "] ", fgWhite, name +abortOnError = nimUnittestAbortOnError +when declared(stdout): + if existsEnv("NIMTEST_ABORT_ON_ERROR"): + deprecateEnvVarHere() + abortOnError = true + +method suiteStarted*(formatter: OutputFormatter, suiteName: string) {.base, gcsafe.} = + discard +method testStarted*(formatter: OutputFormatter, testName: string) {.base, gcsafe.} = + discard +method failureOccurred*(formatter: OutputFormatter, checkpoints: seq[string], + stackTrace: string) {.base, gcsafe.} = + ## ``stackTrace`` is provided only if the failure occurred due to an exception. + ## ``checkpoints`` is never ``nil``. + discard +method testEnded*(formatter: OutputFormatter, testResult: TestResult) {.base, gcsafe.} = + discard +method suiteEnded*(formatter: OutputFormatter) {.base, gcsafe.} = + discard + +proc addOutputFormatter*(formatter: OutputFormatter) = + formatters.add(formatter) + +proc delOutputFormatter*(formatter: OutputFormatter) = + keepIf(formatters, proc (x: OutputFormatter): bool = + x != formatter) + +proc resetOutputFormatters* {.since: (1, 1).} = + formatters = @[] + +proc newConsoleOutputFormatter*(outputLevel: OutputLevel = outputLevelDefault, + colorOutput = true): ConsoleOutputFormatter = + ConsoleOutputFormatter( + outputLevel: outputLevel, + colorOutput: colorOutput + ) + +proc colorOutput(): bool = + let color = nimUnittestColor + case color + of "auto": + when declared(stdout): result = isatty(stdout) + else: result = false + of "on": result = true + of "off": result = false + else: raiseAssert $color + + when declared(stdout): + if existsEnv("NIMTEST_COLOR"): + deprecateEnvVarHere() + let colorEnv = getEnv("NIMTEST_COLOR") + if colorEnv == "never": + result = false + elif colorEnv == "always": + result = true + elif existsEnv("NIMTEST_NO_COLOR"): + deprecateEnvVarHere() + result = false + +proc defaultConsoleFormatter*(): ConsoleOutputFormatter = + var colorOutput = colorOutput() + var outputLevel = nimUnittestOutputLevel.parseEnum[:OutputLevel] + when declared(stdout): + const a = "NIMTEST_OUTPUT_LVL" + if existsEnv(a): + # xxx issue a warning to deprecate this envvar. + outputLevel = getEnv(a).parseEnum[:OutputLevel] + result = newConsoleOutputFormatter(outputLevel, colorOutput) + +method suiteStarted*(formatter: ConsoleOutputFormatter, suiteName: string) = + template rawPrint() = echo("\n[Suite] ", suiteName) + when useTerminal: + if formatter.colorOutput: + styledEcho styleBright, fgBlue, "\n[Suite] ", resetStyle, suiteName + else: rawPrint() + else: rawPrint() + formatter.isInSuite = true + +method testStarted*(formatter: ConsoleOutputFormatter, testName: string) = + formatter.isInTest = true + +method failureOccurred*(formatter: ConsoleOutputFormatter, + checkpoints: seq[string], stackTrace: string) = + if stackTrace.len > 0: + echo stackTrace + let prefix = if formatter.isInSuite: " " else: "" + for msg in items(checkpoints): + echo prefix, msg + +method testEnded*(formatter: ConsoleOutputFormatter, testResult: TestResult) = + formatter.isInTest = false + + if formatter.outputLevel != OutputLevel.PRINT_NONE and + (formatter.outputLevel == OutputLevel.PRINT_ALL or testResult.status == TestStatus.FAILED): + let prefix = if testResult.suiteName.len > 0: " " else: "" + template rawPrint() = echo(prefix, "[", $testResult.status, "] ", + testResult.testName) + when useTerminal: + if formatter.colorOutput: + var color = case testResult.status + of TestStatus.OK: fgGreen + of TestStatus.FAILED: fgRed + of TestStatus.SKIPPED: fgYellow + styledEcho styleBright, color, prefix, "[", $testResult.status, "] ", + resetStyle, testResult.testName else: rawPrint() else: rawPrint() -template test*(name: expr, body: stmt): stmt {.immediate, dirty.} = - bind shouldRun, checkpoints, testDone +method suiteEnded*(formatter: ConsoleOutputFormatter) = + formatter.isInSuite = false + +proc xmlEscape(s: string): string = + result = newStringOfCap(s.len) + for c in items(s): + case c: + of '<': result.add("<") + of '>': result.add(">") + of '&': result.add("&") + of '"': result.add(""") + of '\'': result.add("'") + else: + if ord(c) < 32: + result.add("&#" & $ord(c) & ';') + else: + result.add(c) + +proc newJUnitOutputFormatter*(stream: Stream): JUnitOutputFormatter = + ## Creates a formatter that writes report to the specified stream in + ## JUnit format. + ## The ``stream`` is NOT closed automatically when the test are finished, + ## because the formatter has no way to know when all tests are finished. + ## You should invoke formatter.close() to finalize the report. + result = JUnitOutputFormatter( + stream: stream, + testErrors: @[], + testStackTrace: "", + testStartTime: 0.0 + ) + stream.writeLine("<?xml version=\"1.0\" encoding=\"UTF-8\"?>") + stream.writeLine("<testsuites>") + +proc close*(formatter: JUnitOutputFormatter) = + ## Completes the report and closes the underlying stream. + formatter.stream.writeLine("</testsuites>") + formatter.stream.close() + +method suiteStarted*(formatter: JUnitOutputFormatter, suiteName: string) = + formatter.stream.writeLine("\t<testsuite name=\"$1\">" % xmlEscape(suiteName)) + +method testStarted*(formatter: JUnitOutputFormatter, testName: string) = + formatter.testErrors.setLen(0) + formatter.testStackTrace.setLen(0) + formatter.testStartTime = epochTime() + +method failureOccurred*(formatter: JUnitOutputFormatter, + checkpoints: seq[string], stackTrace: string) = + ## ``stackTrace`` is provided only if the failure occurred due to an exception. + ## ``checkpoints`` is never ``nil``. + formatter.testErrors.add(checkpoints) + if stackTrace.len > 0: + formatter.testStackTrace = stackTrace + +method testEnded*(formatter: JUnitOutputFormatter, testResult: TestResult) = + let time = epochTime() - formatter.testStartTime + let timeStr = time.formatFloat(ffDecimal, precision = 8) + formatter.stream.writeLine("\t\t<testcase name=\"$#\" time=\"$#\">" % [ + xmlEscape(testResult.testName), timeStr]) + case testResult.status + of TestStatus.OK: + discard + of TestStatus.SKIPPED: + formatter.stream.writeLine("<skipped />") + of TestStatus.FAILED: + let failureMsg = if formatter.testStackTrace.len > 0 and + formatter.testErrors.len > 0: + xmlEscape(formatter.testErrors[^1]) + elif formatter.testErrors.len > 0: + xmlEscape(formatter.testErrors[0]) + else: "The test failed without outputting an error" + + var errs = "" + if formatter.testErrors.len > 1: + var startIdx = if formatter.testStackTrace.len > 0: 0 else: 1 + var endIdx = if formatter.testStackTrace.len > 0: + formatter.testErrors.len - 2 + else: formatter.testErrors.len - 1 + + for errIdx in startIdx..endIdx: + if errs.len > 0: + errs.add("\n") + errs.add(xmlEscape(formatter.testErrors[errIdx])) + + if formatter.testStackTrace.len > 0: + formatter.stream.writeLine("\t\t\t<error message=\"$#\">$#</error>" % [ + failureMsg, xmlEscape(formatter.testStackTrace)]) + if errs.len > 0: + formatter.stream.writeLine("\t\t\t<system-err>$#</system-err>" % errs) + else: + formatter.stream.writeLine("\t\t\t<failure message=\"$#\">$#</failure>" % + [failureMsg, errs]) + + formatter.stream.writeLine("\t\t</testcase>") + +method suiteEnded*(formatter: JUnitOutputFormatter) = + formatter.stream.writeLine("\t</testsuite>") + +proc glob(matcher, filter: string): bool = + ## Globbing using a single `*`. Empty `filter` matches everything. + if filter.len == 0: + return true + + if not filter.contains('*'): + return matcher == filter + + let beforeAndAfter = filter.split('*', maxsplit = 1) + if beforeAndAfter.len == 1: + # "foo*" + return matcher.startsWith(beforeAndAfter[0]) + + if matcher.len < filter.len - 1: + return false # "12345" should not match "123*345" + + return matcher.startsWith(beforeAndAfter[0]) and matcher.endsWith( + beforeAndAfter[1]) + +proc matchFilter(suiteName, testName, filter: string): bool = + if filter == "": + return true + if testName == filter: + # corner case for tests containing "::" in their name + return true + let suiteAndTestFilters = filter.split("::", maxsplit = 1) + + if suiteAndTestFilters.len == 1: + # no suite specified + let testFilter = suiteAndTestFilters[0] + return glob(testName, testFilter) + + return glob(suiteName, suiteAndTestFilters[0]) and + glob(testName, suiteAndTestFilters[1]) + +proc shouldRun(currentSuiteName, testName: string): bool = + ## Check if a test should be run by matching suiteName and testName against + ## test filters. + if testsFilters.len == 0: + return true + + for f in testsFilters: + if matchFilter(currentSuiteName, testName, f): + return true + + return false + +proc ensureInitialized() = + if formatters.len == 0: + formatters = @[OutputFormatter(defaultConsoleFormatter())] + + if not disabledParamFiltering: + when declared(paramCount): + # Read tests to run from the command line. + for i in 1 .. paramCount(): + testsFilters.incl(paramStr(i)) + +# These two procs are added as workarounds for +# https://github.com/nim-lang/Nim/issues/5549 +proc suiteEnded() = + for formatter in formatters: + formatter.suiteEnded() + +proc testEnded(testResult: TestResult) = + for formatter in formatters: + formatter.testEnded(testResult) + +template suite*(name, body) {.dirty.} = + ## Declare a test suite identified by `name` with optional ``setup`` + ## and/or ``teardown`` section. + ## + ## A test suite is a series of one or more related tests sharing a + ## common fixture (``setup``, ``teardown``). The fixture is executed + ## for EACH test. + ## + ## ```nim + ## suite "test suite for addition": + ## setup: + ## let result = 4 + ## + ## test "2 + 2 = 4": + ## check(2+2 == result) + ## + ## test "(2 + -2) != 4": + ## check(2 + -2 != result) + ## + ## # No teardown needed + ## ``` + ## + ## The suite will run the individual test cases in the order in which + ## they were listed. With default global settings the above code prints: + ## + ## [Suite] test suite for addition + ## [OK] 2 + 2 = 4 + ## [OK] (2 + -2) != 4 + bind formatters, ensureInitialized, suiteEnded + + block: + template setup(setupBody: untyped) {.dirty, used.} = + var testSetupIMPLFlag {.used.} = true + template testSetupIMPL: untyped {.dirty.} = setupBody + + template teardown(teardownBody: untyped) {.dirty, used.} = + var testTeardownIMPLFlag {.used.} = true + template testTeardownIMPL: untyped {.dirty.} = teardownBody + + let testSuiteName {.used.} = name - if shouldRun(name): + ensureInitialized() + try: + for formatter in formatters: + formatter.suiteStarted(name) + body + finally: + suiteEnded() + +proc exceptionTypeName(e: ref Exception): string {.inline.} = + if e == nil: "<foreign exception>" + else: $e.name + +when not declared(setProgramResult): + {.warning: "setProgramResult not available on platform, unittest will not" & + " give failing exit code on test failure".} + template setProgramResult(a: int) = + discard + +template test*(name, body) {.dirty.} = + ## Define a single test case identified by `name`. + ## + ## ```nim + ## test "roses are red": + ## let roses = "red" + ## check(roses == "red") + ## ``` + ## + ## The above code outputs: + ## + ## [OK] roses are red + bind shouldRun, checkpoints, formatters, ensureInitialized, testEnded, exceptionTypeName, setProgramResult + + ensureInitialized() + + if shouldRun(when declared(testSuiteName): testSuiteName else: "", name): checkpoints = @[] - var testStatusIMPL {.inject.} = OK + var testStatusIMPL {.inject.} = TestStatus.OK + for formatter in formatters: + formatter.testStarted(name) + + {.push warning[BareExcept]:off.} try: - testSetupIMPL() + when declared(testSetupIMPLFlag): testSetupIMPL() + when declared(testTeardownIMPLFlag): + defer: testTeardownIMPL() + {.push warning[BareExcept]:on.} body + {.pop.} except: - checkpoint("Unhandled exception: " & getCurrentExceptionMsg()) - echo getCurrentException().getStackTrace() - fail() + let e = getCurrentException() + let eTypeDesc = "[" & exceptionTypeName(e) & "]" + checkpoint("Unhandled exception: " & getCurrentExceptionMsg() & " " & eTypeDesc) + if e == nil: # foreign + fail() + else: + var stackTrace {.inject.} = e.getStackTrace() + fail() finally: - testTeardownIMPL() - testDone name, testStatusIMPL + if testStatusIMPL == TestStatus.FAILED: + setProgramResult 1 + let testResult = TestResult( + suiteName: when declared(testSuiteName): testSuiteName else: "", + testName: name, + status: testStatusIMPL + ) + testEnded(testResult) + checkpoints = @[] + {.pop.} proc checkpoint*(msg: string) = + ## Set a checkpoint identified by `msg`. Upon test failure all + ## checkpoints encountered so far are printed out. Example: + ## + ## ```nim + ## checkpoint("Checkpoint A") + ## check((42, "the Answer to life and everything") == (1, "a")) + ## checkpoint("Checkpoint B") + ## ``` + ## + ## outputs "Checkpoint A" once it fails. checkpoints.add(msg) # TODO: add support for something like SCOPED_TRACE from Google Test template fail* = - bind checkpoints - for msg in items(checkpoints): - # this used to be 'echo' which now breaks due to a bug. XXX will revisit - # this issue later. - stdout.writeln msg - - when not defined(ECMAScript): - if abortOnError: quit(1) - + ## Print out the checkpoints encountered so far and quit if ``abortOnError`` + ## is true. Otherwise, erase the checkpoints and indicate the test has + ## failed (change exit code and test status). This template is useful + ## for debugging, but is otherwise mostly used internally. Example: + ## + ## ```nim + ## checkpoint("Checkpoint A") + ## complicatedProcInThread() + ## fail() + ## ``` + ## + ## outputs "Checkpoint A" before quitting. + bind ensureInitialized, setProgramResult when declared(testStatusIMPL): - testStatusIMPL = FAILED + testStatusIMPL = TestStatus.FAILED else: - programResult += 1 + setProgramResult 1 + + ensureInitialized() + + # var stackTrace: string = nil + for formatter in formatters: + when declared(stackTrace): + formatter.failureOccurred(checkpoints, stackTrace) + else: + formatter.failureOccurred(checkpoints, "") + + if abortOnError: quit(1) checkpoints = @[] -macro check*(conditions: stmt): stmt {.immediate.} = - let checked = callsite()[1] +template skip* = + ## Mark the test as skipped. Should be used directly + ## in case when it is not possible to perform test + ## for reasons depending on outer environment, + ## or certain application logic conditions or configurations. + ## The test code is still executed. + ## ```nim + ## if not isGLContextCreated(): + ## skip() + ## ``` + bind checkpoints + + testStatusIMPL = TestStatus.SKIPPED + checkpoints = @[] + +macro check*(conditions: untyped): untyped = + ## Verify if a statement or a list of statements is true. + ## A helpful error message and set checkpoints are printed out on + ## failure (if ``outputLevel`` is not ``PRINT_NONE``). + runnableExamples: + import std/strutils - var - argsAsgns = newNimNode(nnkStmtList) - argsPrintOuts = newNimNode(nnkStmtList) - counter = 0 + check("AKB48".toLowerAscii() == "akb48") - template asgn(a, value: expr): stmt = + let teams = {'A', 'K', 'B', '4', '8'} + + check: + "AKB48".toLowerAscii() == "akb48" + 'C' notin teams + + let checked = callsite()[1] + + template asgn(a: untyped, value: typed) = var a = value # XXX: we need "var: var" here in order to # preserve the semantics of var params - template print(name, value: expr): stmt = + template print(name: untyped, value: typed) = when compiles(string($value)): checkpoint(name & " was " & $value) - proc inspectArgs(exp: NimNode) = - for i in 1 .. <exp.len: - if exp[i].kind notin nnkLiterals: - inc counter - var arg = newIdentNode(":p" & $counter) - var argStr = exp[i].toStrLit - var paramAst = exp[i] - if exp[i].kind in nnkCallKinds: inspectArgs(exp[i]) - if exp[i].kind == nnkExprEqExpr: - # ExprEqExpr - # Ident !"v" - # IntLit 2 - paramAst = exp[i][1] - argsAsgns.add getAst(asgn(arg, paramAst)) - argsPrintOuts.add getAst(print(argStr, arg)) - if exp[i].kind != nnkExprEqExpr: - exp[i] = arg - else: - exp[i][1] = arg + proc inspectArgs(exp: NimNode): tuple[assigns, check, printOuts: NimNode] = + result.check = copyNimTree(exp) + result.assigns = newNimNode(nnkStmtList) + result.printOuts = newNimNode(nnkStmtList) + + var counter = 0 + + if exp[0].kind in {nnkIdent, nnkOpenSymChoice, nnkClosedSymChoice, nnkSym} and + $exp[0] in ["not", "in", "notin", "==", "<=", + ">=", "<", ">", "!=", "is", "isnot"]: + + for i in 1 ..< exp.len: + if exp[i].kind notin nnkLiterals: + inc counter + let argStr = exp[i].toStrLit + let paramAst = exp[i] + if exp[i].kind == nnkIdent: + result.printOuts.add getAst(print(argStr, paramAst)) + if exp[i].kind in nnkCallKinds + {nnkDotExpr, nnkBracketExpr, nnkPar} and + (exp[i].typeKind notin {ntyTypeDesc} or $exp[0] notin ["is", "isnot"]): + let callVar = newIdentNode(":c" & $counter) + result.assigns.add getAst(asgn(callVar, paramAst)) + result.check[i] = callVar + result.printOuts.add getAst(print(argStr, callVar)) + if exp[i].kind == nnkExprEqExpr: + # ExprEqExpr + # Ident "v" + # IntLit 2 + result.check[i] = exp[i][1] + if exp[i].typeKind notin {ntyTypeDesc}: + let arg = newIdentNode(":p" & $counter) + result.assigns.add getAst(asgn(arg, paramAst)) + result.printOuts.add getAst(print(argStr, arg)) + if exp[i].kind != nnkExprEqExpr: + result.check[i] = arg + else: + result.check[i][1] = arg case checked.kind of nnkCallKinds: - template rewrite(call, lineInfoLit: expr, callLit: string, - argAssgs, argPrintOuts: stmt): stmt = + + let (assigns, check, printOuts) = inspectArgs(checked) + let lineinfo = newStrLitNode(checked.lineInfo) + let callLit = checked.toStrLit + result = quote do: block: - argAssgs - if not call: - checkpoint(lineInfoLit & ": Check failed: " & callLit) - argPrintOuts + `assigns` + if `check`: + discard + else: + checkpoint(`lineinfo` & ": Check failed: " & `callLit`) + `printOuts` fail() - var checkedStr = checked.toStrLit - inspectArgs(checked) - result = getAst(rewrite(checked, checked.lineinfo, checkedStr, - argsAsgns, argsPrintOuts)) - of nnkStmtList: result = newNimNode(nnkStmtList) - for i in countup(0, checked.len - 1): - if checked[i].kind != nnkCommentStmt: - result.add(newCall(!"check", checked[i])) + for node in checked: + if node.kind != nnkCommentStmt: + result.add(newCall(newIdentNode("check"), node)) else: - template rewrite(Exp, lineInfoLit: expr, expLit: string): stmt = - if not Exp: - checkpoint(lineInfoLit & ": Check failed: " & expLit) - fail() + let lineinfo = newStrLitNode(checked.lineInfo) + let callLit = checked.toStrLit - result = getAst(rewrite(checked, checked.lineinfo, checked.toStrLit)) + result = quote do: + if `checked`: + discard + else: + checkpoint(`lineinfo` & ": Check failed: " & `callLit`) + fail() -template require*(conditions: stmt): stmt {.immediate, dirty.} = +template require*(conditions: untyped) = + ## Same as `check` except any failed test causes the program to quit + ## immediately. Any teardown statements are not executed and the failed + ## test output is not generated. + let savedAbortOnError = abortOnError block: - const AbortOnError {.inject.} = true + abortOnError = true check conditions - -macro expect*(exceptions: varargs[expr], body: stmt): stmt {.immediate.} = - let exp = callsite() - template expectBody(errorTypes, lineInfoLit: expr, - body: stmt): NimNode {.dirty.} = + abortOnError = savedAbortOnError + +macro expect*(exceptions: varargs[typed], body: untyped): untyped = + ## Test if `body` raises an exception found in the passed `exceptions`. + ## The test passes if the raised exception is part of the acceptable + ## exceptions. Otherwise, it fails. + runnableExamples: + import std/[math, random, strutils] + proc defectiveRobot() = + randomize() + case rand(1..4) + of 1: raise newException(OSError, "CANNOT COMPUTE!") + of 2: discard parseInt("Hello World!") + of 3: raise newException(IOError, "I can't do that Dave.") + else: assert 2 + 2 == 5 + + expect IOError, OSError, ValueError, AssertionDefect: + defectiveRobot() + + template expectBody(errorTypes, lineInfoLit, body): NimNode {.dirty.} = + {.push warning[BareExcept]:off.} try: + {.push warning[BareExcept]:on.} body + {.pop.} checkpoint(lineInfoLit & ": Expect Failed, no exception was thrown.") fail() except errorTypes: discard - - var body = exp[exp.len - 1] + except: + let err = getCurrentException() + checkpoint(lineInfoLit & ": Expect Failed, " & $err.name & " was thrown.") + fail() + {.pop.} var errorTypes = newNimNode(nnkBracket) - for i in countup(1, exp.len - 2): - errorTypes.add(exp[i]) - - result = getAst(expectBody(errorTypes, exp.lineinfo, body)) - - -when declared(stdout): - ## Reading settings - var envOutLvl = os.getEnv("NIMTEST_OUTPUT_LVL").string - - abortOnError = existsEnv("NIMTEST_ABORT_ON_ERROR") - colorOutput = not existsEnv("NIMTEST_NO_COLOR") + for exp in exceptions: + errorTypes.add(exp) -else: - var envOutLvl = "" # TODO - colorOutput = false + result = getAst(expectBody(errorTypes, errorTypes.lineInfo, body)) -if envOutLvl.len > 0: - for opt in countup(low(OutputLevel), high(OutputLevel)): - if $opt == envOutLvl: - outputLevel = opt - break +proc disableParamFiltering* = + ## disables filtering tests with the command line params + disabledParamFiltering = true |