This commit is contained in:
Ștefan Talpalaru 2019-05-19 23:11:17 +02:00
commit ae4d471d38
No known key found for this signature in database
GPG Key ID: CBF7934204F1B6F9
11 changed files with 3063 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
nimcache/

22
LICENSE.txt Normal file
View File

@ -0,0 +1,22 @@
Copyright (c) 2011-2018 Zahary Karadjov. All rights reserved.
Copyright (c) 2018-2019 Ștefan Talpalaru <stefantalpalaru@yahoo.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
[ MIT license: http://www.opensource.org/licenses/mit-license.php ]

36
README.md Normal file
View File

@ -0,0 +1,36 @@
## description
**unittest2** is a fork of the [unittest](https://nim-lang.org/docs/unittest.html) module in the
[Nim](https://nim-lang.org/) standard library, with a focus on parallel test
execution.
This fork was originally a pull request: [https://github.com/nim-lang/Nim/pull/9724](https://github.com/nim-lang/Nim/pull/9724)
## testing
```text
nimble test
```
## installation
```text
nimble install
```
## usage
Replace `import unittest` with `import unittest2` and see the [unittest2.html]() documentation generated by `nim doc unittest2.nim`.
## license
MIT
## credits
- original author: Zahary Karadjov
- fork author: Ștefan Talpalaru <stefantalpalaru@yahoo.com>
- homepage: https://github.com/stefantalpalaru/nim-unittest2

8
nim.cfg Normal file
View File

@ -0,0 +1,8 @@
@if release:
nimcache = "nimcache/release/$projectName"
@else:
nimcache = "nimcache/debug/$projectName"
@end
--threads:on

2
tests/nim.cfg Normal file
View File

@ -0,0 +1,2 @@
--path:"$projectDir/.."

179
tests/tunittest.nim Normal file
View File

@ -0,0 +1,179 @@
discard """
output: '''[Suite] suite with only teardown
[Suite] suite with only setup
[Suite] suite with none
[Suite] suite with both
[Suite] bug #4494
[Suite] bug #5571
[Suite] bug #5784
[Suite] test suite
[Suite] test name filtering
'''
"""
import unittest2, sequtils
proc doThings(spuds: var int): int =
spuds = 24
return 99
test "#964":
var spuds = 0
check doThings(spuds) == 99
check spuds == 24
from strutils import toUpperAscii
test "#1384":
check(@["hello", "world"].map(toUpperAscii) == @["HELLO", "WORLD"])
import options
test "unittest typedescs":
check(none(int) == none(int))
check(none(int) != some(1))
test "unittest multiple requires":
require(true)
require(true)
import math, random
from strutils import parseInt
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
test "unittest expect":
expect IOError, OSError, ValueError, AssertionError:
defectiveRobot()
var
a = 1
b = -1
c = 1
#unittests are sequential right now
suite "suite with only teardown":
teardown:
b = 2
test "unittest with only teardown 1":
check a == c
test "unittest with only teardown 2":
check b > a
suite "suite with only setup":
setup:
var testVar {.used.} = "from setup"
test "unittest with only setup 1":
check testVar == "from setup"
check b > a
b = -1
test "unittest with only setup 2":
check b < a
suite "suite with none":
test "unittest with none":
check b < a
suite "suite with both":
setup:
a = -2
teardown:
c = 2
test "unittest with both 1":
check b > a
test "unittest with both 2":
check c == 2
suite "bug #4494":
test "Uniqueness check":
var tags = @[1, 2, 3, 4, 5]
check:
allIt(0..3, tags[it] != tags[it + 1])
suite "bug #5571":
test "can define gcsafe procs within tests":
proc doTest {.gcsafe.} =
let line = "a"
check: line == "a"
doTest()
suite "bug #5784":
test "`or` should short circuit":
type Obj = ref object
field: int
var obj: Obj
check obj.isNil or obj.field == 0
type
SomeType = object
value: int
children: seq[SomeType]
# bug #5252
proc `==`(a, b: SomeType): bool =
return a.value == b.value
suite "test suite":
test "test":
let a = SomeType(value: 10)
let b = SomeType(value: 10)
check(a == b)
when defined(testing):
suite "test name filtering":
test "test name":
check matchFilter("suite1", "foo", "")
check matchFilter("suite1", "foo", "foo")
check matchFilter("suite1", "foo", "::")
check matchFilter("suite1", "foo", "*")
check matchFilter("suite1", "foo", "::foo")
check matchFilter("suite1", "::foo", "::foo")
test "test name - glob":
check matchFilter("suite1", "foo", "f*")
check matchFilter("suite1", "foo", "*oo")
check matchFilter("suite1", "12345", "12*345")
check matchFilter("suite1", "q*wefoo", "q*wefoo")
check false == matchFilter("suite1", "foo", "::x")
check false == matchFilter("suite1", "foo", "::x*")
check false == matchFilter("suite1", "foo", "::*x")
# overlap
check false == matchFilter("suite1", "12345", "123*345")
check matchFilter("suite1", "ab*c::d*e::f", "ab*c::d*e::f")
test "suite name":
check matchFilter("suite1", "foo", "suite1::")
check false == matchFilter("suite1", "foo", "suite2::")
check matchFilter("suite1", "qwe::foo", "qwe::foo")
check matchFilter("suite1", "qwe::foo", "suite1::qwe::foo")
test "suite name - glob":
check matchFilter("suite1", "foo", "::*")
check matchFilter("suite1", "foo", "*::*")
check matchFilter("suite1", "foo", "*::foo")
check false == matchFilter("suite1", "foo", "*ite2::")
check matchFilter("suite1", "q**we::foo", "q**we::foo")
check matchFilter("suite1", "a::b*c::d*e", "a::b*c::d*e")

134
tests/tunittestparallel.nim Normal file
View File

@ -0,0 +1,134 @@
discard """
output: '''[Suite] suite #1
[Suite] suite #2
'''
"""
# Unfortunately, it's not possible to decouple the thread execution order from
# the number of available cores, due to how threadpool dynamically (and lazily)
# adjusts the number of worker threads, so we can't have a PRINT_ALL output in
# the verification section above.
import unittest2, os
test "independent test #1":
sleep(1000)
check 1 == 1
# check 1 == 2
# require 1 == 2
# var g {.global.}: seq[int]
# g.add(1)
test "independent test #2":
sleep(800)
check 1 == 1
test "independent test #3":
## nested tests
# we might as well keep this futile attempt at finding a problem with
# uninitialized `flowVars` in child threads
test "independent test #4":
test "independent test #5":
test "independent test #8":
test "independent test #9":
test "independent test #10":
test "independent test #11":
test "independent test #12":
test "independent test #13":
test "independent test #14":
test "independent test #15":
sleep(200)
check 1 == 1
test "independent test #16":
sleep(200)
check 1 == 1
test "independent test #17":
sleep(200)
check 1 == 1
test "independent test #18":
sleep(200)
check 1 == 1
test "independent test #19":
sleep(200)
check 1 == 1
test "independent test #20":
sleep(200)
check 1 == 1
test "independent test #21":
sleep(200)
check 1 == 1
test "independent test #22":
sleep(200)
check 1 == 1
test "independent test #23":
sleep(200)
check 1 == 1
test "independent test #24":
sleep(200)
check 1 == 1
test "independent test #25":
test "independent test #26":
sleep(200)
check 1 == 1
sleep(200)
check 1 == 1
sleep(200)
check 1 == 1
sleep(200)
check 1 == 1
sleep(200)
check 1 == 1
sleep(200)
check 1 == 1
sleep(200)
check 1 == 1
sleep(200)
check 1 == 1
sleep(200)
check 1 == 1
sleep(200)
check 1 == 1
sleep(400)
check 1 == 1
sleep(600)
check 1 == 1
suite "suite #1":
test "suite #1, test #1":
sleep(400)
check 1 == 1
test "suite #1, test #2":
sleep(300)
check 1 == 1
suite "suite #2":
setup:
# only here can we set formatters when running the tests in parallel
# (this setup will be executed for each test, so it may run multiple times
# in the same thread, hence the clearing of the threadvar before adding to it)
clearOutputFormatters()
addOutputFormatter(newConsoleOutputFormatter(PRINT_FAILURES, colorOutput=false))
teardown:
# we don't want the custom formatter to remain in worker threads after this
# suite is done
clearOutputFormatters()
test "suite #2, test #1":
sleep(200)
check 1 == 1
test "suite #2, test #2":
sleep(100)
check 1 == 1
test "independent test #6":
sleep(200)
check 1 == 1
test "independent test #7":
sleep(100)
check 1 == 1

View File

@ -0,0 +1,2 @@
-d:nimtestParallel

1836
unittest2.html Normal file

File diff suppressed because it is too large Load Diff

828
unittest2.nim Normal file
View File

@ -0,0 +1,828 @@
# See the file "LICENSE.txt", included in this
# distribution, for details about the copyright.
#
## :Authors: Zahary Karadjov, Ștefan Talpalaru
##
## 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 return the number of failed test as exit code, while
##
## .. code::
## nim c -r testfile.nim
##
## exits with 0 or 1.
##
## Running individual tests
## ========================
##
## Specify the test names as command line arguments.
##
## .. 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 suite 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 #*::' '::#*'
##
## Running tests in parallel
## =========================
##
## To enable the threadpool-based test parallelisation, "--threads:on" needs to
## be passed to the compiler, along with "-d:nimtestParallel" or the
## NIMTEST_PARALLEL environment variable:
##
## .. code::
##
## nim c -r --threads:on -d:nimtestParallel testfile.nim
## # or
## NIMTEST_PARALLEL=1 nim c -r --threads:on testfile.nim
##
## Since output formatters are kept in a threadvar, they need to be initialised
## for each thread in the thread pool. This means that customisation can only be
## done in a suite's "setup" section, which will run before each test in that
## suite - hence the need to clear existing formatters before adding new ones:
##
## .. code:: nim
##
## suite "custom formatter":
## setup:
## clearOutputFormatters()
## addOutputFormatter(newConsoleOutputFormatter(PRINT_FAILURES, colorOutput=false))
##
## # if you need to revert back to the default after the suite
## teardown:
## clearOutputFormatters()
##
## There are some implicit barriers where we wait for all the spawned jobs to
## complete: before and after each test suite and at the main thread's exit.
##
## The suite-related barriers are there to avoid mixing test output, but they
## also affect which groups of tests can be run in parallel, so keep them in
## mind when deciding how many tests to place in different suites (or between
## suites).
##
## Example
## -------
##
## .. 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(IndexError):
## discard v[4]
##
## echo "suite teardown: run once after the tests"
import
macros, strutils, streams, times, sets
when declared(stdout):
import os
when not defined(ECMAScript):
import terminal
when declared(stdout):
const paralleliseTests* = existsEnv("NIMTEST_PARALLEL") or defined(nimtestParallel)
## Whether parallel test running was enabled (set at compile time).
## This constant might be useful in custom output formatters.
else:
const paralleliseTests* = false
when paralleliseTests:
import threadpool, locks
# repeatedly calling sync() without waiting for results - on procs that don't
# return any - doesn't work properly (probably due to gSomeReady getting its
# counter increased back to the pre-call value) so we're stuck with these
# dummy flowvars
# (`flowVars` will be initialized in each child thread, when using nested tests, by the compiler)
var flowVars {.threadvar.}: seq[FlowVarBase]
proc repeatableSync() =
sync()
for flowVar in flowVars:
blockUntil(flowVar)
flowVars = @[]
# make sure all the spawned tests are done before exiting
# (this will be the last sync, so no need for repeatability)
let mainThreadID = getThreadId()
proc quitProc() {.noconv.} =
# "require" can exit from a worker thread and syncing in there would block
if getThreadId() == mainThreadID:
sync()
addQuitProc(quitProc)
var outputLock: Lock # used by testEnded() to avoid mixed test outputs
initLock(outputLock)
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 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.
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.
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,
## unless the ``NIMTEST_ABORT_ON_ERROR``
## environment variable is set for
## the non-js target.
checkpoints {.threadvar.}: seq[string]
formatters {.threadvar.}: seq[OutputFormatter]
testsFilters {.threadvar.}: HashSet[string]
disabledParamFiltering: bool
when declared(stdout):
abortOnError = existsEnv("NIMTEST_ABORT_ON_ERROR")
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 clearOutputFormatters*() =
formatters = @[]
proc addOutputFormatter*(formatter: OutputFormatter) =
formatters.add(formatter)
proc newConsoleOutputFormatter*(outputLevel: OutputLevel = PRINT_ALL,
colorOutput = true): ConsoleOutputFormatter =
ConsoleOutputFormatter(
outputLevel: outputLevel,
colorOutput: colorOutput
)
proc defaultConsoleFormatter*(): ConsoleOutputFormatter =
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")
if colorEnv == "never":
colorOutput = false
elif colorEnv == "always":
colorOutput = 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()
method suiteStarted*(formatter: ConsoleOutputFormatter, suiteName: string) =
template rawPrint() = echo("\n[Suite] ", suiteName)
when not defined(ECMAScript):
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 != PRINT_NONE and
(formatter.outputLevel == PRINT_ALL or testResult.status == FAILED):
let prefix = if testResult.suiteName.len > 0: " " else: ""
template rawPrint() = echo(prefix, "[", $testResult.status, "] ", testResult.testName)
when not defined(ECMAScript):
if formatter.colorOutput and not defined(ECMAScript):
var color = case testResult.status
of OK: fgGreen
of FAILED: fgRed
of 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("&lt;")
of '>': result.add("&gt;")
of '&': result.add("&amp;")
of '"': result.add("&quot;")
of '\'': result.add("&apos;")
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 OK:
discard
of SKIPPED:
formatter.stream.writeLine("<skipped />")
of 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 test_f = suiteAndTestFilters[0]
return glob(testName, test_f)
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.
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 and not testsFilters.isValid:
testsFilters.init()
when declared(paramCount):
# Read tests to run from the command line.
for i in 1 .. paramCount():
testsFilters.incl(paramStr(i))
proc suiteStarted(name: string) =
when paralleliseTests:
repeatableSync() # wait for any independent tests from the threadpool before starting the suite
for formatter in formatters:
formatter.suiteStarted(name)
proc suiteEnded() =
when paralleliseTests:
repeatableSync() # wait for a suite's tests from the threadpool before moving on to the next suite
for formatter in formatters:
formatter.suiteEnded()
proc testStarted(name: string) =
for formatter in formatters:
formatter.testStarted(name)
proc testEnded(testResult: TestResult) =
for formatter in formatters:
when paralleliseTests:
withLock outputLock:
formatter.testEnded(testResult)
else:
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, suiteStarted, 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:
suiteStarted(name)
body
finally:
suiteEnded()
template exceptionTypeName(e: typed): string = $e.name
template test*(name, body) =
## 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, testStarted, testEnded, exceptionTypeName
# `gensym` can't be in here because it's not a first-class pragma
when paralleliseTests:
{.pragma: testrunner, gcsafe.}
else:
{.pragma: testrunner.}
proc runTest(testSuiteName: string): int {.gensym, testrunner.} =
# when running tests in parallel, the only place we can use
# addOutputFormatter() is in a suite's setup(), so we need to run it before
# ensureInitialized()
when declared(testSetupIMPLFlag):
testSetupIMPL()
ensureInitialized()
if shouldRun(testSuiteName, name):
checkpoints = @[]
var testStatusIMPL {.inject.} = OK
testStarted(name)
try:
body
except:
when not defined(js):
let e = getCurrentException()
let eTypeDesc = "[" & exceptionTypeName(e) & "]"
checkpoint("Unhandled exception: " & getCurrentExceptionMsg() & " " & eTypeDesc)
var stackTrace {.inject.} = e.getStackTrace()
fail()
finally:
if testStatusIMPL == FAILED:
programResult += 1
let testResult = TestResult(
suiteName: testSuiteName,
testName: name,
status: testStatusIMPL
)
testEnded(testResult)
checkpoints = @[]
# when running tests in parallel, "formatters" manipulation may occur in
# teardown(), so it needs to be after testEnded()
when declared(testTeardownIMPLFlag):
testTeardownIMPL()
let optionalTestSuiteName = when declared(testSuiteName): testSuiteName else: ""
when paralleliseTests:
flowVars.add(spawn runTest(optionalTestSuiteName))
else:
discard runTest(optionalTestSuiteName)
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
when declared(testStatusIMPL):
testStatusIMPL = FAILED
else:
programResult += 1
ensureInitialized()
# var stackTrace: string = nil
for formatter in formatters:
when declared(stackTrace):
formatter.failureOccurred(checkpoints, stackTrace)
else:
formatter.failureOccurred(checkpoints, "")
when not defined(ECMAScript):
if abortOnError:
when declared(testStatusIMPL):
# this wasn't incremented yet, because it's normally incremented in the
# "test" template, but we're exiting earlier here
programResult += 1
quit(programResult)
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 isGLConextCreated():
## skip()
bind checkpoints
testStatusIMPL = 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
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 == nnkIdent 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 }:
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 not `check`:
checkpoint(`lineinfo` & ": Check failed: " & `callLit`)
`printOuts`
fail()
of nnkStmtList:
result = newNimNode(nnkStmtList)
for node in checked:
if node.kind != nnkCommentStmt:
result.add(newCall(!"check", node))
else:
let lineinfo = newStrLitNode(checked.lineinfo)
let callLit = checked.toStrLit
result = quote do:
if not `checked`:
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.
## 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()
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 body = exp[exp.len - 1]
var errorTypes = newNimNode(nnkBracket)
for i in countup(1, exp.len - 2):
errorTypes.add(exp[i])
result = getAst(expectBody(errorTypes, exp.lineinfo, body))
proc disableParamFiltering* =
## disables filtering tests with the command line params
disabledParamFiltering = true

14
unittest2.nimble Normal file
View File

@ -0,0 +1,14 @@
mode = ScriptMode.Verbose
version = "0.0.1"
author = "Ștefan Talpalaru"
description = "unittest fork with support for parallel test execution"
license = "MIT"
requires "nim >= 0.19.4"
task test, "Run tests":
for f in listFiles("tests"):
if f.len > 4 and f[^4..^1] == ".nim":
exec "nim c -r -f --hints:off --verbosity:0 " & f
rmFile(f[0..^5].toExe())