diff options
Diffstat (limited to 'lib/pure/unittest.nim')
-rw-r--r-- | lib/pure/unittest.nim | 476 |
1 files changed, 269 insertions, 207 deletions
diff --git a/lib/pure/unittest.nim b/lib/pure/unittest.nim index d804ba7c8..cfb762258 100644 --- a/lib/pure/unittest.nim +++ b/lib/pure/unittest.nim @@ -17,51 +17,59 @@ ## parent test as failed. Setup and teardown are inherited. Setup can be ## overridden locally. ## -## Compiled test files return the number of failed test as exit code, while -## ``nim c -r <testfile.nim>`` exits with 0 or 1 +## 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. ## -## .. code:: -## +## ```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 ``"::"``. -## -## .. code:: +## 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 ``"::"``. +## Delimit the end of a suite name with `"::"`. ## ## Tests matching **any** of the arguments are executed. ## -## .. code:: -## +## ```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 #*::' '::#*' +## ``` ## -## Example -## ------- -## -## .. code:: nim +## Examples +## ======== ## +## ```nim ## suite "description for this stuff": ## echo "suite setup: run once before the tests" ## @@ -83,19 +91,35 @@ ## ## 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, strutils, streams, times, sets +when defined(nimPreviewSlimSystem): + import std/assertions + +import std/[macros, strutils, streams, times, sets, sequtils] when declared(stdout): - import os + import std/os + +const useTerminal = not defined(js) -when not defined(ECMAScript): - import terminal +when useTerminal: + import std/terminal type TestStatus* = enum ## The status of a test when it is done. @@ -103,10 +127,10 @@ type 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. + 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 @@ -121,21 +145,19 @@ type ConsoleOutputFormatter* = ref object of OutputFormatter colorOutput: bool ## Have test results printed in color. - ## Default is true for the non-js target, - ## for which ``stdout`` is a tty. - ## Setting the environment variable - ## ``NIMTEST_COLOR`` to ``always`` or - ## ``never`` changes the default for the - ## non-js target to true or false respectively. - ## The deprecated environment variable - ## ``NIMTEST_NO_COLOR``, when set, - ## changes the defualt to true, if - ## ``NIMTEST_COLOR`` is undefined. + ## 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``, unless - ## the ``NIMTEST_OUTPUT_LVL`` environment - ## variable is set for the non-js target. + ## 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 @@ -145,28 +167,41 @@ type testStartTime: float testStackTrace: string -{.deprecated: [TTestStatus: TestStatus, TOutputLevel: OutputLevel]} - var abortOnError* {.threadvar.}: bool ## Set to true in order to quit ## immediately on fail. Default is false, - ## unless the ``NIMTEST_ABORT_ON_ERROR`` - ## environment variable is set for - ## the non-js target. + ## 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 +const + outputLevelDefault = PRINT_ALL + nimUnittestOutputLevel {.strdefine.} = $outputLevelDefault + nimUnittestColor {.strdefine.} = "auto" ## auto|on|off + nimUnittestAbortOnError {.booldefine.} = false + +template deprecateEnvVarHere() = + # xxx issue a runtime warning to deprecate this envvar. + discard + +abortOnError = nimUnittestAbortOnError when declared(stdout): - abortOnError = existsEnv("NIMTEST_ABORT_ON_ERROR") + 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.} = +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 @@ -176,45 +211,57 @@ method suiteEnded*(formatter: OutputFormatter) {.base, gcsafe.} = discard proc addOutputFormatter*(formatter: OutputFormatter) = - if formatters == nil: - formatters = @[formatter] - else: - formatters.add(formatter) + formatters.add(formatter) + +proc delOutputFormatter*(formatter: OutputFormatter) = + keepIf(formatters, proc (x: OutputFormatter): bool = + x != formatter) -proc newConsoleOutputFormatter*(outputLevel: OutputLevel = PRINT_ALL, +proc resetOutputFormatters* {.since: (1, 1).} = + formatters = @[] + +proc newConsoleOutputFormatter*(outputLevel: OutputLevel = outputLevelDefault, colorOutput = true): ConsoleOutputFormatter = ConsoleOutputFormatter( outputLevel: outputLevel, colorOutput: colorOutput ) -proc defaultConsoleFormatter*(): ConsoleOutputFormatter = +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): - # Reading settings - # On a terminal this branch is executed - var envOutLvl = os.getEnv("NIMTEST_OUTPUT_LVL").string - var colorOutput = isatty(stdout) if existsEnv("NIMTEST_COLOR"): - let colorEnv = getenv("NIMTEST_COLOR") + deprecateEnvVarHere() + let colorEnv = getEnv("NIMTEST_COLOR") if colorEnv == "never": - colorOutput = false + result = false elif colorEnv == "always": - colorOutput = true + result = true elif existsEnv("NIMTEST_NO_COLOR"): - colorOutput = false - var outputLevel = PRINT_ALL - if envOutLvl.len > 0: - for opt in countup(low(OutputLevel), high(OutputLevel)): - if $opt == envOutLvl: - outputLevel = opt - break - result = newConsoleOutputFormatter(outputLevel, colorOutput) - else: - result = newConsoleOutputFormatter() + 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 not defined(ECMAScript): + when useTerminal: if formatter.colorOutput: styledEcho styleBright, fgBlue, "\n[Suite] ", resetStyle, suiteName else: rawPrint() @@ -224,8 +271,9 @@ method suiteStarted*(formatter: ConsoleOutputFormatter, suiteName: string) = method testStarted*(formatter: ConsoleOutputFormatter, testName: string) = formatter.isInTest = true -method failureOccurred*(formatter: ConsoleOutputFormatter, checkpoints: seq[string], stackTrace: string) = - if stackTrace != nil: +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): @@ -234,18 +282,19 @@ method failureOccurred*(formatter: ConsoleOutputFormatter, checkpoints: seq[stri method testEnded*(formatter: ConsoleOutputFormatter, testResult: TestResult) = formatter.isInTest = false - if formatter.outputLevel != PRINT_NONE and - (formatter.outputLevel == PRINT_ALL or testResult.status == FAILED): - let prefix = if testResult.suiteName != nil: " " else: "" - template rawPrint() = echo(prefix, "[", $testResult.status, "] ", testResult.testName) - when not defined(ECMAScript): - if formatter.colorOutput and not defined(ECMAScript): + 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 OK: fgGreen - of FAILED: fgRed - of SKIPPED: fgYellow - else: fgWhite - styledEcho styleBright, color, prefix, "[", $testResult.status, "] ", resetStyle, testResult.testName + of TestStatus.OK: fgGreen + of TestStatus.FAILED: fgRed + of TestStatus.SKIPPED: fgYellow + styledEcho styleBright, color, prefix, "[", $testResult.status, "] ", + resetStyle, testResult.testName else: rawPrint() else: @@ -297,23 +346,25 @@ method testStarted*(formatter: JUnitOutputFormatter, testName: string) = formatter.testStackTrace.setLen(0) formatter.testStartTime = epochTime() -method failureOccurred*(formatter: JUnitOutputFormatter, checkpoints: seq[string], stackTrace: string) = +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 != nil: + 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 OK: + formatter.stream.writeLine("\t\t<testcase name=\"$#\" time=\"$#\">" % [ + xmlEscape(testResult.testName), timeStr]) + case testResult.status + of TestStatus.OK: discard - of SKIPPED: + of TestStatus.SKIPPED: formatter.stream.writeLine("<skipped />") - of FAILED: + of TestStatus.FAILED: let failureMsg = if formatter.testStackTrace.len > 0 and formatter.testErrors.len > 0: xmlEscape(formatter.testErrors[^1]) @@ -324,8 +375,9 @@ method testEnded*(formatter: JUnitOutputFormatter, testResult: TestResult) = 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 + 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: @@ -333,11 +385,13 @@ method testEnded*(formatter: JUnitOutputFormatter, testResult: TestResult) = errs.add(xmlEscape(formatter.testErrors[errIdx])) if formatter.testStackTrace.len > 0: - formatter.stream.writeLine("\t\t\t<error message=\"$#\">$#</error>" % [failureMsg, xmlEscape(formatter.testStackTrace)]) + 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\t<failure message=\"$#\">$#</failure>" % + [failureMsg, errs]) formatter.stream.writeLine("\t\t</testcase>") @@ -352,15 +406,16 @@ proc glob(matcher, filter: string): bool = if not filter.contains('*'): return matcher == filter - let beforeAndAfter = filter.split('*', maxsplit=1) + let beforeAndAfter = filter.split('*', maxsplit = 1) if beforeAndAfter.len == 1: # "foo*" - return matcher.startswith(beforeAndAfter[0]) + return matcher.startsWith(beforeAndAfter[0]) if matcher.len < filter.len - 1: - return false # "12345" should not match "123*345" + return false # "12345" should not match "123*345" - return matcher.startsWith(beforeAndAfter[0]) and matcher.endsWith(beforeAndAfter[1]) + return matcher.startsWith(beforeAndAfter[0]) and matcher.endsWith( + beforeAndAfter[1]) proc matchFilter(suiteName, testName, filter: string): bool = if filter == "": @@ -368,16 +423,15 @@ proc matchFilter(suiteName, testName, filter: string): bool = if testName == filter: # corner case for tests containing "::" in their name return true - let suiteAndTestFilters = filter.split("::", maxsplit=1) + let suiteAndTestFilters = filter.split("::", maxsplit = 1) if suiteAndTestFilters.len == 1: # no suite specified - let test_f = suiteAndTestFilters[0] - return glob(testName, test_f) + let testFilter = suiteAndTestFilters[0] + return glob(testName, testFilter) - return glob(suiteName, suiteAndTestFilters[0]) and glob(testName, suiteAndTestFilters[1]) - -when defined(testing): export matchFilter + 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 @@ -392,11 +446,10 @@ proc shouldRun(currentSuiteName, testName: string): bool = return false proc ensureInitialized() = - if formatters == nil: + if formatters.len == 0: formatters = @[OutputFormatter(defaultConsoleFormatter())] - if not disabledParamFiltering and not testsFilters.isValid: - testsFilters.init() + if not disabledParamFiltering: when declared(paramCount): # Read tests to run from the command line. for i in 1 .. paramCount(): @@ -420,27 +473,26 @@ template suite*(name, body) {.dirty.} = ## common fixture (``setup``, ``teardown``). The fixture is executed ## for EACH test. ## - ## .. code-block:: nim - ## suite "test suite for addition": - ## setup: - ## let result = 4 + ## ```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) ## - ## test "(2 + -2) != 4": - ## check(2 + -2 != result) + ## test "(2 + -2) != 4": + ## check(2 + -2 != result) ## - ## # No teardown needed + ## # 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: ## - ## .. code-block:: - ## - ## [Suite] test suite for addition - ## [OK] 2 + 2 = 4 - ## [OK] (2 + -2) != 4 + ## [Suite] test suite for addition + ## [OK] 2 + 2 = 4 + ## [OK] (2 + -2) != 4 bind formatters, ensureInitialized, suiteEnded block: @@ -462,71 +514,81 @@ template suite*(name, body) {.dirty.} = finally: suiteEnded() -template exceptionTypeName(e: typed): string = $e.name +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`. ## - ## .. code-block:: nim - ## - ## test "roses are red": - ## let roses = "red" - ## check(roses == "red") + ## ```nim + ## test "roses are red": + ## let roses = "red" + ## check(roses == "red") + ## ``` ## ## The above code outputs: ## - ## .. code-block:: - ## - ## [OK] roses are red - bind shouldRun, checkpoints, formatters, ensureInitialized, testEnded, exceptionTypeName + ## [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: when declared(testSetupIMPLFlag): testSetupIMPL() when declared(testTeardownIMPLFlag): defer: testTeardownIMPL() + {.push warning[BareExcept]:on.} body + {.pop.} except: - when not defined(js): - let e = getCurrentException() - let eTypeDesc = "[" & exceptionTypeName(e) & "]" - checkpoint("Unhandled exception: " & getCurrentExceptionMsg() & " " & eTypeDesc) + let e = getCurrentException() + let eTypeDesc = "[" & exceptionTypeName(e) & "]" + checkpoint("Unhandled exception: " & getCurrentExceptionMsg() & " " & eTypeDesc) + if e == nil: # foreign + fail() + else: var stackTrace {.inject.} = e.getStackTrace() - fail() + fail() finally: - if testStatusIMPL == FAILED: - programResult += 1 + if testStatusIMPL == TestStatus.FAILED: + setProgramResult 1 let testResult = TestResult( - suiteName: when declared(testSuiteName): testSuiteName else: nil, + 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: ## - ## .. code-block:: nim - ## - ## checkpoint("Checkpoint A") - ## check((42, "the Answer to life and everything") == (1, "a")) - ## checkpoint("Checkpoint B") + ## ```nim + ## checkpoint("Checkpoint A") + ## check((42, "the Answer to life and everything") == (1, "a")) + ## checkpoint("Checkpoint B") + ## ``` ## ## outputs "Checkpoint A" once it fails. - if checkpoints == nil: - checkpoints = @[] checkpoints.add(msg) # TODO: add support for something like SCOPED_TRACE from Google Test @@ -536,19 +598,18 @@ template fail* = ## failed (change exit code and test status). This template is useful ## for debugging, but is otherwise mostly used internally. Example: ## - ## .. code-block:: nim - ## - ## checkpoint("Checkpoint A") - ## complicatedProcInThread() - ## fail() + ## ```nim + ## checkpoint("Checkpoint A") + ## complicatedProcInThread() + ## fail() + ## ``` ## ## outputs "Checkpoint A" before quitting. - bind ensureInitialized - + bind ensureInitialized, setProgramResult when declared(testStatusIMPL): - testStatusIMPL = FAILED + testStatusIMPL = TestStatus.FAILED else: - programResult += 1 + setProgramResult 1 ensureInitialized() @@ -557,10 +618,9 @@ template fail* = when declared(stackTrace): formatter.failureOccurred(checkpoints, stackTrace) else: - formatter.failureOccurred(checkpoints, nil) + formatter.failureOccurred(checkpoints, "") - when not defined(ECMAScript): - if abortOnError: quit(programResult) + if abortOnError: quit(1) checkpoints = @[] @@ -570,33 +630,30 @@ template skip* = ## for reasons depending on outer environment, ## or certain application logic conditions or configurations. ## The test code is still executed. - ## - ## .. code-block:: nim - ## - ## if not isGLConextCreated(): - ## skip() + ## ```nim + ## if not isGLContextCreated(): + ## skip() + ## ``` bind checkpoints - testStatusIMPL = SKIPPED + 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``). - ## Example: - ## - ## .. code-block:: nim - ## - ## import strutils - ## - ## check("AKB48".toLowerAscii() == "akb48") - ## - ## let teams = {'A', 'K', 'B', '4', '8'} - ## - ## check: - ## "AKB48".toLowerAscii() == "akb48" - ## 'C' in teams + runnableExamples: + import std/strutils + + check("AKB48".toLowerAscii() == "akb48") + + let teams = {'A', 'K', 'B', '4', '8'} + + check: + "AKB48".toLowerAscii() == "akb48" + 'C' notin teams + let checked = callsite()[1] template asgn(a: untyped, value: typed) = @@ -614,7 +671,7 @@ macro check*(conditions: untyped): untyped = var counter = 0 - if exp[0].kind == nnkIdent and + if exp[0].kind in {nnkIdent, nnkOpenSymChoice, nnkClosedSymChoice, nnkSym} and $exp[0] in ["not", "in", "notin", "==", "<=", ">=", "<", ">", "!=", "is", "isnot"]: @@ -625,17 +682,18 @@ macro check*(conditions: untyped): untyped = let paramAst = exp[i] if exp[i].kind == nnkIdent: result.printOuts.add getAst(print(argStr, paramAst)) - if exp[i].kind in nnkCallKinds + { nnkDotExpr, nnkBracketExpr }: + 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" + # Ident "v" # IntLit 2 result.check[i] = exp[i][1] - if exp[i].typekind notin {ntyTypeDesc}: + 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)) @@ -648,12 +706,14 @@ macro check*(conditions: untyped): untyped = of nnkCallKinds: let (assigns, check, printOuts) = inspectArgs(checked) - let lineinfo = newStrLitNode(checked.lineinfo) + let lineinfo = newStrLitNode(checked.lineInfo) let callLit = checked.toStrLit result = quote do: block: `assigns` - if not `check`: + if `check`: + discard + else: checkpoint(`lineinfo` & ": Check failed: " & `callLit`) `printOuts` fail() @@ -662,14 +722,16 @@ macro check*(conditions: untyped): untyped = result = newNimNode(nnkStmtList) for node in checked: if node.kind != nnkCommentStmt: - result.add(newCall(!"check", node)) + result.add(newCall(newIdentNode("check"), node)) else: - let lineinfo = newStrLitNode(checked.lineinfo) + let lineinfo = newStrLitNode(checked.lineInfo) let callLit = checked.toStrLit result = quote do: - if not `checked`: + if `checked`: + discard + else: checkpoint(`lineinfo` & ": Check failed: " & `callLit`) fail() @@ -687,40 +749,40 @@ 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. - ## Example: - ## - ## .. code-block:: nim - ## - ## import math, random - ## proc defectiveRobot() = - ## randomize() - ## case random(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, AssertionError: - ## defectiveRobot() - let exp = callsite() + 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 except: - checkpoint(lineInfoLit & ": Expect Failed, unexpected exception was thrown.") + let err = getCurrentException() + checkpoint(lineInfoLit & ": Expect Failed, " & $err.name & " was thrown.") fail() - - var body = exp[exp.len - 1] + {.pop.} var errorTypes = newNimNode(nnkBracket) - for i in countup(1, exp.len - 2): - errorTypes.add(exp[i]) + for exp in exceptions: + errorTypes.add(exp) - result = getAst(expectBody(errorTypes, exp.lineinfo, body)) + result = getAst(expectBody(errorTypes, errorTypes.lineInfo, body)) proc disableParamFiltering* = ## disables filtering tests with the command line params |