einheit/einheit.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")