123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779 |
- #
- #
- # Nim's Runtime Library
- # (c) Copyright 2015 Nim Contributors
- #
- # See the file "copying.txt", included in this
- # distribution, for details about the copyright.
- #
- ## :Author: Zahary Karadjov
- ##
- ## This module implements boilerplate to make unit testing easy.
- ##
- ## The test status and name is printed after any output or traceback.
- ##
- ## 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.
- ##
- ## .. code::
- ##
- ## 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::
- ##
- ## 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.
- ##
- ## .. code::
- ##
- ## 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
- ##
- ## 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)
- ##
- ## test "slightly less obvious stuff":
- ## # print a nasty message and move on, skipping
- ## # the remainder of this block
- ## check(1 != 1)
- ## check("asd"[2] == 'd')
- ##
- ## test "out of bounds error is thrown on bad access":
- ## let v = @[1, 2, 3] # you can do initialization here
- ## 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
- when defined(nimPreviewSlimSystem):
- import std/assertions
- import macros, strutils, streams, times, sets, sequtils
- when declared(stdout):
- import os
- const useTerminal = not defined(js)
- when useTerminal:
- import terminal
- 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 ## 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
- 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):
- 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: doAssert false, $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()
- 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.
- ##
- ## .. code-block:: 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:
- ##
- ## .. code-block::
- ##
- ## [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
- 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
- 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")
- ##
- ## The above code outputs:
- ##
- ## .. code-block::
- ##
- ## [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.} = TestStatus.OK
- for formatter in formatters:
- formatter.testStarted(name)
- try:
- when declared(testSetupIMPLFlag): testSetupIMPL()
- when declared(testTeardownIMPLFlag):
- defer: testTeardownIMPL()
- body
- except:
- 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:
- if testStatusIMPL == TestStatus.FAILED:
- setProgramResult 1
- let testResult = TestResult(
- suiteName: when declared(testSuiteName): testSuiteName else: "",
- testName: name,
- status: testStatusIMPL
- )
- testEnded(testResult)
- checkpoints = @[]
- 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")
- ##
- ## outputs "Checkpoint A" once it fails.
- checkpoints.add(msg)
- # TODO: add support for something like SCOPED_TRACE from Google Test
- template fail* =
- ## 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:
- ##
- ## .. code-block:: nim
- ##
- ## checkpoint("Checkpoint A")
- ## complicatedProcInThread()
- ## fail()
- ##
- ## outputs "Checkpoint A" before quitting.
- bind ensureInitialized, setProgramResult
- when declared(testStatusIMPL):
- testStatusIMPL = TestStatus.FAILED
- else:
- 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 = @[]
- 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.
- ##
- ## .. code-block:: 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
- 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) =
- var a = value # XXX: we need "var: var" here in order to
- # preserve the semantics of var params
- template print(name: untyped, value: typed) =
- when compiles(string($value)):
- checkpoint(name & " was " & $value)
- 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:
- let (assigns, check, printOuts) = inspectArgs(checked)
- let lineinfo = newStrLitNode(checked.lineInfo)
- let callLit = checked.toStrLit
- result = quote do:
- block:
- `assigns`
- if `check`:
- discard
- else:
- checkpoint(`lineinfo` & ": Check failed: " & `callLit`)
- `printOuts`
- fail()
- of nnkStmtList:
- result = newNimNode(nnkStmtList)
- for node in checked:
- if node.kind != nnkCommentStmt:
- result.add(newCall(newIdentNode("check"), node))
- else:
- let lineinfo = newStrLitNode(checked.lineInfo)
- let callLit = checked.toStrLit
- result = quote do:
- if `checked`:
- discard
- else:
- checkpoint(`lineinfo` & ": Check failed: " & `callLit`)
- fail()
- 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:
- abortOnError = true
- check conditions
- 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.} =
- try:
- body
- checkpoint(lineInfoLit & ": Expect Failed, no exception was thrown.")
- fail()
- except errorTypes:
- discard
- except:
- checkpoint(lineInfoLit & ": Expect Failed, unexpected exception was thrown.")
- fail()
- var errorTypes = newNimNode(nnkBracket)
- for exp in exceptions:
- errorTypes.add(exp)
- result = getAst(expectBody(errorTypes, errorTypes.lineInfo, body))
- proc disableParamFiltering* =
- ## disables filtering tests with the command line params
- disabledParamFiltering = true
|