diff options
Diffstat (limited to 'lib/pure/unittest.nim')
-rw-r--r-- | lib/pure/unittest.nim | 339 |
1 files changed, 192 insertions, 147 deletions
diff --git a/lib/pure/unittest.nim b/lib/pure/unittest.nim index bea7d9c44..cfb762258 100644 --- a/lib/pure/unittest.nim +++ b/lib/pure/unittest.nim @@ -9,11 +9,6 @@ ## :Author: Zahary Karadjov ## -## **Note**: Instead of ``unittest.nim``, please consider to use -## the ``testament`` tool which offers process isolation for your tests. -## Also ``when isMainModule: doAssert conditionHere`` is usually a -## much simpler solution for testing purposes. -## ## This module implements boilerplate to make unit testing easy. ## ## The test status and name is printed after any output or traceback. @@ -22,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 #*::' '::#*' +## ``` ## ## Examples ## ======== ## -## .. code:: nim -## +## ```nim ## suite "description for this stuff": ## echo "suite setup: run once before the tests" ## @@ -92,19 +95,31 @@ ## 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 + +when defined(nimPreviewSlimSystem): + import std/assertions -import - macros, strutils, streams, times, sets, sequtils +import std/[macros, strutils, streams, times, sets, sequtils] when declared(stdout): - import os + import std/os const useTerminal = not defined(js) when useTerminal: - import terminal + import std/terminal type TestStatus* = enum ## The status of a test when it is done. @@ -130,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 default 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 @@ -157,17 +170,31 @@ type 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 @@ -193,36 +220,44 @@ proc delOutputFormatter*(formatter: OutputFormatter) = proc resetOutputFormatters* {.since: (1, 1).} = formatters = @[] -proc newConsoleOutputFormatter*(outputLevel: OutputLevel = OutputLevel.PRINT_ALL, - colorOutput = true): <//>ConsoleOutputFormatter = +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"): + 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 = 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) @@ -283,7 +318,7 @@ proc xmlEscape(s: string): string = else: result.add(c) -proc newJUnitOutputFormatter*(stream: Stream): <//>JUnitOutputFormatter = +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, @@ -398,8 +433,6 @@ proc matchFilter(suiteName, testName, filter: string): bool = return glob(suiteName, suiteAndTestFilters[0]) and glob(testName, suiteAndTestFilters[1]) -when defined(testing): export matchFilter - proc shouldRun(currentSuiteName, testName: string): bool = ## Check if a test should be run by matching suiteName and testName against ## test filters. @@ -440,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: @@ -482,23 +514,29 @@ 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() @@ -509,22 +547,28 @@ template test*(name, body) {.dirty.} = 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: let e = getCurrentException() let eTypeDesc = "[" & exceptionTypeName(e) & "]" checkpoint("Unhandled exception: " & getCurrentExceptionMsg() & " " & eTypeDesc) - var stackTrace {.inject.} = e.getStackTrace() - fail() + if e == nil: # foreign + fail() + else: + var stackTrace {.inject.} = e.getStackTrace() + fail() finally: if testStatusIMPL == TestStatus.FAILED: - programResult = 1 + setProgramResult 1 let testResult = TestResult( suiteName: when declared(testSuiteName): testSuiteName else: "", testName: name, @@ -532,16 +576,17 @@ template test*(name, body) {.dirty.} = ) 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. checkpoints.add(msg) @@ -553,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 = TestStatus.FAILED else: - programResult = 1 + setProgramResult 1 ensureInitialized() @@ -576,8 +620,7 @@ template fail* = else: formatter.failureOccurred(checkpoints, "") - when declared(programResult): - if abortOnError: quit(programResult) + if abortOnError: quit(1) checkpoints = @[] @@ -587,11 +630,10 @@ 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 isGLContextCreated(): - ## skip() + ## ```nim + ## if not isGLContextCreated(): + ## skip() + ## ``` bind checkpoints testStatusIMPL = TestStatus.SKIPPED @@ -601,19 +643,17 @@ 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) = @@ -642,14 +682,15 @@ 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}: @@ -670,7 +711,9 @@ macro check*(conditions: untyped): untyped = result = quote do: block: `assigns` - if not `check`: + if `check`: + discard + else: checkpoint(`lineinfo` & ": Check failed: " & `callLit`) `printOuts` fail() @@ -679,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 callLit = checked.toStrLit result = quote do: - if not `checked`: + if `checked`: + discard + else: checkpoint(`lineinfo` & ": Check failed: " & `callLit`) fail() @@ -704,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 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() - 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 |