Skip to content

Commit

Permalink
convert unittest.suite into a macro with structure
Browse files Browse the repository at this point in the history
 * remove usage of macros.callsite()
 * add workaround for bug nim-works#193
 * add structural checks for unittest.suite
  • Loading branch information
krux02 committed Jan 25, 2022
1 parent eaf1e8a commit 06a1ccf
Show file tree
Hide file tree
Showing 3 changed files with 120 additions and 66 deletions.
180 changes: 117 additions & 63 deletions lib/pure/unittest.nim
Original file line number Diff line number Diff line change
Expand Up @@ -460,11 +460,52 @@ proc suiteEnded() =
for formatter in formatters:
formatter.suiteEnded()

proc testStarted(testName: string) =
for formatter in formatters:
formatter.testStarted(testName)

proc testEnded(testResult: TestResult) =
for formatter in formatters:
formatter.testEnded(testResult)

template suite*(name, body) {.dirty.} =
proc exceptionTypeName(e: ref Exception): string {.inline.} =
if e == nil: "<foreign exception>"
else: $e.name

proc suiteStarted(name: string) =
ensureInitialized()
for formatter in formatters:
formatter.suiteStarted(name)

template testInternal(testSuiteName, testNameArg, testBody: untyped): untyped =
bind shouldRun, checkpoints, formatters, testEnded, exceptionTypeName, setProgramResult
if shouldRun(testSuiteName, testNameArg):
checkpoints = @[]
var testStatusIMPL {.inject.} = TestStatus.OK
testStarted(testNameArg)
try:
testBody
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: testSuiteName,
testName: testNameArg,
status: testStatusIMPL
)
testEnded(testResult)
checkpoints.setLen(0)

macro suite*(name: string,body: untyped): untyped =
## Declare a test suite identified by `name` with optional ``setup``
## and/or ``teardown`` section.
##
Expand Down Expand Up @@ -493,32 +534,59 @@ template suite*(name, body) {.dirty.} =
## [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.} =
body.expectKind(nnkStmtList)
# parse top level suite constructs and put their structure into local variables.
var setup: NimNode
var teardown: NimNode
var tests: seq[NimNode]

# stuff that really should not be placed top level in a unittest.
result = newStmtList()

for command in body:
if command.kind in nnkCallKinds:
let typ = command[0]
typ.expectKind nnkIdent
if typ.eqIdent("test"):
command.expectLen 3
let testName = command[1]
testName.expectKind {nnkStrLit..nnkTripleStrLit}
tests.add command
elif typ.eqIdent("setup"):
command.expectLen 2
if setup == nil:
setup = command
else:
error("double definition of setup", command)
elif typ.eqIdent("teardown"):
command.expectLen 2
if teardown == nil:
teardown = command
else:
error("double definition of teardown", command)
else:
error("illegal construct for unittest suite", command)
else:
warning("please only put `test`, `setup` and `teardown` at top level of `suite`", command)
# for backwards compatibility, preserve these constructs unprocessed (really it should be an error)
result.add command

# start generating code from the earlier constructs
result.add newCall(bindSym"suiteStarted", name)

for test in tests:
let tryBody = newStmtList()
if setup != nil:
tryBody.add setup[1]
if teardown != nil:
tryBody.add nnkDefer.newTree(teardown[1])
tryBody.add test[2]
let testName = test[1]
result.add newCall(bindSym"testInternal", name, testName, tryBody)
result.add newCall(bindSym"suiteEnded")

template test*(name: string, body: untyped) =
## Define a single test case identified by `name`.
##
## .. code-block:: nim
Expand All @@ -532,43 +600,8 @@ template test*(name, body) {.dirty.} =
## .. 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 = @[]
testInternal("", name, body)

proc checkpoint*(msg: string) =
## Set a checkpoint identified by `msg`. Upon test failure all
Expand Down Expand Up @@ -639,6 +672,26 @@ proc print[T: not typedesc](name: string, value: T) =
proc print[T](name: string, typ: typedesc[T]) =
checkpoint(name & " was " & $typ)

proc untype(arg: NimNode): NimNode =
case arg.kind
of nnkCharLit..nnkUInt64Lit:
result = newNimNode(arg.kind, arg)
result.intVal = arg.intVal
of nnkFloatLit..nnkFloat128Lit:
result = newNimNode(arg.kind, arg)
result.floatVal = arg.floatVal
of nnkStrLit..nnkTripleStrLit:
result = newNimNode(arg.kind, arg)
result.strVal = arg.strVal
of nnkSym, nnkOpenSymChoice, nnkClosedSymChoice:
result = newIdentNode(arg.repr)
of nnkIdent:
result = arg
else:
result = newNimNode(arg.kind, arg)
for n in arg:
result.add untype(n)

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
Expand All @@ -654,7 +707,8 @@ macro check*(conditions: untyped): untyped =
"AKB48".toLowerAscii() == "akb48"
'C' notin teams

let checked = callsite()[1]
# the call to `untype` is a workaround for https://github.com/nim-works/nimskull/issues/193
let checked = untype(conditions)

proc inspectArgs(exp: NimNode): tuple[assigns, check, printOuts: NimNode] =
result.check = copyNimTree(exp)
Expand Down
4 changes: 2 additions & 2 deletions tests/stdlib/tunittest.nim
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,14 @@ import std/[unittest, sequtils]
proc doThings(spuds: var int): int =
spuds = 24
return 99

test "#964":
var spuds = 0
check doThings(spuds) == 99
check spuds == 24


from std/strutils import toUpperAscii
from std/strutils import toUpperAscii, parseInt
test "#1384":
check(@["hello", "world"].map(toUpperAscii) == @["HELLO", "WORLD"])

Expand All @@ -50,7 +51,6 @@ test "unittest multiple requires":


import std/random
from std/strutils import parseInt
proc defectiveRobot() =
case rand(1..4)
of 1: raise newException(OSError, "CANNOT COMPUTE!")
Expand Down
2 changes: 1 addition & 1 deletion tests/system/tdollars.nim
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ duplication (which always results in weaker test coverage in practice).
]#

import std/unittest
template test[T](a: T, expected: string) =
template test(a, expected: untyped): untyped =
check $a == expected
var b = a
check $b == expected
Expand Down

0 comments on commit 06a1ccf

Please sign in to comment.