compile-time test support (#34)

This change brings 3 new items to `unittest2`:

* `-d:unittest2Static` compile-time flag that enables `test` to run both
at compile time and runtime
* `staticTest` that only run at compilet ime no matter the flag
* `runtimeTest` that only run at run time no matter the flag
This commit is contained in:
Jacek Sieka 2023-11-10 13:49:41 +01:00 committed by GitHub
parent 91973dfa38
commit 333e74fa2d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 161 additions and 134 deletions

View File

@ -156,16 +156,10 @@ jobs:
- name: Run tests
run: |
nim --version
env TEST_LANG="c" NIMFLAGS="${NIMFLAGS} --gc:refc" nim test
env TEST_LANG="cpp" NIMFLAGS="${NIMFLAGS} --gc:refc" nim test
if [[ "${{ matrix.branch }}" == "devel" ]]; then
echo -e "\nTesting with '--gc:orc':\n"
if env TEST_LANG="c" NIMFLAGS="${NIMFLAGS} --gc:orc" nim test; then
echo "Nim devel with --gc:orc works again! Please remove this check in ci.yml"
false
fi
if env TEST_LANG="cpp" NIMFLAGS="${NIMFLAGS} --gc:orc" nim test; then
echo "Nim devel with --gc:orc works again! Please remove this check in ci.yml"
false
fi
env TEST_LANG="c" NIMFLAGS="${NIMFLAGS}" nim test
env TEST_LANG="cpp" NIMFLAGS="${NIMFLAGS}" nim test
if [[ "${{ matrix.branch }}" != "version-1-6" ]]; then
echo -e "\nTesting with '--mm:refc':\n"
TEST_LANG="c" NIMFLAGS="${NIMFLAGS} --mm:refc" nim test
TEST_LANG="cpp" NIMFLAGS="${NIMFLAGS} --mm:refc" nim test
fi

View File

@ -41,12 +41,10 @@ test "unittest typedescs":
check(none(int) == none(int))
check(none(int) != some(1))
test "unittest multiple requires":
require(true)
require(true)
import random
proc defectiveRobot() =
randomize()
@ -55,7 +53,7 @@ proc defectiveRobot() =
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":
runtimeTest "unittest expect":
expect IOError, OSError, ValueError, AssertionDefect:
defectiveRobot()
expect CatchableError:
@ -77,26 +75,26 @@ suite "suite with only teardown":
teardown:
b = 2
test "unittest with only teardown 1":
runtimeTest "unittest with only teardown 1":
check a == c
test "unittest with only teardown 2":
runtimeTest "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":
runtimeTest "unittest with only setup 1":
check testVar == "from setup"
check b > a
b = -1
test "unittest with only setup 2":
runtimeTest "unittest with only setup 2":
check b < a
suite "suite with none":
test "unittest with none":
runtimeTest "unittest with none":
check b < a
suite "suite with both":
@ -106,10 +104,10 @@ suite "suite with both":
teardown:
c = 2
test "unittest with both 1":
runtimeTest "unittest with both 1":
check b > a
test "unittest with both 2":
runtimeTest "unittest with both 2":
check c == 2
suite "bug #4494":
@ -187,4 +185,3 @@ when defined(testing):
# Also supposed to work outside tests:
check 1 == 1

View File

@ -1,6 +1,5 @@
# unittest2
#
#
# Nim's Runtime Library
# (c) Copyright 2015 Nim Contributors
# (c) Copyright 2019-2021 Ștefan Talpalaru
# (c) Copyright 2021-Onwards Status Research and Development
@ -156,6 +155,11 @@ const
unittest2PreviewIsolate {.booldefine.} = false
## Preview isolation mode where each test is run in a separate process - may
## be removed in the future
unittest2Static* {.booldefine.} = false
## Run tests at compile time as well - only a subset of functionality is
## enabled at compile-time meaning that tests must be written
## conservatively. `suite` features (`setup` etc) in particular are not
## supported.
when useTerminal:
import std/terminal
@ -406,6 +410,23 @@ const
maxStatusLen = 7
maxDurationLen = 6
func formatStatus(status: string): string =
"[" & alignLeft(status, maxStatusLen) & "]"
func formatStatus(status: TestStatus): string =
formatStatus($status)
proc formatDuration(dur: Duration, aligned = true): string =
let
seconds = dur.inMilliseconds.float / 1000.0
precision = max(3 - ($seconds.int).len, 1)
str = formatFloat(seconds, ffDecimal, precision)
if aligned:
"(" & align(str, maxDurationLen) & "s)"
else:
"(" & str & "s)"
when collect:
proc formatFraction(cur, total: int): string =
let
@ -448,7 +469,7 @@ method suiteStarted*(formatter: ConsoleOutputFormatter, suiteName: string) =
counter =
when collect: formatFraction(formatter.curSuite, formatter.tests.len) & " "
else:
if formatter.outputLevel == VERBOSE: "[Suite ] " else: ""
if formatter.outputLevel == VERBOSE: formatStatus("Suite") & " " else: ""
maxNameLen = when collect: max(toSeq(formatter.tests.keys()).mapIt(it.len)) else: 0
eol = if formatter.outputLevel == VERBOSE: "\n" else: " "
formatter.write do:
@ -479,7 +500,7 @@ method testStarted*(formatter: ConsoleOutputFormatter, testName: string) =
try: formatFraction(formatter.curTest, formatter.tests[formatter.curSuiteName]) & " "
except CatchableError: ""
else:
"[Test ]"
formatStatus("Test")
formatter.write do:
stdout.styledWrite " ", fgBlue, alignLeft(counter, maxStatusLen + maxDurationLen + 7)
@ -510,20 +531,6 @@ proc marker(status: TestStatus): string =
of TestStatus.FAILED: "F"
of TestStatus.SKIPPED: "s"
proc formatDuration(dur: Duration, aligned = true): string =
let
seconds = dur.inMilliseconds.float / 1000.0
precision = max(3 - ($seconds.int).len, 1)
str = formatFloat(seconds, ffDecimal, precision)
if aligned:
"(" & align(str, maxDurationLen) & "s)"
else:
"(" & str & "s)"
proc formatStatus(status: TestStatus): string =
"[" & alignLeft($status, maxStatusLen) & "]"
proc getAppFilename2(): string =
# TODO https://github.com/nim-lang/Nim/pull/22544
try:
@ -850,14 +857,17 @@ 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):
when nimvm:
true
else:
if testsFilters.len == 0:
return true
return false
for f in testsFilters:
if matchFilter(currentSuiteName, testName, f):
return true
return false
proc parseParameters*(args: openArray[string]) =
var
@ -950,16 +960,18 @@ template suite*(nameParam: string, body: untyped) {.dirty.} =
var testSuiteTeardownIMPLFlag {.used.} = true
template testSuiteTeardownIMPL: untyped {.dirty.} = suiteTeardownBody
let suiteName {.inject.} = nameParam
when nimvm:
discard
else:
let suiteName {.inject.} = nameParam
when not collect:
# TODO deal with suite nesting
if currentSuite.len > 0:
suiteEnded()
currentSuite.reset()
currentSuite = suiteName
when not collect:
# TODO deal with suite nesting
if currentSuite.len > 0:
suiteEnded()
currentSuite.reset()
currentSuite = suiteName
suiteStarted(suiteName)
suiteStarted(suiteName)
# TODO what about exceptions in the suite itself?
body
@ -967,9 +979,12 @@ template suite*(nameParam: string, body: untyped) {.dirty.} =
when declared(testSuiteTeardownIMPLFlag):
testSuiteTeardownIMPL()
when not collect:
suiteEnded()
currentSuite.reset()
when nimvm:
discard
else:
when not collect:
suiteEnded()
currentSuite.reset()
template checkpoint*(msg: string) =
## Set a checkpoint identified by `msg`. Upon test failure all
@ -982,8 +997,16 @@ template checkpoint*(msg: string) =
## checkpoint("Checkpoint B")
##
## outputs "Checkpoint A" once it fails.
checkpoints.add(msg)
# TODO: add support for something like SCOPED_TRACE from Google Test
when nimvm:
when compiles(testName):
echo testName
echo msg
else:
bind checkpoints
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``
@ -998,24 +1021,28 @@ template fail* =
## fail()
##
## outputs "Checkpoint A" before quitting.
when declared(testStatusIMPL):
testStatusIMPL = TestStatus.FAILED
when nimvm:
echo "Tests failed"
quit 1
else:
when declared(testStatusIMPL):
testStatusIMPL = TestStatus.FAILED
programResult = 1
programResult = 1
for formatter in formatters:
let formatter = formatter # avoid lent iterator
when declared(stackTrace):
when stackTrace is string:
formatter.failureOccurred(checkpoints, stackTrace)
for formatter in formatters:
let formatter = formatter # avoid lent iterator
when declared(stackTrace):
when stackTrace is string:
formatter.failureOccurred(checkpoints, stackTrace)
else:
formatter.failureOccurred(checkpoints, "")
else:
formatter.failureOccurred(checkpoints, "")
else:
formatter.failureOccurred(checkpoints, "")
if abortOnError: quit(1)
if abortOnError: quit(1)
checkpoints.reset()
checkpoints.reset()
template skip* =
## Mark the test as skipped. Should be used directly
@ -1028,10 +1055,13 @@ template skip* =
##
## if not isGLContextCreated():
## skip()
bind checkpoints
when nimvm:
discard
else:
bind checkpoints
testStatusIMPL = TestStatus.SKIPPED
checkpoints = @[]
testStatusIMPL = TestStatus.SKIPPED
checkpoints = @[]
proc runDirect(test: Test) =
when not collect:
@ -1063,23 +1093,12 @@ proc runDirect(test: Test) =
duration: duration
))
template test*(nameParam: string, body: untyped) =
## 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
template runtimeTest*(nameParam: string, body: untyped) =
## Similar to `test` but runs only at run time, no matter the `unittest2Static`
## setting
bind collect, runDirect, shouldRun, checkpoints
proc runTest(suiteName, testName: string): TestStatus {.gensym.} =
proc runTest(suiteName, testName: string): TestStatus {.raises: [], gensym.} =
var testStatusIMPL {.inject.} = TestStatus.OK
let suiteName {.inject, used.} = suiteName
let testName {.inject, used.} = testName
@ -1127,6 +1146,37 @@ template test*(nameParam: string, body: untyped) =
else:
runDirect(instance)
template staticTest*(nameParam: string, body: untyped) =
## Similar to `test` but runs only at compiletime, no matter the
## `unittest2Static` setting
static:
block:
echo "[Test ] ", nameParam
body
echo "[", TestStatus.OK, " ] ", nameParam
template test*(nameParam: string, body: untyped) =
## 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
when nimvm:
when unittest2Static:
staticTest nameParam:
body
runtimeTest nameParam:
body
{.pop.} # raises: []
macro check*(conditions: untyped): untyped =
@ -1194,18 +1244,12 @@ macro check*(conditions: untyped): untyped =
else:
result.check[i][1] = arg
let
checkpointSym = bindSym("checkpoint")
checkSym = bindSym("check")
failSym = bindSym("fail")
proc buildCheck(lineinfo, callLit, assigns, check, printOuts: NimNode): NimNode =
let
checkpointSym = bindSym("checkpoint")
failSym = bindSym("fail")
case checked.kind
of nnkCallKinds:
let (assigns, check, printOuts) = inspectArgs(checked)
let lineinfo = newStrLitNode(checked.lineInfo)
let callLit = checked.toStrLit
let checkpointSym = bindSym("checkpoint")
result = nnkBlockStmt.newTree(
nnkBlockStmt.newTree(
newEmptyNode(),
nnkStmtList.newTree(
assigns,
@ -1233,6 +1277,17 @@ macro check*(conditions: untyped): untyped =
)
)
let
checkSym = bindSym("check")
case checked.kind
of nnkCallKinds:
let
(assigns, check, printOuts) = inspectArgs(checked)
lineinfo = newStrLitNode(checked.lineInfo)
callLit = checked.toStrLit
result = buildCheck(lineinfo, callLit, assigns, check, printOuts)
of nnkStmtList:
result = newNimNode(nnkStmtList)
for node in checked:
@ -1240,44 +1295,25 @@ macro check*(conditions: untyped): untyped =
result.add(newCall(checkSym, node))
else:
let lineinfo = newStrLitNode(checked.lineInfo)
let callLit = checked.toStrLit
let
lineinfo = newStrLitNode(checked.lineInfo)
callLit = checked.toStrLit
result = nnkBlockStmt.newTree(
newEmptyNode(),
nnkStmtList.newTree(
nnkIfStmt.newTree(
nnkElifBranch.newTree(
nnkCall.newTree(ident("not"), checked),
nnkStmtList.newTree(
nnkCall.newTree(
checkpointSym,
nnkInfix.newTree(
ident("&"),
nnkInfix.newTree(
ident("&"),
lineinfo,
newLit(": Check failed: ")
),
callLit
)
),
nnkCall.newTree(failSym)
)
)
)
)
)
result = buildCheck(
lineinfo, callLit, newEmptyNode(), checked, newEmptyNode())
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
when nimvm:
check conditions
abortOnError = savedAbortOnError
else:
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`.

View File

@ -1,6 +1,6 @@
mode = ScriptMode.Verbose
version = "0.1.0"
version = "0.2.0"
author = "Status Research & Development GmbH"
description = "unittest fork with support for parallel test execution"
license = "MIT"