568 lines
16 KiB
Nim
568 lines
16 KiB
Nim
## :Author: Joey Payne
|
|
## This module is an alternate implementation of
|
|
## the unittest module in Nim. Inspired by the python
|
|
## unit test module.
|
|
##
|
|
## Example:
|
|
##
|
|
## .. code:: nim
|
|
##
|
|
## testSuite UnitTests:
|
|
## proc thisIsATest()=
|
|
## self.assertEquals(1, 1)
|
|
## self.assertTrue(2 != 3)
|
|
## self.assertFalse(3 == 4)
|
|
## self.assertRaises(OSError, newException(OSError, "OS is exploding!"))
|
|
##
|
|
import macros
|
|
import strutils
|
|
when not defined(ECMAScript):
|
|
import terminal
|
|
|
|
proc `$`*[T](ar: openarray[T]): string=
|
|
## Converts an array into a string
|
|
result = "["
|
|
if ar.len() > 0:
|
|
result &= $ar[0]
|
|
for i in 1..ar.len()-1:
|
|
result &= ", " & $ar[i]
|
|
result &= "]"
|
|
return result
|
|
|
|
proc `==`*[T](ar: openarray[T], ar2: openarray[T]): bool=
|
|
if len(ar) != len(ar2):
|
|
return false
|
|
for i in countup(0, ar.len()):
|
|
if ar[i] != ar2[i]:
|
|
return false
|
|
return true
|
|
|
|
type
|
|
TestSuite = ref object of RootObj
|
|
## The base TestSuite
|
|
name: string
|
|
currentTestName: string
|
|
testsPassed: int
|
|
numTests: int
|
|
|
|
TestAssertError = object of Exception
|
|
## assertTrue and other assert_* statements will raise
|
|
## this exception when the condition fails
|
|
lineNumber: int
|
|
fileName: string
|
|
codeSnip: string
|
|
testName: string
|
|
|
|
# -- Methods for the TestSuite base --
|
|
|
|
method setup*(suite: TestSuite)=
|
|
## Base method for setup code
|
|
discard
|
|
|
|
method tearDown*(suite: TestSuite)=
|
|
## Base method for setup code
|
|
discard
|
|
|
|
method runTests*(suite: TestSuite)=
|
|
## Base method for running tests
|
|
discard
|
|
|
|
# ------------------------------------
|
|
|
|
|
|
template returnException(name, testName, snip, vals)=
|
|
## private template for raising an exception
|
|
let pos = instantiationInfo(fullpaths=true)
|
|
let posRel = instantiationInfo()
|
|
var
|
|
filename = posRel.filename
|
|
line = pos.line
|
|
var message = "\n"
|
|
message &= " Condition: $2($1)\n".format(snip.replace("\n","").strip(), name)
|
|
message &= " Reason: $1\n".format(vals)
|
|
message &= " Location: $1; line $2".format(filename, line)
|
|
|
|
var exc = newException(TestAssertError, message)
|
|
exc.fileName = filename
|
|
exc.lineNumber = line
|
|
exc.codeSnip = snip
|
|
exc.testName = testName
|
|
raise exc
|
|
|
|
# ------------------------ Templates for assertion ----------------------------
|
|
|
|
template assertRaises*(self: TestSuite, error: Exception,
|
|
code: untyped): untyped {.immediate.}=
|
|
## Raises a TestAssertError when the exception "error" is
|
|
## not thrown in the code
|
|
try:
|
|
code
|
|
var
|
|
snip = astToStr(code)
|
|
vals = "No Exception Raised"
|
|
testName = self.currentTestName
|
|
|
|
returnException("assertRaises", testName, snip, vals)
|
|
|
|
except error:
|
|
discard
|
|
except TestAssertError:
|
|
raise
|
|
except Exception:
|
|
var
|
|
snip = astToStr(code)
|
|
vals = "Exception != $1".format(astToStr(error))
|
|
testName = self.currentTestName
|
|
|
|
returnException("assertRaises", testName, snip, vals)
|
|
|
|
macro isInfix(code: untyped): untyped=
|
|
if code.kind == nnkInfix:
|
|
return newLit(true)
|
|
return newLit(false)
|
|
|
|
macro getLhs(code: untyped): untyped=
|
|
return code[1].toStrLit()
|
|
|
|
macro getLhsVal(code: untyped): untyped=
|
|
return code[1]
|
|
|
|
macro getRhs(code: untyped): untyped=
|
|
return code[2]
|
|
|
|
macro getRhsVal(code: untyped): untyped=
|
|
return code[2]
|
|
|
|
template assert*(self: TestSuite, code: untyped){.immediate.}=
|
|
## Assertions for tests
|
|
if not code:
|
|
|
|
var
|
|
snip = ""
|
|
vals = ""
|
|
testName = self.currentTestName
|
|
|
|
when isInfix(code) == 0:
|
|
snip = astToStr(code)
|
|
vals = "$1 == $2".format(snip, code)
|
|
else:
|
|
snip = astToStr(code)
|
|
var
|
|
lhs = getLhs(code)
|
|
lhsVal = getLhsVal(code)
|
|
rhs = getRhs(code)
|
|
rhsVal = getRhsVal(code)
|
|
if $lhs != $lhsVal:
|
|
vals &= "$1 == $2".format(lhs, lhsVal)
|
|
if $rhs != $rhsVal:
|
|
vals &= "$1 == $2".format(rhs, rhsVal)
|
|
|
|
returnException("assert", testName, snip, vals)
|
|
|
|
# -----------------------------------------------------------------------------
|
|
|
|
|
|
# A list to hold all test suites that are created
|
|
var testSuites: seq[TestSuite] = @[]
|
|
|
|
macro testSuite*(head: untyped, body: untyped): untyped =
|
|
## Compile-time macro that allows a user to define tests and run them
|
|
##
|
|
## Methods are used for inheritance, so if you want to derive a test
|
|
## suite, then you have to make sure the base suite uses methods
|
|
## for the tests that you want to derive.
|
|
##
|
|
## If you don't want inheritance, you can just use procs.
|
|
##
|
|
## A special proc/method is called setup(). The macro will inject
|
|
## this if it doesn't exist and it will be called before running
|
|
## the test suite.
|
|
##
|
|
## Test methods/procs to be run are prefixed with "test" in the
|
|
## method/proc name. This is so that you can write tests that call
|
|
## procs that do other things and won't be run as a test.
|
|
##
|
|
## For each suite method/proc, an implicit variable called "self"
|
|
## is added. This lets you access the testSuite in an OO kind
|
|
## of way.
|
|
##
|
|
## Usage:
|
|
##
|
|
## .. code:: nim
|
|
##
|
|
## testSuite SuiteName of TestSuite:
|
|
##
|
|
## var
|
|
## suiteVar: string
|
|
##
|
|
## method setup()=
|
|
## ## do setup code here
|
|
## self.suiteVar = "Testing"
|
|
##
|
|
## method testAddingString()=
|
|
## ## adds a string to the suiteVar
|
|
## self.suiteVar &= " 123"
|
|
## self.assertEqual(self.suiteVar, "Testing 123")
|
|
##
|
|
## when isMainModule:
|
|
## einheit.runTests()
|
|
##
|
|
|
|
|
|
# object reference name inside methods.
|
|
# ie: self, self
|
|
let objReference = "self"
|
|
var exportClass: bool = false
|
|
|
|
template importRequiredLibs()=
|
|
import strutils
|
|
when not defined(ECMAScript):
|
|
import terminal
|
|
|
|
var typeName, baseName: NimNode
|
|
|
|
if head.kind == nnkIdent:
|
|
# `head` is expression `typeName`
|
|
# echo head.treeRepr
|
|
# --------------------
|
|
# Ident !"UnitTests"
|
|
typeName = head
|
|
|
|
elif head.kind == nnkInfix and $head[0] == "of":
|
|
# `head` is expression `typeName of baseClass`
|
|
# echo head.treeRepr
|
|
# --------------------
|
|
# Infix
|
|
# Ident !"of"
|
|
# Ident !"UnitTests"
|
|
# Ident !"RootObj"
|
|
typeName = head[1]
|
|
baseName = head[2]
|
|
|
|
elif head.kind == nnkInfix and $head[0] == "*" and $head[1] == "of":
|
|
# echo head.treeRepr
|
|
# -----------
|
|
# Infix
|
|
# Ident !"*"
|
|
# Ident !"UnitTests
|
|
# Prefix
|
|
# Ident !"of"
|
|
# Ident !"RootObj"
|
|
exportClass = true
|
|
typeName = head[1]
|
|
baseName = head[2][1]
|
|
elif head.kind == nnkInfix and $head[0] == "*":
|
|
exportClass = true
|
|
typeName = head[1]
|
|
else:
|
|
quit "Invalid node: " & head.lispRepr
|
|
|
|
|
|
# echo treeRepr(body)
|
|
# --------------------
|
|
# StmtList
|
|
# VarSection
|
|
# IdentDefs
|
|
# Ident !"name"
|
|
# Ident !"string"
|
|
# Empty
|
|
# IdentDefs
|
|
# Ident !"age"
|
|
# Ident !"int"
|
|
# Empty
|
|
# MethodDef
|
|
# Ident !"vocalize"
|
|
# Empty
|
|
# Empty
|
|
# FormalParams
|
|
# Ident !"string"
|
|
# Empty
|
|
# Empty
|
|
# StmtList
|
|
# StrLit ...
|
|
# MethodDef
|
|
# Ident !"ageHumanYrs"
|
|
# Empty
|
|
# Empty
|
|
# FormalParams
|
|
# Ident !"int"
|
|
# Empty
|
|
# Empty
|
|
# StmtList
|
|
# DotExpr
|
|
# Ident !"self"
|
|
# Ident !"age"
|
|
|
|
# create a new stmtList for the result
|
|
result = newStmtList()
|
|
|
|
# var declarations will be turned into object fields
|
|
var recList = newNimNode(nnkRecList)
|
|
|
|
# add a super function to simulate OOP
|
|
# inheritance tree (Doesn't do what is expected because of dynamic binding)
|
|
#if not isNil(`baseName`):
|
|
# var super = quote do:
|
|
# proc super(self: `typeName`): `baseName`=
|
|
# return `baseName`(self)
|
|
# result.add(super)
|
|
|
|
template setNodeName(n2, procName, typeName)=
|
|
if n2.name.kind == nnkIdent:
|
|
procName = $(n2.name.toStrLit())
|
|
n2.name = ident(procName & typeName)
|
|
elif n2.name.kind == nnkPostFix:
|
|
if n2.name[1].kind == nnkIdent:
|
|
procName = $(n2.name[1].toStrLit())
|
|
n2.name[1] = ident(procName & typeName)
|
|
elif n2.name[1].kind == nnkAccQuoted:
|
|
procName = $(n2.name[1][0].toStrLit())
|
|
n2.name[1][0] = ident(procName & typeName)
|
|
elif n2.name.kind == nnkAccQuoted:
|
|
procName = $(n2.name[0].toStrLit())
|
|
n2.name[0] = ident(procName & typeName)
|
|
result.add(n2)
|
|
|
|
|
|
template runTestsProc(self, typeName, baseMethod, typeMethod)=
|
|
method typeMethod(self: typeName)=
|
|
when compiles(self.baseMethod()):
|
|
self.baseMethod()
|
|
|
|
method runTests(self: typeName)=
|
|
self.typeMethod()
|
|
|
|
var baseMethodName = ident("runTests" & $baseName.toStrLit())
|
|
var typeMethodName = ident("runTests" & $typeName.toStrLit())
|
|
|
|
var runTests = getAst(runTestsProc(ident(objReference), typeName, baseMethodName, typeMethodName))
|
|
|
|
var
|
|
foundSetup = false
|
|
foundTeardown = false
|
|
|
|
# Make forward declarations so that function order
|
|
# does not matter, just like in real OOP!
|
|
for node in body.children:
|
|
case node.kind:
|
|
of nnkMethodDef, nnkProcDef:
|
|
# inject `self: T` into the arguments
|
|
let n = copyNimTree(node)
|
|
n.params.insert(1, newIdentDefs(ident(objReference), typeName))
|
|
# clear the body so we only get a
|
|
# declaration
|
|
n.body = newEmptyNode()
|
|
result.add(n)
|
|
|
|
# forward declare the inheritable method
|
|
let n2 = copyNimTree(n)
|
|
let typeName = $(typeName.toStrLit())
|
|
var procName = ""
|
|
|
|
setNodeName(n2, procName, typeName)
|
|
|
|
if procName.toLower() == "setup":
|
|
foundSetup = true
|
|
if procName.toLower() == "teardown":
|
|
foundTeardown = true
|
|
else:
|
|
discard
|
|
|
|
if not foundSetup:
|
|
template setupProc(self, typeName, setupProc)=
|
|
method setup(self: typeName)
|
|
method setupProc(self: typeName)
|
|
|
|
template setupDecl(self, baseMethod)=
|
|
method setup()=
|
|
self.baseMethod()
|
|
|
|
var setupProcTypename = ident("setup" & $typeName.toStrLit())
|
|
var baseMethodName = ident("setup" & $baseName.toStrLit())
|
|
result.add(getAst(setupProc(ident(objReference), typeName, setupProcTypename)))
|
|
var setupBaseAst = getAst(setupDecl(ident(objReference), baseMethodName))
|
|
body.add(setupBaseAst[0])
|
|
|
|
if not foundTeardown:
|
|
template teardownProc(self, typeName, tdProc)=
|
|
method tearDown(self: typeName)
|
|
method tdProc(self: typeName)
|
|
|
|
template teardownDecl(self, baseMethod)=
|
|
method tearDown()=
|
|
when compiles(self.baseMethod()):
|
|
self.baseMethod()
|
|
|
|
var teardownProcTypename = ident("tearDown" & $typeName.toStrLit())
|
|
var baseTearMethodName = ident("tearDown" & $baseName.toStrLit())
|
|
result.add(getAst(teardownProc(ident(objReference),
|
|
typeName,
|
|
teardownProcTypename)))
|
|
var teardownBaseAst = getAst(teardownDecl(ident(objReference),
|
|
baseTearMethodName))
|
|
body.add(teardownBaseAst[0])
|
|
|
|
|
|
# Iterate over the statements, adding `self: T`
|
|
# to the parameters of functions
|
|
for node in body.children:
|
|
case node.kind:
|
|
of nnkMethodDef, nnkProcDef:
|
|
# inject `self: T` into the arguments
|
|
let n = copyNimTree(node)
|
|
n.params.insert(1, newIdentDefs(ident(objReference), typeName))
|
|
|
|
# Copy the proc or method for inheritance
|
|
# ie: procName_ClassName()
|
|
let n2 = copyNimTree(node)
|
|
n2.params.insert(1, newIdentDefs(ident(objReference), typeName))
|
|
|
|
let typeName = $(typeName.toStrLit())
|
|
var procName = $(n2.name.toStrLit())
|
|
var isAssignment = procName.contains("=")
|
|
|
|
setNodeName(n2, procName, typeName)
|
|
|
|
if procName.toLower() == "setup":
|
|
let dotName = newDotExpr(ident(objReference), ident("name"))
|
|
let setName = newAssignment(dotName, newLit(typeName))
|
|
n2.body.add(setName)
|
|
elif procName.toLower() == "teardown":
|
|
discard
|
|
elif procName.startswith("test"):
|
|
let procCall = newDotExpr(ident(objReference),
|
|
ident(procName & typeName))
|
|
|
|
template setTestName(self, procName)=
|
|
self.currentTestName = procName
|
|
|
|
template tryBlock(self, testCall)=
|
|
self.numTests += 1
|
|
try:
|
|
testCall
|
|
styledEcho(styleBright, fgGreen, "[OK]",
|
|
fgWhite, " ", self.currentTestName)
|
|
self.testsPassed += 1
|
|
except TestAssertError:
|
|
let e = (ref TestAssertError)(getCurrentException())
|
|
styledEcho(styleBright,
|
|
fgRed, "[Failed]",
|
|
fgWhite, " ", self.currentTestName, e.msg)
|
|
|
|
runTests[0][6].add(getAst(setTestName(ident(objReference), procName)))
|
|
runTests[0][6].add(getAst(tryBlock(ident(objReference), procCall)))
|
|
|
|
# simply call the class method from here
|
|
# proc procName=
|
|
# procName_ClassName()
|
|
var p: seq[NimNode] = @[]
|
|
for i in 1..n.params.len-1:
|
|
p.add(n.params[i][0])
|
|
if isAssignment:
|
|
let dot = newDotExpr(ident(objReference), ident(procName & typeName))
|
|
n.body = newStmtList(newAssignment(dot, p[1]))
|
|
else:
|
|
n.body = newStmtList(newCall(procName & typeName, p))
|
|
|
|
result.add(n)
|
|
|
|
of nnkVarSection:
|
|
# variables get turned into fields of the type.
|
|
for n in node.children:
|
|
recList.add(n)
|
|
else:
|
|
result.add(node)
|
|
|
|
# The following prints out the AST structure:
|
|
#
|
|
# import macros
|
|
# dumptree:
|
|
# type X = ref object of Y
|
|
# z: int
|
|
# --------------------
|
|
# TypeSection
|
|
# TypeDef
|
|
# Ident !"X"
|
|
# Empty
|
|
# RefTy
|
|
# ObjectTy
|
|
# Empty
|
|
# OfInherit
|
|
# Ident !"Y"
|
|
# RecList
|
|
# IdentDefs
|
|
# Ident !"z"
|
|
# Ident !"int"
|
|
# Empty
|
|
|
|
var typeDecl: NimNode
|
|
|
|
template declareTypeExport(tname, bname)=
|
|
type tname* = ref object of bname
|
|
template declareType(tname, bname)=
|
|
type tname = ref object of bname
|
|
|
|
if baseName == nil:
|
|
if exportClass:
|
|
typeDecl = getAst(declareTypeExport(typeName, TestSuite))
|
|
else:
|
|
typeDecl = getAst(declareType(typeName, TestSuite))
|
|
else:
|
|
if exportClass:
|
|
typeDecl = getAst(declareTypeExport(typeName, baseName))
|
|
else:
|
|
typeDecl = getAst(declareType(typeName, baseName))
|
|
|
|
# Inspect the tree structure:
|
|
#
|
|
# echo typeDecl.treeRepr
|
|
# --------------------
|
|
# StmtList
|
|
# TypeSection
|
|
# TypeDef
|
|
# Ident !"UnitTests"
|
|
# Empty
|
|
# RefTy
|
|
# ObjectTy
|
|
# Empty
|
|
# OfInherit
|
|
# Ident !"RootObj"
|
|
# Empty <= We want to replace this
|
|
typeDecl[0][0][2][0][2] = recList
|
|
|
|
# insert the type declaration
|
|
result.insert(0, typeDecl)
|
|
|
|
# insert libs needed
|
|
result.insert(0, getAst(importRequiredLibs()))
|
|
|
|
result.add(runTests)
|
|
|
|
template addTestSuite(typeName)=
|
|
testSuites.add(typeName())
|
|
|
|
result.add(getAst(addTestSuite(typeName)))
|
|
|
|
|
|
proc runTests*()=
|
|
## The method that runs the tests. Invoke
|
|
## after setting up all of the tests and
|
|
## usually inside a "when isMainModule" block
|
|
for suite in testSuites:
|
|
suite.setup()
|
|
|
|
styledEcho(styleBright,
|
|
fgYellow, "\n[Running]",
|
|
fgWhite, " $1\n".format(suite.name))
|
|
suite.runTests()
|
|
suite.tearDown()
|
|
|
|
# Output red if tests didn't pass, green otherwise
|
|
var color = fgGreen
|
|
if suite.testsPassed != suite.numTests:
|
|
color = fgRed
|
|
|
|
styledEcho(styleBright, color,
|
|
"\n[", $suite.testsPassed, "/", $suite.numTests, "]",
|
|
fgWhite, " tests passed.\n")
|
|
|