Make test macro work in compile only mode

This commit is contained in:
Joey Payne 2019-02-27 19:08:23 -07:00
commit 7d44349f2b

View file

@ -1,24 +1,32 @@
import macros, tables, strutils import macros
import tables, strutils, os
export tables, strutils, os
when defined(ECMAScript): when defined(ECMAScript):
const noColors = true const noColors = true
else: else:
const noColors = defined(noColors) const noColors = defined(noColors)
import terminal import terminal
export terminal
import private/utils import private/utils
type type
Test = ref object Test = ref object
procDef: proc(test: Test) procDef: NimNode
name: string name: NimNode
TestsInfo = ref object TestsModule = ref object
## The base TestSuite ## The base TestSuite
fileName: string fileName: string
currentTestName: string tests: seq[Test]
TestsInfo = ref object
fileName: string
numTests: int
testsPassed: int testsPassed: int
lastTestFailed: bool lastTestFailed: bool
tests: seq[Test]
TestAssertError = object of Exception TestAssertError = object of Exception
## check and other check* statements will raise ## check and other check* statements will raise
@ -31,7 +39,11 @@ type
checkFuncName: string checkFuncName: string
valTable: Table[string, string] valTable: Table[string, string]
var testsInfoMap = newTable[string, TestsInfo]() PrivateTestError = object of TestAssertError
## Raised when a test tries to access something
## that is private to a module
var testsModuleMap {.compileTime.} = newTable[string, TestsModule]()
proc `==`*[T](ar: openarray[T], ar2: openarray[T]): bool = proc `==`*[T](ar: openarray[T], ar2: openarray[T]): bool =
## helper proc to compare arrays ## helper proc to compare arrays
@ -51,8 +63,9 @@ template returnException(name, testName, snip, vals, pos, posRel) =
var message = "\l" var message = "\l"
message &= " Condition: $2($1)\l".format(snip, name) message &= " Condition: $2($1)\l".format(snip, name)
message &= " Where:\l"
if vals.len() > 0:
message &= " Where:\l"
for k, v in vals.pairs: for k, v in vals.pairs:
message &= " $1 -> $2\l".format(k, v) message &= " $1 -> $2\l".format(k, v)
@ -69,7 +82,7 @@ template returnException(name, testName, snip, vals, pos, posRel) =
raise exc raise exc
proc `$`(test: Test): string = proc `$`(test: Test): string =
return "proc `"&test.name&"`()" return "proc `" & $test.name.toStrLit & "`()"
proc `$`*[T](ar: openarray[T]): string = proc `$`*[T](ar: openarray[T]): string =
## Converts an array into a string ## Converts an array into a string
@ -139,36 +152,13 @@ macro toString*(obj: typed): untyped =
$(obj) $(obj)
result = getAst(toStrAst(obj)) result = getAst(toStrAst(obj))
proc getTestsInfo(name: string): TestsInfo = proc getTestsModule(name: string): TestsModule {.compileTime.} =
if not testsInfoMap.hasKey(name): if not testsModuleMap.hasKey(name):
testsInfoMap[name] = TestsInfo(fileName: name) testsModuleMap[name] = TestsModule(fileName: name)
return testsInfoMap[name] return testsModuleMap[name]
proc addTest*(testsInfo: TestsInfo, procDef: proc(test: Test), name: string) = proc addTest(testsModule: TestsModule, procDef: NimNode, name: NimNode) =
testsInfo.tests.add(Test(procDef: procDef, name: name)) testsModule.tests.add(Test(procDef: procDef, name: name))
template addToTests(body, name, sym) =
let
posRel = instantiationInfo()
testsInfo = getTestsInfo(posRel.filename)
testsInfo.addTest(proc(sym: Test) = body, name)
macro test*(name: string, body: untyped): untyped =
let sym = genSym(nskParam, "t")
body.insert(0,
nnkLetSection.newTree(
nnkIdentDefs.newTree(
ident("self"),
newEmptyNode(),
sym
)
)
)
result = getAst(addToTests(body, name, sym))
template strRep(n: NimNode): untyped = template strRep(n: NimNode): untyped =
toString(n) toString(n)
@ -233,68 +223,52 @@ macro getSyms(code:untyped): untyped =
template check*(code: untyped) = template check*(code: untyped) =
## Assertions for tests ## Assertions for tests
if not code: if not code:
# These need to be here to capture the actual info
let let
pos = instantiationInfo(fullpaths=true) pos = instantiationInfo(fullpaths=true)
posRel = instantiationInfo() posRel = instantiationInfo()
vals = getSyms(code)
var snip = ""
let testName = $self.name
var vals = getSyms(code)
# get ast string with extra spaces ignored # get ast string with extra spaces ignored
snip = astToStr(code).strip().split({'\t', '\v', '\c', '\n', '\f'}).join("; ") snip = astToStr(code).strip().split({'\t', '\v', '\c', '\n', '\f'}).join("; ")
returnException("check", testName, snip, vals, pos, posRel) returnException("check", testName, snip, vals, pos, posRel)
template checkRaises*(error: untyped, template wrapCode(code): untyped =
code: untyped): untyped = # This is needed to prevent an "unreachable code" error
# if the code block raises an exception
(proc () = code)()
template checkRaises*(error: untyped, code: untyped): untyped =
## Raises a TestAssertError when the exception "error" is ## Raises a TestAssertError when the exception "error" is
## not thrown in the code ## not thrown in the code
let let
pos = instantiationInfo(fullpaths=true) pos = instantiationInfo(fullpaths=true)
posRel = instantiationInfo() posRel = instantiationInfo()
when error isnot Exception:
try: try:
code wrapCode(code)
let let
codeStr = astToStr(code).split().join(" ") codeStr = astToStr(code).strip().split().join(" ")
snip = "$1, $2".format(astToStr(error), codeStr) snip = "$1, $2".format(astToStr(error), codeStr)
vals = {codeStr: "No Exception Raised"}.toTable() vals = {codeStr: "No Exception Raised"}.toTable()
testName = $self.name testName = testName
returnException("checkRaises", testName, snip, vals, pos, posRel) returnException("checkRaises", testName, snip, vals, pos, posRel)
except error: except error:
discard discard
except TestAssertError: except TestAssertError:
raise raise
except Exception: except:
let let
e = getCurrentException() e = getCurrentException()
codeStr = astToStr(code).split().join(" ") codeStr = astToStr(code).strip().split().join(" ")
snip = "$1, $2".format(astToStr(error), codeStr) snip = "$1, $2".format(astToStr(error), codeStr)
vals = {codeStr: $e.name}.toTable() vals = {codeStr: $e.name}.toTable()
testName = $self.name testName = testName
returnException("checkRaises", testName, snip, vals, pos, posRel) returnException("checkRaises", testName, snip, vals, pos, posRel)
else:
try:
code
let
codeStr = astToStr(code).split().join(" ")
snip = "$1, $2".format(astToStr(error), codeStr)
vals = {codeStr: "No Exception Raised"}.toTable()
testName = $self.name
returnException("checkRaises", testName, snip, vals, pos, posRel)
except error:
discard
except TestAssertError:
raise
proc printRunning(info: TestsInfo) = proc printRunning*(testsInfo: TestsInfo) =
let termSize = getTermSize() let termSize = getTermSize()
var var
numTicks = termSize.width numTicks = termSize.width
@ -309,21 +283,25 @@ proc printRunning(info: TestsInfo) =
styleBright, styleBright,
fgYellow, "\l" & ticks, fgYellow, "\l" & ticks,
fgYellow, "\l\l[Running]", fgYellow, "\l\l[Running]",
fgWhite, " tests in $1 ".format(info.fileName) fgWhite, " tests in $1 ".format(testsInfo.fileName)
) )
else: else:
echo "\l$1\l".format(ticks) echo "\l$1\l".format(ticks)
echo "[Running] tests in $1".format(info.name) echo "[Running] tests in $1".format(testsInfo.fileName)
proc printPassedTests*(info: TestsInfo) =
if info.testsPassed == 0 and info.numTests == 0:
return
proc printPassedTests(info: TestsInfo) =
when not noColors: when not noColors:
# Output red if tests didn't pass, green otherwise # Output red if tests didn't pass, green otherwise
var color = fgGreen var color = fgGreen
if info.testsPassed != info.tests.len(): if info.testsPassed != info.numTests:
color = fgRed color = fgRed
var passedStr = "[" & $info.testsPassed & "/" & $info.tests.len() & "]" var passedStr = "[" & $info.testsPassed & "/" & $info.numTests & "]"
when not defined(quiet): when not defined(quiet):
when not noColors: when not noColors:
@ -335,15 +313,10 @@ proc printPassedTests(info: TestsInfo) =
echo "\l$1 tests passed for $2.".format(passedStr, info.fileName) echo "\l$1 tests passed for $2.".format(passedStr, info.fileName)
proc runTests(info: TestsInfo) = template runTest(procCall, info, testName) =
when noColors:
stdout.write(info.fileName & " ")
else:
setForegroundColor(fgWhite)
writeStyled(info.fileName & " ", {styleBright})
for t in info.tests:
try: try:
t.procDef(t) procCall
when defined(quiet): when defined(quiet):
when noColors: when noColors:
stdout.write(".") stdout.write(".")
@ -358,15 +331,53 @@ proc runTests(info: TestsInfo) =
when not noColors: when not noColors:
styledEcho(styleBright, fgGreen, okStr, styledEcho(styleBright, fgGreen, okStr,
fgWhite, " ", t.name) fgWhite, " ", testName)
else: else:
echo "$1 $2".format(okStr, t.name) echo "$1 $2".format(okStr, t.name)
info.testsPassed += 1 info.testsPassed += 1
info.lastTestFailed = false info.lastTestFailed = false
except TestAssertError: except PrivateTestError as e:
let e = (ref TestAssertError)(getCurrentException()) info.numTests -= 1
when defined(quiet):
when noColors:
stdout.write("N")
else:
setForegroundColor(fgBlue)
writeStyled("N", {styleBright})
setForegroundColor(fgWhite)
else:
when not noColors:
styledEcho(styleBright,
fgBlue, "\l[Not run]",
fgWhite, " ", testName)
else:
echo "\l[Not run] $1".format(t.name)
let
name = e.checkFuncName
snip = e.codeSnip
line = e.lineNumber
col = e.column
filename = e.fileName
vals = e.valTable
when not noColors:
styledEcho(styleDim, fgWhite,
" Test code contains private or non accessible symbols:")
styledEcho(styleDim, fgGreen, " " & snip)
styledEcho(
styleDim, fgWhite,
" Location: $1; line $2; col $3".format(filename, line, col))
else:
echo " Test code contains private or non accessible symbols:\l $1".format(snip)
echo " Location: $1; line $2; col: $3".format(filename, line, col)
info.lastTestFailed = true
except TestAssertError as e:
when defined(quiet): when defined(quiet):
when noColors: when noColors:
stdout.write("F") stdout.write("F")
@ -378,7 +389,7 @@ proc runTests(info: TestsInfo) =
when not noColors: when not noColors:
styledEcho(styleBright, styledEcho(styleBright,
fgRed, "\l[Failed]", fgRed, "\l[Failed]",
fgWhite, " ", t.name) fgWhite, " ", testName)
else: else:
echo "\l[Failed] $1".format(t.name) echo "\l[Failed] $1".format(t.name)
@ -391,7 +402,10 @@ proc runTests(info: TestsInfo) =
vals = e.valTable vals = e.valTable
when not noColors: when not noColors:
styledEcho(styleDim, fgWhite, " Condition: $2($1)\l".format(snip, name), " Where:") styledEcho(styleDim, fgWhite, " Condition: $2($1)".format(snip, name))
if vals.len > 0:
styledEcho(styleDim, fgWhite, " Where:")
for k, v in vals.pairs: for k, v in vals.pairs:
styledEcho(styleDim, fgCyan, " ", k, styledEcho(styleDim, fgCyan, " ", k,
fgWhite, " -> ", fgWhite, " -> ",
@ -401,14 +415,15 @@ proc runTests(info: TestsInfo) =
" Location: $1; line $2; col $3".format(filename, line, col)) " Location: $1; line $2; col $3".format(filename, line, col))
else: else:
echo " Condition: $2($1)".format(snip, name) echo " Condition: $2($1)".format(snip, name)
if vals.len > 0:
echo " Where:" echo " Where:"
for k, v in vals.pairs: for k, v in vals.pairs:
echo " ", k, " -> ", v echo " ", k, " -> ", v
echo " Location: $1; line $2; col: $3".format(filename, line, col) echo " Location: $1; line $2; col: $3".format(filename, line, col)
info.lastTestFailed = true info.lastTestFailed = true
echo ""
proc printSummary(totalTestsPassed: int, totalTests: int) = proc printSummary(totalTestsPassed: int, totalTests: int) =
when not noColors: when not noColors:
@ -450,23 +465,119 @@ proc printSummary(totalTestsPassed: int, totalTests: int) =
echo "\l[Summary]" echo "\l[Summary]"
echo "\l $1 tests passed.".format(passedStr) echo "\l $1 tests passed.".format(passedStr)
proc runTests*() = template createRunTests(tests, testsInfo, totalTests, totalTestsPassed) =
testsInfo.printRunning()
when defined(quiet):
echo ""
when noColors:
stdout.write(testsInfo.fileName & " ")
else:
setForegroundColor(fgWhite)
writeStyled(testsInfo.fileName & " ", {styleBright})
tests
testsInfo.printPassedTests()
totalTests += testsInfo.numTests
totalTestsPassed += testsInfo.testsPassed
template makeProc(body, tnameSym, tName, lineInfo, currentDir) =
when not compiles((proc (tnameSym: string) = body)(tName)):
(proc (tnameSym: string) =
let
#vals = getSyms(body)
# get ast string with extra spaces ignored
astBody = astToStr(body).strip().split({'\t', '\v', '\c', '\n', '\f'})
snip = astBody[1..<astBody.len].join("\l ")
var
filename = lineInfo.fileName.relativePath(currentDir)
line = lineInfo.line
col = lineInfo.column
var message = "\lCode contains private or non accessible symbols."
message &= " Location: $1; line $2; col: $3".format(filename, line, col)
var exc = newException(PrivateTestError, message)
exc.fileName = filename
exc.lineNumber = line
exc.column = col
exc.codeSnip = snip
exc.testName = tName
#exc.valTable = vals
exc.checkFuncName = tnameSym
raise exc
)(tName)
else:
(proc (tnameSym: string) = body)(tName)
template createInfo(infoSym, name, ntests, currentDir) =
let infoSym = TestsInfo(fileName: name.relativePath(currentDir), numTests: ntests)
template createTotalVars(totalTestsPassed, totalTests) =
var var
totalTests = 0 totalTests = 0
totalTestsPassed = 0 totalTestsPassed = 0
for name in testsInfoMap.keys(): template printFinalSummary(totalTestsPassed, totalTests) =
let testsInfo = getTestsInfo(name)
testsInfo.printRunning()
testsInfo.runTests()
testsInfo.printPassedTests()
totalTests += testsInfo.tests.len()
totalTestsPassed += testsInfo.testsPassed
printSummary(totalTestsPassed, totalTests) printSummary(totalTestsPassed, totalTests)
when isMainModule: proc expandTests(currentDir: string): NimNode =
runTests()
result = newNimNode(nnkStmtList)
let
totalTestsPassed = genSym(nskVar, "totalPassed")
totalTests = genSym(nskVar, "totalTests")
result.add(getAst(createTotalVars(totalTestsPassed, totalTests)))
for name in testsModuleMap.keys():
let
testsModule = testsModuleMap[name]
infoSym = genSym(nskLet, "testsInfo")
allTests = newNimNode(nnkStmtList)
numTests = newLit(testsModule.tests.len())
result.add(getAst(createInfo(infoSym, name, numTests, currentDir)))
for test in testsModule.tests:
let
body = test.procDef
tname = genSym(nskParam, "tname")
body.insert(0,
nnkLetSection.newTree(
nnkIdentDefs.newTree(
ident("testName"),
newEmptyNode(),
tname
)
)
)
let
lineInfo = body.lineInfoObj
testCall = getAst(makeProc(body, tname, test.name, lineInfo, currentDir))
testRun = getAst(runTest(testCall, infoSym, test.name))
allTests.add(testRun)
result.add(getAst(createRunTests(allTests, infoSym, totalTests, totalTestsPassed)))
result.add(getAst(printFinalSummary(totalTestsPassed, totalTests)))
macro test*(name: string, body: untyped): untyped =
## The test macro. No runtime overhead is introduced using it and
## it can be run from anywhere, including in the proc being tested.
let
fileName = body.lineInfoObj.fileName
testsInfo = getTestsModule(fileName)
testsInfo.addTest(body, name)
macro runTests*(callsite: varargs[untyped]): untyped =
## This macro must be run in the main testing module that imports
## all of the other tests
let currentDir = getProjectPath()
return expandTests(currentDir)