From 7d44349f2b600215544ee48d5ce8e38e7b833e91 Mon Sep 17 00:00:00 2001 From: Joey Payne Date: Wed, 27 Feb 2019 19:08:23 -0700 Subject: [PATCH] Make test macro work in compile only mode --- src/tani.nim | 473 +++++++++++++++++++++++++++++++-------------------- 1 file changed, 292 insertions(+), 181 deletions(-) diff --git a/src/tani.nim b/src/tani.nim index 10b6ec9..08fed0d 100644 --- a/src/tani.nim +++ b/src/tani.nim @@ -1,24 +1,32 @@ -import macros, tables, strutils +import macros +import tables, strutils, os + +export tables, strutils, os + when defined(ECMAScript): const noColors = true else: const noColors = defined(noColors) import terminal + export terminal import private/utils type Test = ref object - procDef: proc(test: Test) - name: string + procDef: NimNode + name: NimNode - TestsInfo = ref object + TestsModule = ref object ## The base TestSuite fileName: string - currentTestName: string + tests: seq[Test] + + TestsInfo = ref object + fileName: string + numTests: int testsPassed: int lastTestFailed: bool - tests: seq[Test] TestAssertError = object of Exception ## check and other check* statements will raise @@ -31,7 +39,11 @@ type checkFuncName: 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 = ## helper proc to compare arrays @@ -43,33 +55,34 @@ proc `==`*[T](ar: openarray[T], ar2: openarray[T]): bool = return true template returnException(name, testName, snip, vals, pos, posRel) = - ## private template for raising an exception - var - filename = posRel.filename - line = pos.line - col = pos.column + ## private template for raising an exception + var + filename = posRel.filename + line = pos.line + col = pos.column - var message = "\l" - message &= " Condition: $2($1)\l".format(snip, name) + var message = "\l" + message &= " Condition: $2($1)\l".format(snip, name) + + if vals.len() > 0: message &= " Where:\l" - for k, v in vals.pairs: message &= " $1 -> $2\l".format(k, v) - message &= " Location: $1; line $2; col: $3".format(filename, line, col) + message &= " Location: $1; line $2; col: $3".format(filename, line, col) - var exc = newException(TestAssertError, message) - exc.fileName = filename - exc.lineNumber = line - exc.column = col - exc.codeSnip = snip - exc.testName = testName - exc.valTable = vals - exc.checkFuncName = name - raise exc + var exc = newException(TestAssertError, message) + exc.fileName = filename + exc.lineNumber = line + exc.column = col + exc.codeSnip = snip + exc.testName = testName + exc.valTable = vals + exc.checkFuncName = name + raise exc proc `$`(test: Test): string = - return "proc `"&test.name&"`()" + return "proc `" & $test.name.toStrLit & "`()" proc `$`*[T](ar: openarray[T]): string = ## Converts an array into a string @@ -139,36 +152,13 @@ macro toString*(obj: typed): untyped = $(obj) result = getAst(toStrAst(obj)) -proc getTestsInfo(name: string): TestsInfo = - if not testsInfoMap.hasKey(name): - testsInfoMap[name] = TestsInfo(fileName: name) - return testsInfoMap[name] +proc getTestsModule(name: string): TestsModule {.compileTime.} = + if not testsModuleMap.hasKey(name): + testsModuleMap[name] = TestsModule(fileName: name) + return testsModuleMap[name] -proc addTest*(testsInfo: TestsInfo, procDef: proc(test: Test), name: string) = - testsInfo.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)) +proc addTest(testsModule: TestsModule, procDef: NimNode, name: NimNode) = + testsModule.tests.add(Test(procDef: procDef, name: name)) template strRep(n: NimNode): untyped = toString(n) @@ -233,68 +223,52 @@ macro getSyms(code:untyped): untyped = template check*(code: untyped) = ## Assertions for tests if not code: - # These need to be here to capture the actual info let pos = instantiationInfo(fullpaths=true) posRel = instantiationInfo() - - var snip = "" - let testName = $self.name - - var vals = getSyms(code) - # get ast string with extra spaces ignored - snip = astToStr(code).strip().split({'\t', '\v', '\c', '\n', '\f'}).join("; ") + vals = getSyms(code) + # get ast string with extra spaces ignored + snip = astToStr(code).strip().split({'\t', '\v', '\c', '\n', '\f'}).join("; ") returnException("check", testName, snip, vals, pos, posRel) -template checkRaises*(error: untyped, - code: untyped): untyped = +template wrapCode(code): 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 ## not thrown in the code let pos = instantiationInfo(fullpaths=true) posRel = instantiationInfo() - when error isnot Exception: - 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) + try: + wrapCode(code) + let + codeStr = astToStr(code).strip().split().join(" ") + snip = "$1, $2".format(astToStr(error), codeStr) + vals = {codeStr: "No Exception Raised"}.toTable() + testName = testName + returnException("checkRaises", testName, snip, vals, pos, posRel) - except error: - discard - except TestAssertError: - raise - except Exception: - let - e = getCurrentException() - codeStr = astToStr(code).split().join(" ") - snip = "$1, $2".format(astToStr(error), codeStr) - vals = {codeStr: $e.name}.toTable() - testName = $self.name + except error: + discard + except TestAssertError: + raise + except: + let + e = getCurrentException() + codeStr = astToStr(code).strip().split().join(" ") + snip = "$1, $2".format(astToStr(error), codeStr) + vals = {codeStr: $e.name}.toTable() + testName = testName - 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 + returnException("checkRaises", testName, snip, vals, pos, posRel) -proc printRunning(info: TestsInfo) = +proc printRunning*(testsInfo: TestsInfo) = let termSize = getTermSize() var numTicks = termSize.width @@ -307,23 +281,27 @@ proc printRunning(info: TestsInfo) = when not noColors: styledEcho( styleBright, - fgYellow, "\l"&ticks, + fgYellow, "\l" & ticks, fgYellow, "\l\l[Running]", - fgWhite, " tests in $1 ".format(info.fileName) + fgWhite, " tests in $1 ".format(testsInfo.fileName) ) else: 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: # Output red if tests didn't pass, green otherwise var color = fgGreen - if info.testsPassed != info.tests.len(): + if info.testsPassed != info.numTests: color = fgRed - var passedStr = "[" & $info.testsPassed & "/" & $info.tests.len() & "]" + var passedStr = "[" & $info.testsPassed & "/" & $info.numTests & "]" when not defined(quiet): when not noColors: @@ -335,80 +313,117 @@ proc printPassedTests(info: TestsInfo) = echo "\l$1 tests passed for $2.".format(passedStr, info.fileName) -proc runTests(info: TestsInfo) = - when noColors: - stdout.write(info.fileName & " ") - else: - setForegroundColor(fgWhite) - writeStyled(info.fileName & " ", {styleBright}) - for t in info.tests: - try: - t.procDef(t) - when defined(quiet): - when noColors: - stdout.write(".") - else: - setForegroundColor(fgGreen) - writeStyled(".", {styleBright}) - setForegroundColor(fgWhite) +template runTest(procCall, info, testName) = + + try: + procCall + when defined(quiet): + when noColors: + stdout.write(".") else: - var okStr = "[OK]" - if info.lastTestFailed: - okStr = "\l" & okStr + setForegroundColor(fgGreen) + writeStyled(".", {styleBright}) + setForegroundColor(fgWhite) + else: + var okStr = "[OK]" + if info.lastTestFailed: + okStr = "\l" & okStr - when not noColors: - styledEcho(styleBright, fgGreen, okStr, - fgWhite, " ", t.name) - else: - echo "$1 $2".format(okStr, t.name) - - info.testsPassed += 1 - info.lastTestFailed = false - except TestAssertError: - let e = (ref TestAssertError)(getCurrentException()) - - when defined(quiet): - when noColors: - stdout.write("F") - else: - setForegroundColor(fgRed) - writeStyled("F", {styleBright}) - setForegroundColor(fgWhite) + when not noColors: + styledEcho(styleBright, fgGreen, okStr, + fgWhite, " ", testName) else: - when not noColors: - styledEcho(styleBright, - fgRed, "\l[Failed]", - fgWhite, " ", t.name) - else: - echo "\l[Failed] $1".format(t.name) + echo "$1 $2".format(okStr, t.name) - let - name = e.checkFuncName - snip = e.codeSnip - line = e.lineNumber - col = e.column - filename = e.fileName - vals = e.valTable + info.testsPassed += 1 + info.lastTestFailed = false + except PrivateTestError as e: + 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) - when not noColors: - styledEcho(styleDim, fgWhite, " Condition: $2($1)\l".format(snip, name), " Where:") + 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 noColors: + stdout.write("F") + else: + setForegroundColor(fgRed) + writeStyled("F", {styleBright}) + setForegroundColor(fgWhite) + else: + when not noColors: + styledEcho(styleBright, + fgRed, "\l[Failed]", + fgWhite, " ", testName) + else: + echo "\l[Failed] $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, " Condition: $2($1)".format(snip, name)) + + if vals.len > 0: + styledEcho(styleDim, fgWhite, " Where:") for k, v in vals.pairs: styledEcho(styleDim, fgCyan, " ", k, fgWhite, " -> ", fgGreen, v) - styledEcho( - styleDim, fgWhite, - " Location: $1; line $2; col $3".format(filename, line, col)) - else: - echo " Condition: $2($1)".format(snip, name) + styledEcho( + styleDim, fgWhite, + " Location: $1; line $2; col $3".format(filename, line, col)) + else: + echo " Condition: $2($1)".format(snip, name) + if vals.len > 0: echo " Where:" for k, v in vals.pairs: echo " ", k, " -> ", v - echo " Location: $1; line $2; col: $3".format(filename, line, col) - info.lastTestFailed = true + echo " Location: $1; line $2; col: $3".format(filename, line, col) + + info.lastTestFailed = true - echo "" proc printSummary(totalTestsPassed: int, totalTests: int) = when not noColors: @@ -450,23 +465,119 @@ proc printSummary(totalTestsPassed: int, totalTests: int) = echo "\l[Summary]" 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..