mirror of
https://github.com/nim-lang/Nim.git
synced 2026-01-03 19:52:36 +00:00
362 lines
11 KiB
Nim
Executable File
362 lines
11 KiB
Nim
Executable File
#
|
|
#
|
|
# 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.
|
|
##
|
|
## Example:
|
|
##
|
|
## .. code:: nim
|
|
##
|
|
## suite "description for this stuff":
|
|
## 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]
|
|
|
|
import
|
|
macros
|
|
|
|
when declared(stdout):
|
|
import os
|
|
|
|
when not defined(ECMAScript):
|
|
import terminal
|
|
|
|
type
|
|
TestStatus* = enum OK, FAILED ## The status of a test when it is done.
|
|
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.
|
|
|
|
{.deprecated: [TTestStatus: TestStatus, TOutputLevel: OutputLevel]}
|
|
|
|
var ## Global unittest settings!
|
|
|
|
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.
|
|
outputLevel* {.threadvar.}: 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.
|
|
|
|
colorOutput* {.threadvar.}: bool ## Have test results printed in color.
|
|
## Default is true for the non-js target
|
|
## unless, the environment variable
|
|
## ``NIMTEST_NO_COLOR`` is set.
|
|
|
|
checkpoints {.threadvar.}: seq[string]
|
|
|
|
checkpoints = @[]
|
|
|
|
proc shouldRun(testName: string): bool =
|
|
result = true
|
|
|
|
template suite*(name: expr, body: stmt): stmt {.immediate, 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::
|
|
##
|
|
## [OK] 2 + 2 = 4
|
|
## [OK] (2 + -2) != 4
|
|
block:
|
|
template setup(setupBody: stmt): stmt {.immediate, dirty.} =
|
|
var testSetupIMPLFlag = true
|
|
template testSetupIMPL: stmt {.immediate, dirty.} = setupBody
|
|
|
|
template teardown(teardownBody: stmt): stmt {.immediate, dirty.} =
|
|
var testTeardownIMPLFlag = true
|
|
template testTeardownIMPL: stmt {.immediate, dirty.} = teardownBody
|
|
|
|
body
|
|
|
|
proc testDone(name: string, s: TestStatus) =
|
|
if s == FAILED:
|
|
programResult += 1
|
|
|
|
if outputLevel != PRINT_NONE and (outputLevel == PRINT_ALL or s == FAILED):
|
|
template rawPrint() = echo("[", $s, "] ", name)
|
|
when not defined(ECMAScript):
|
|
if colorOutput and not defined(ECMAScript):
|
|
var color = (if s == OK: fgGreen else: fgRed)
|
|
styledEcho styleBright, color, "[", $s, "] ", fgWhite, name
|
|
else:
|
|
rawPrint()
|
|
else:
|
|
rawPrint()
|
|
|
|
template test*(name: expr, body: stmt): stmt {.immediate, 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, testDone
|
|
|
|
if shouldRun(name):
|
|
checkpoints = @[]
|
|
var testStatusIMPL {.inject.} = OK
|
|
|
|
try:
|
|
when declared(testSetupIMPLFlag): testSetupIMPL()
|
|
body
|
|
when declared(testTeardownIMPLFlag):
|
|
defer: testTeardownIMPL()
|
|
|
|
except:
|
|
when not defined(js):
|
|
checkpoint("Unhandled exception: " & getCurrentExceptionMsg())
|
|
echo getCurrentException().getStackTrace()
|
|
fail()
|
|
|
|
finally:
|
|
testDone name, testStatusIMPL
|
|
|
|
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 checkpoints
|
|
for msg in items(checkpoints):
|
|
echo msg
|
|
|
|
when not defined(ECMAScript):
|
|
if abortOnError: quit(1)
|
|
|
|
when declared(testStatusIMPL):
|
|
testStatusIMPL = FAILED
|
|
else:
|
|
programResult += 1
|
|
|
|
checkpoints = @[]
|
|
|
|
macro check*(conditions: stmt): stmt {.immediate.} =
|
|
## 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".toLower() == "akb48")
|
|
##
|
|
## let teams = {'A', 'K', 'B', '4', '8'}
|
|
##
|
|
## check:
|
|
## "AKB48".toLower() == "akb48"
|
|
## 'C' in teams
|
|
let checked = callsite()[1]
|
|
var
|
|
argsAsgns = newNimNode(nnkStmtList)
|
|
argsPrintOuts = newNimNode(nnkStmtList)
|
|
counter = 0
|
|
|
|
template asgn(a, value: expr): stmt =
|
|
var a = value # XXX: we need "var: var" here in order to
|
|
# preserve the semantics of var params
|
|
|
|
template print(name, value: expr): stmt =
|
|
when compiles(string($value)):
|
|
checkpoint(name & " was " & $value)
|
|
|
|
proc inspectArgs(exp: NimNode): NimNode =
|
|
result = copyNimTree(exp)
|
|
for i in countup(1, exp.len - 1):
|
|
if exp[i].kind notin nnkLiterals:
|
|
inc counter
|
|
var arg = newIdentNode(":p" & $counter)
|
|
var argStr = exp[i].toStrLit
|
|
var paramAst = exp[i]
|
|
if exp[i].kind == nnkIdent:
|
|
argsPrintOuts.add getAst(print(argStr, paramAst))
|
|
if exp[i].kind in nnkCallKinds:
|
|
var callVar = newIdentNode(":c" & $counter)
|
|
argsAsgns.add getAst(asgn(callVar, paramAst))
|
|
result[i] = callVar
|
|
argsPrintOuts.add getAst(print(argStr, callVar))
|
|
if exp[i].kind == nnkExprEqExpr:
|
|
# ExprEqExpr
|
|
# Ident !"v"
|
|
# IntLit 2
|
|
result[i] = exp[i][1]
|
|
if exp[i].typekind notin {ntyTypeDesc}:
|
|
argsAsgns.add getAst(asgn(arg, paramAst))
|
|
argsPrintOuts.add getAst(print(argStr, arg))
|
|
if exp[i].kind != nnkExprEqExpr:
|
|
result[i] = arg
|
|
else:
|
|
result[i][1] = arg
|
|
|
|
case checked.kind
|
|
of nnkCallKinds:
|
|
template rewrite(call, lineInfoLit: expr, callLit: string,
|
|
argAssgs, argPrintOuts: stmt): stmt =
|
|
block:
|
|
argAssgs #all callables (and assignments) are run here
|
|
if not call:
|
|
checkpoint(lineInfoLit & ": Check failed: " & callLit)
|
|
argPrintOuts
|
|
fail()
|
|
|
|
var checkedStr = checked.toStrLit
|
|
let parameterizedCheck = inspectArgs(checked)
|
|
result = getAst(rewrite(parameterizedCheck, checked.lineinfo, checkedStr,
|
|
argsAsgns, argsPrintOuts))
|
|
|
|
of nnkStmtList:
|
|
result = newNimNode(nnkStmtList)
|
|
for i in countup(0, checked.len - 1):
|
|
if checked[i].kind != nnkCommentStmt:
|
|
result.add(newCall(!"check", checked[i]))
|
|
|
|
else:
|
|
template rewrite(Exp, lineInfoLit: expr, expLit: string): stmt =
|
|
if not Exp:
|
|
checkpoint(lineInfoLit & ": Check failed: " & expLit)
|
|
fail()
|
|
|
|
result = getAst(rewrite(checked, checked.lineinfo, checked.toStrLit))
|
|
|
|
template require*(conditions: stmt): stmt {.immediate.} =
|
|
## 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[expr], body: stmt): stmt {.immediate.} =
|
|
## 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
|
|
## 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: expr,
|
|
body: stmt): 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))
|
|
|
|
|
|
when declared(stdout):
|
|
# Reading settings
|
|
# On a terminal this branch is executed
|
|
var envOutLvl = os.getEnv("NIMTEST_OUTPUT_LVL").string
|
|
abortOnError = existsEnv("NIMTEST_ABORT_ON_ERROR")
|
|
colorOutput = not existsEnv("NIMTEST_NO_COLOR")
|
|
|
|
else:
|
|
var envOutLvl = "" # TODO
|
|
colorOutput = false
|
|
|
|
if envOutLvl.len > 0:
|
|
for opt in countup(low(OutputLevel), high(OutputLevel)):
|
|
if $opt == envOutLvl:
|
|
outputLevel = opt
|
|
break
|