From 8ca9a17681fb6ba9f236c8204a7dc7e22ad308be Mon Sep 17 00:00:00 2001 From: Joey Payne Date: Wed, 20 Feb 2019 21:17:31 -0700 Subject: [PATCH] Initial testing framework --- .gitignore | 4 + src/private/utils.nim | 77 +++++++ src/tani.nim | 472 ++++++++++++++++++++++++++++++++++++++++++ tani.nimble | 17 ++ tests/nim.cfg | 1 + tests/test.nim | 6 + 6 files changed, 577 insertions(+) create mode 100644 src/private/utils.nim create mode 100644 src/tani.nim create mode 100644 tani.nimble create mode 100644 tests/nim.cfg create mode 100644 tests/test.nim diff --git a/.gitignore b/.gitignore index 67d9b34..e88636e 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,5 @@ nimcache/ +*.swp +*.swo +/tests/test +src/tani diff --git a/src/private/utils.nim b/src/private/utils.nim new file mode 100644 index 0000000..87e4065 --- /dev/null +++ b/src/private/utils.nim @@ -0,0 +1,77 @@ +when defined(windows): + import winlean + type + SHORT = int16 + COORD = object + X: SHORT + Y: SHORT + + SMALL_RECT = object + Left: SHORT + Top: SHORT + Right: SHORT + Bottom: SHORT + + CONSOLE_SCREEN_BUFFER_INFO = object + dwSize: COORD + dwCursorPosition: COORD + wAttributes: int16 + srWindow: SMALL_RECT + dwMaximumWindowSize: COORD + + proc getConsoleScreenBufferInfo(hConsoleOutput: HANDLE, + lpConsoleScreenBufferInfo: ptr CONSOLE_SCREEN_BUFFER_INFO): WINBOOL{.stdcall, + dynlib: "kernel32", importc: "GetConsoleScreenBufferInfo".} + + proc getTermSize*(): tuple[width, height: int] = + let handle = getStdHandle(STD_OUTPUT_HANDLE) + var scrbuf: CONSOLE_SCREEN_BUFFER_INFO + let ret = getConsoleScreenBufferInfo(handle, addr(scrbuf)) + + if ret == -1: + return (width: -1, height: -1) + + let cols = scrbuf.srWindow.Right - scrbuf.srWindow.Left + 1 + let rows = scrbuf.srWindow.Bottom - scrbuf.srWindow.Top + 1 + + return (width: cols.int, height: rows.int) + +elif defined(ECMAScript) and defined(nodejs): + type + StreamObj {.importc.} = object + columns: int + rows: int + + ProcessObj {.importc.} = object + stdout: ref StreamObj + + var + process {.importc, nodecl.}: ref ProcessObj + + proc getTermSize*(): (int, int) = + let t = process.stdout + return (t.rows, t.columns) + +else: + import posix + type + winsize = object + ws_row: cushort + ws_col: cushort + ws_xpixel: cushort + ws_ypixel: cushort + + var + TIOCGWINSZ{.importc: "TIOCGWINSZ", header: "".}: uint + + proc ioctl*(f: int, device: uint, w: var winsize): int {.importc: "ioctl", + header: "", varargs, tags: [WriteIOEffect].} + + proc getTermSize*(): tuple[width, height: int] = + var w: winsize + let ret = ioctl(STDOUT_FILENO, TIOCGWINSZ, addr(w)) + + if ret == -1: + return (width: -1, height: -1) + + return (width: w.ws_col.int, height: w.ws_row.int) diff --git a/src/tani.nim b/src/tani.nim new file mode 100644 index 0000000..10b6ec9 --- /dev/null +++ b/src/tani.nim @@ -0,0 +1,472 @@ +import macros, tables, strutils +when defined(ECMAScript): + const noColors = true +else: + const noColors = defined(noColors) + import terminal + +import private/utils + +type + Test = ref object + procDef: proc(test: Test) + name: string + + TestsInfo = ref object + ## The base TestSuite + fileName: string + currentTestName: string + testsPassed: int + lastTestFailed: bool + tests: seq[Test] + + TestAssertError = object of Exception + ## check and other check* statements will raise + ## this exception when the condition fails + lineNumber: int + column: int + fileName: string + codeSnip: string + testName: string + checkFuncName: string + valTable: Table[string, string] + +var testsInfoMap = newTable[string, TestsInfo]() + +proc `==`*[T](ar: openarray[T], ar2: openarray[T]): bool = + ## helper proc to compare arrays + if len(ar) != len(ar2): + return false + for i in countup(0, ar.len()): + if ar[i] != ar2[i]: + return false + 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 + + var message = "\l" + message &= " Condition: $2($1)\l".format(snip, name) + 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) + + 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&"`()" + +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 &= "]" + +proc typeToStr*[T](some:typedesc[T]): string = name(T) + +template tupleObjToStr(obj): string {.dirty.} = + var res = typeToStr(type(obj)) + template helper(n) {.gensym.} = + res.add("(") + var firstElement = true + for name, value in n.fieldPairs(): + when compiles(value): + if not firstElement: + res.add(", ") + res.add(name) + res.add(": ") + when (value is object or value is tuple): + when (value is tuple): + res.add("tuple " & typeToStr(type(value))) + else: + res.add(typeToStr(type(value))) + helper(value) + elif (value is string): + res.add("\"" & $value & "\"") + else: + res.add($value) + firstElement = false + res.add(")") + helper(obj) + res + +proc `$`*(s: ref object): string = + result = "ref " & tupleObjToStr(s[]).replace(":ObjectType", "") + +proc objToStr*[T: object](obj: var T): string = + tupleObjToStr(obj) + +proc objToStr*[T: tuple](obj: T): string = + result = "tuple " & tupleObjToStr(obj) + +macro toString*(obj: typed): untyped = + ## this macro is to work around not being + ## able to override system.`$` + ## + ## Basically, I want to use my proc to print + ## objects and tuples, but the regular $ for + ## everything else + let kind = obj.getType().typeKind + case kind: + of ntyTuple, ntyObject: + template toStrAst(obj): string = + einheit.objToStr(obj) + result = getAst(toStrAst(obj)) + of ntyString: + template toStrAst(obj): string = + "\"" & $(obj) & "\"" + result = getAst(toStrAst(obj)) + else: + template toStrAst(obj): string = + $(obj) + result = getAst(toStrAst(obj)) + +proc getTestsInfo(name: string): TestsInfo = + if not testsInfoMap.hasKey(name): + testsInfoMap[name] = TestsInfo(fileName: name) + return testsInfoMap[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)) + +template strRep(n: NimNode): untyped = + toString(n) + +template tableEntry(n: NimNode): untyped = + newNimNode(nnkExprColonExpr).add(n.toStrLit(), getAst(strRep(n))) + +template recursive(node, action): untyped {.dirty.} = + ## recursively iterate over AST nodes and perform an + ## action on them + proc helper(child: NimNode): NimNode {.gensym.} = + action + result = child.copy() + for c in child.children: + if child.kind == nnkCall and c.kind == nnkDotExpr: + # ignore dot expressions that are also calls + continue + result.add helper(c) + discard helper(node) + +macro getSyms(code:untyped): untyped = + ## This macro gets all symbols and values of an expression + ## into a table + ## + ## Table[string, string] -> symbolName, value + ## + var + tableCall = newNimNode(nnkCall).add(ident("toTable")) + tableConstr = newNimNode(nnkTableConstr) + + recursive(code): + let ch1 = child + case ch1.kind: + of nnkInfix: + if child[1].kind == nnkIdent: + tableConstr.add(tableEntry(child[1])) + if child[2].kind == nnkIdent: + tableConstr.add(tableEntry(child[2])) + of nnkExprColonExpr: + if child[0].kind == nnkIdent: + tableConstr.add(tableEntry(child[0])) + if child[1].kind == nnkIdent: + tableConstr.add(tableEntry(child[1])) + of nnkCall, nnkCommand: + tableConstr.add(tableEntry(ch1)) + if ch1.len() > 0 and ch1[0].kind == nnkDotExpr: + tableConstr.add(tableEntry(ch1[0][0])) + for i in 1 ..< ch1.len(): + tableConstr.add(tableEntry(ch1[i])) + of nnkDotExpr: + tableConstr.add(tableEntry(ch1)) + else: + discard + if tableConstr.len() != 0: + tableCall.add(tableConstr) + result = tableCall + else: + template emptyTable() = + initTable[string, string]() + result = getAst(emptyTable()) + +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("; ") + + returnException("check", testName, snip, vals, pos, posRel) + +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) + + 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 + + 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) = + let termSize = getTermSize() + var + numTicks = termSize.width + ticks = "" + + for i in 0.. ", + fgGreen, v) + styledEcho( + styleDim, fgWhite, + " Location: $1; line $2; col $3".format(filename, line, col)) + else: + echo " Condition: $2($1)".format(snip, name) + 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 "" + +proc printSummary(totalTestsPassed: int, totalTests: int) = + when not noColors: + var summaryColor = fgGreen + + if totalTestsPassed != totalTests: + summaryColor = fgRed + + var passedStr = "[" & $totalTestsPassed & "/" & $totalTests & "]" + + when defined(quiet): + when not noColors: + styledEcho(styleBright, summaryColor, + "\l\l", passedStr, + fgWhite, " tests passed.") + else: + echo "\l\l$1 tests passed.".format(passedStr) + else: + + let termSize = getTermSize() + + var + ticks = "" + numTicks = termSize.width + + for i in 0..= 0.19.4" + +task test, "Run tests": + exec "nim c -r tests/test.nim" + +task testjs, "Run tests on Node.js": + exec "nim js -d:nodejs -r tests/test.nim" diff --git a/tests/nim.cfg b/tests/nim.cfg new file mode 100644 index 0000000..85bf6c4 --- /dev/null +++ b/tests/nim.cfg @@ -0,0 +1 @@ +--path:"../src/" diff --git a/tests/test.nim b/tests/test.nim new file mode 100644 index 0000000..6f08d99 --- /dev/null +++ b/tests/test.nim @@ -0,0 +1,6 @@ +import tani, tables, strutils + +test "test 2": + let foo = 100 + check(foo < 4) +runTests()