From d74374a7cb77411ab5c357960c3e86cb9913b198 Mon Sep 17 00:00:00 2001 From: Joey Payne Date: Sat, 23 Mar 2019 14:12:36 -0600 Subject: [PATCH] Add support for private tests and captured output --- src/private/captureoutput.nim | 58 +++++ src/tani.nim | 467 +++++++++++++++++----------------- tests/test.nim | 27 +- tests/test2.nim | 29 +++ 4 files changed, 338 insertions(+), 243 deletions(-) create mode 100644 src/private/captureoutput.nim create mode 100644 tests/test2.nim diff --git a/src/private/captureoutput.nim b/src/private/captureoutput.nim new file mode 100644 index 0000000..b11a1f9 --- /dev/null +++ b/src/private/captureoutput.nim @@ -0,0 +1,58 @@ +import os, oids, tables + +when defined(windows): + proc dup(fd: cint): cint {.importc: "_dup", header: "".} + proc dup2(fd1, fd2: cint): cint {.importc: "_dup2", header: "".} +else: + import posix + +proc getTempFile(): string = + return getTempDir() / $genOid() & ".tmp" + +template captureOutput*(code: untyped): (string, ref Exception) = + var backupFds = newTable[File, cint]() + + let + streams = [stdout, stderr] + fname = getTempFile() + tempFile = open(fname, fmReadWrite) + tempHandle = tempFile.getFileHandle() + + for stream in streams: + let handle = stream.getFileHandle() + + let save = dup(handle) + backupFds[stream] = save + + discard dup2(tempHandle, handle) + + var exc: ref Exception + var output = "" + + try: + code + + tempFile.setFilePos(0) + output = tempFile.readAll() + tempFile.close() + + (output, exc) + except Exception as e: + exc = e + + tempFile.setFilePos(0) + output = tempFile.readAll() + tempFile.close() + + (output, exc) + finally: + for stream in streams: + let handle = stream.getFileHandle() + let save = backupFds[stream] + discard dup2(save, handle) + + try: + os.removeFile(fname) + except: + discard + diff --git a/src/tani.nim b/src/tani.nim index ad3044b..c455d1a 100644 --- a/src/tani.nim +++ b/src/tani.nim @@ -1,9 +1,9 @@ import macros -import tables, strutils, os +import tables, strutils, os, typetraits -export tables, strutils, os +export tables, strutils, os, typetraits -when defined(ECMAScript): +when defined(js): const noColors = true else: const noColors = defined(noColors) @@ -11,6 +11,8 @@ else: export terminal import private/utils +when not defined(js): + import private/captureoutput type Test = ref object @@ -39,12 +41,12 @@ type checkFuncName: string valTable: Table[string, string] - PrivateTestError = object of TestAssertError - ## Raised when a test tries to access something - ## that is private to a module - var testsModuleMap {.compileTime.} = newTable[string, TestsModule]() +var + totalTestsPassed = 0 + totalTests = 0 + proc `==`*[T](ar: openarray[T], ar2: openarray[T]): bool = ## helper proc to compare arrays if len(ar) != len(ar2): @@ -54,7 +56,7 @@ proc `==`*[T](ar: openarray[T], ar2: openarray[T]): bool = return false return true -template returnException(name, testName, snip, vals, pos, posRel) = +template returnException(name, tname, snip, vals, pos, posRel) = ## private template for raising an exception var filename = posRel.filename @@ -76,7 +78,7 @@ template returnException(name, testName, snip, vals, pos, posRel) = exc.lineNumber = line exc.column = col exc.codeSnip = snip - exc.testName = testName + exc.testName = tname exc.valTable = vals exc.checkFuncName = name raise exc @@ -93,7 +95,7 @@ proc `$`*[T](ar: openarray[T]): string = result &= ", " & $ar[i] result &= "]" -proc typeToStr*[T](some:typedesc[T]): string = name(T) +proc typeToStr*[T](some: typedesc[T]): string = name(T) template tupleObjToStr(obj): string {.dirty.} = var res = typeToStr(type(obj)) @@ -127,6 +129,9 @@ proc `$`*(s: ref object): string = proc objToStr*[T: object](obj: var T): string = tupleObjToStr(obj) +proc objToStr*[T: object](obj: T): string = + tupleObjToStr(obj) + proc objToStr*[T: tuple](obj: T): string = result = "tuple " & tupleObjToStr(obj) @@ -141,7 +146,7 @@ macro toString*(obj: typed): untyped = case kind: of ntyTuple, ntyObject: template toStrAst(obj): string = - einheit.objToStr(obj) + tani.objToStr(obj) result = getAst(toStrAst(obj)) of ntyString: template toStrAst(obj): string = @@ -179,7 +184,7 @@ template recursive(node, action): untyped {.dirty.} = result.add helper(c) discard helper(node) -macro getSyms(code:untyped): untyped = +macro getSyms(code: untyped): untyped = ## This macro gets all symbols and values of an expression ## into a table ## @@ -220,62 +225,52 @@ macro getSyms(code:untyped): untyped = initTable[string, string]() result = getAst(emptyTable()) -template check*(code: untyped) = - ## Assertions for tests - if not code: - let - pos = instantiationInfo(fullpaths=true) - posRel = instantiationInfo() - 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 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() - - try: - wrapCode(code) +template genCheckRaises(tname) = + template checkRaises(error: typed, code: typed): untyped {.used.}= + ## Raises a TestAssertError when the exception "error" is + ## not thrown in the 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) + pos = instantiationInfo(fullpaths=true) + posRel = instantiationInfo() - 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 + try: + wrapCode(code) + let + codeStr = astToStr(code).strip().split().join(" ") + snip = "$1, $2".format(astToStr(error), codeStr) + vals = {codeStr: "No Exception Raised"}.toTable() + returnException("checkRaises", tname, snip, vals, pos, posRel) - returnException("checkRaises", testName, snip, vals, pos, posRel) + 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() + returnException("checkRaises", tname, snip, vals, pos, posRel) -proc printRunning*(testsInfo: TestsInfo) = +proc getTicks(): string {.used.}= let termSize = getTermSize() + var numTicks = termSize.width - ticks = "" for i in 0.. 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) + 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) + + +template printTestOutput(info, testName, output) = + when not defined(quiet): + if output.len > 0: + let ticks = getTicks() + when noColors: + echo "\l" & ticks + echo "\l[Captured output for test \"$1\"]\l".format(testName) + echo " " & output.strip().replace("\l", "\l ") + echo "\l$1\l".format(ticks) + else: + styledEcho(styleDim, + fgBlue, "\l$1\l".format(ticks), + fgBlue, "\l[Captured output for test \"$1\"]".format(testName)) + + styledEcho(styleDim, + fgWhite, "\l " & output.strip().replace("\l", "\l ")) + + styledEcho(styleDim, + fgBlue, "\l$1\l".format(ticks)) + template runTest(procCall, info, testName) = + var output = "" try: - procCall - when defined(quiet): - when noColors: - stdout.write(".") - else: - setForegroundColor(fgGreen) - writeStyled(".", {styleBright}) - setForegroundColor(fgWhite) - else: - var okStr = "[OK]" - if info.lastTestFailed: - okStr = "\l" & okStr + when not defined(js): + let (testOut, exc) = captureOutput(procCall) + output = testOut - when not noColors: - styledEcho(styleBright, fgGreen, okStr, - fgWhite, " ", testName) - else: - echo "$1 $2".format(okStr, testName) + if not exc.isNil: + raise exc + else: + procCall + + printTestProgress(info, testName) + printTestOutput(info, testName, output) 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(testName) - - 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(testName) - - 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) - 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) - + printTestFailed(info, testName, e) + printTestOutput(info, testName, output) info.lastTestFailed = true -proc printSummary(totalTestsPassed: int, totalTests: int) = +proc printSummary*() = when not noColors: var summaryColor = fgGreen @@ -443,14 +436,7 @@ proc printSummary(totalTestsPassed: int, totalTests: int) = echo "\l\l$1 tests passed.".format(passedStr) else: - let termSize = getTermSize() - - var - ticks = "" - numTicks = termSize.width - - for i in 0.. 0 + + test "foo bar": + echo os.joinPath("test", "output") + check y() > 0 + checkRaises(Exception): + raise newException(Exception, "Foo Exception") + + return 10 + +runTestsMain() diff --git a/tests/test2.nim b/tests/test2.nim new file mode 100644 index 0000000..4c58776 --- /dev/null +++ b/tests/test2.nim @@ -0,0 +1,29 @@ +import tani + +type + MyObj* = object + stuff*: int + +proc stuff2*(): int = + test "adfsadf": + check stuff2() == 200 + return 200 + + +proc testing(): MyObj = + test "my cool test": + check testing().stuff == 200 + + return MyObj(stuff: 200) + +proc stuff() = + test "my cool test": + var x = 20 + if x == 20: + echo "hoo" + stuff() + check x == 30 + check testing().stuff == 200 + echo "hello" + +runTests()