├── .gitignore ├── LICENSE ├── README.md ├── einheit.nimble ├── src ├── einheit.nim └── einheit │ └── utils.nim └── tests ├── nim.cfg └── test.nim /.gitignore: -------------------------------------------------------------------------------- 1 | nimcache/ 2 | *.swp 3 | *.swo 4 | /tests/test 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Joey 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # einheit 2 | 3 | Einheit means unit in German. 4 | 5 | Einheit is a Nim unit testing library inspired by Python's unit tests. Nim's unittest library is good, but I wanted something a little more "Nim" feeling. I also really like Python's unittest module and thought it would be nice to have something similar in Nim. Also, unittest doesn't have much documentation on how to use it, and it's pretty bare bones, so I wanted a little more functionality and documentation. 6 | 7 | The benefit of the macro style I chose means that you can document your tests nicely as well :) 8 | 9 | ### Description 10 | testSuite is a compile-time macro that allows a user to easily define tests and run them. 11 | 12 | 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. 13 | 14 | If you don't want inheritance, you can just use procs. 15 | 16 | Two special methods/procs are called setup() and tearDown(). The macro will inject these methods/procs if they don't exist and they will be called before and after running the test suite, respectively. 17 | 18 | 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. 19 | 20 | For each suite method/proc, an implicit variable called "self" is added. This lets you access the testSuite in an OO kind of way. 21 | 22 | On failure, the macro gathers names and values of *all* arguments and functions and prints them out. It's really useful for debugging! 23 | 24 | ### Installation 25 | 26 | Install with nimble! 27 | 28 | ```bash 29 | nimble install einheit 30 | ``` 31 | 32 | ### Running 33 | 34 | ```bash 35 | nim c -r testing_script.nim 36 | 37 | # Or no colors 38 | nim c -r -d:noColors testing_script.nim 39 | 40 | # No colors, quiet output 41 | nim c -r -d:quiet -d:noColors testing_script.nim 42 | 43 | # With Node.js as a target 44 | nim js -d:nodejs -r testing_script.nim 45 | 46 | ``` 47 | 48 | ### Usage 49 | 50 | ```nim 51 | import einheit 52 | 53 | testSuite SuiteName of TestSuite: 54 | 55 | var 56 | suiteVar: string 57 | testObj: int 58 | 59 | method setup()= 60 | ## do setup code here 61 | self.suiteVar = "Testing" 62 | self.testObj = 90 63 | 64 | method tearDown()= 65 | ## do tear down code here 66 | self.suiteVar = nil 67 | self.testObj = 0 68 | 69 | method testAddingString()= 70 | ## adds a string to the suiteVar 71 | self.suiteVar &= " 123" 72 | self.check(self.suiteVar == "Testing 123") 73 | 74 | proc raisesOs()= 75 | # This proc won't be invoked as a test, it must begin with "test" in lowercase 76 | raise newException(OSError, "Oh no! OS malfunction!") 77 | 78 | method testRaises()= 79 | # Two ways of checking 80 | self.checkRaises OSError: 81 | self.raisesOs() 82 | 83 | self.checkRaises(OSError, self.raisesOs()) 84 | 85 | method testTestObj()= 86 | self.check(self.testObj == 90) 87 | 88 | method testMoreMore()= 89 | self.check("String" == "String") 90 | 91 | when isMainModule: 92 | einheit.runTests() 93 | ``` 94 | 95 | You can also find examples in the [test.nim](test.nim) file, including inheritance. 96 | 97 | 98 | Output of running 99 | 100 | ```bash 101 | nim c -r test.nim 102 | ``` 103 | 104 | is this: 105 | 106 | ``` 107 | [Running] UnitTests ----------------------------------------------------------- 108 | 109 | [OK] testForB 110 | [Failed] testArrayAssert 111 | Condition: check(self.testArray == [0, 1, 2]) 112 | Where: 113 | self.testArray -> [0, 1, 2, 3] 114 | Location: test.nim; line 27 115 | 116 | [Failed] testForC 117 | Condition: check(c == 1) 118 | Where: 119 | c -> 0 120 | Location: test.nim; line 32 121 | 122 | 123 | [1/3] tests passed for UnitTests. ---------------------------------------------- 124 | 125 | 126 | [Running] UnitTestsNew -------------------------------------------------------- 127 | 128 | [OK] testTestObj 129 | [OK] testStuff 130 | [Failed] testMore 131 | Condition: check(more == 1) 132 | Where: 133 | more -> 23 134 | Location: test.nim; line 56 135 | 136 | [Failed] testMoreMore 137 | Condition: check(self.returnTrue()) 138 | Where: 139 | self.returnTrue() -> false 140 | self -> ref UnitTestsNew(testObj: 90, name: UnitTestsNew, currentTestName: testMoreMore, testsPassed: 2, numTests: 4) 141 | Location: test.nim; line 59 142 | 143 | 144 | [2/4] tests passed for UnitTestsNew. ------------------------------------------- 145 | 146 | 147 | [Running] TestInherit --------------------------------------------------------- 148 | 149 | [OK] testTestObj 150 | [OK] testStuff 151 | [Failed] testMore 152 | Condition: check(more == 1) 153 | Where: 154 | more -> 23 155 | Location: test.nim; line 56 156 | 157 | [Failed] testMoreMore 158 | Condition: check(self.returnTrue()) 159 | Where: 160 | self.returnTrue() -> false 161 | self -> ref UnitTestsNew(testObj: 90, name: TestInherit, currentTestName: testMoreMore, testsPassed: 2, numTests: 4) 162 | Location: test.nim; line 59 163 | 164 | [Failed] testRaises 165 | Condition: checkRaises(OSError, self.raisesOs()) 166 | Where: 167 | self.raisesOs() -> SystemError 168 | Location: test.nim; line 72 169 | 170 | 171 | [2/5] tests passed for TestInherit. -------------------------------------------- 172 | 173 | 174 | [Running] MoreInheritance ----------------------------------------------------- 175 | 176 | [Failed] testTestObj 177 | Condition: check(self.testObj == 90) 178 | Where: 179 | self.testObj -> 12345 180 | Location: test.nim; line 46 181 | 182 | [OK] testStuff 183 | [Failed] testMore 184 | Condition: check(more == 1) 185 | Where: 186 | more -> 23 187 | Location: test.nim; line 56 188 | 189 | [Failed] testMoreMore 190 | Condition: check(self.returnTrue()) 191 | Where: 192 | self.returnTrue() -> false 193 | self -> ref UnitTestsNew(testObj: 12345, name: MoreInheritance, currentTestName: testMoreMore, testsPassed: 1, numTests: 4) 194 | Location: test.nim; line 59 195 | 196 | [Failed] testRaises 197 | Condition: checkRaises(OSError, self.raisesOs()) 198 | Where: 199 | self.raisesOs() -> SystemError 200 | Location: test.nim; line 72 201 | 202 | [OK] testTestObj 203 | [OK] testNewObj 204 | [Failed] testRefObject 205 | Condition: check(d == k) 206 | Where: 207 | k -> ref TestObj(t: 30) 208 | d -> ref TestObj(t: 3) 209 | Location: test.nim; line 123 210 | 211 | [Failed] testObject 212 | Condition: check(d == k) 213 | Where: 214 | k -> TestObj(t: 30) 215 | d -> TestObj(t: 3) 216 | Location: test.nim; line 138 217 | 218 | [Failed] testComplexObject 219 | Condition: check(x.isObj(p)) 220 | Where: 221 | x.isObj(p) -> false 222 | p -> 4 223 | x -> Obj2(d: Obj1(e: Hey)) 224 | Location: test.nim; line 150 225 | 226 | [Failed] testTuple 227 | Condition: check(t == r) 228 | Where: 229 | r -> tuple Person(name: P, age: 3) 230 | t -> tuple Person(name: Peter, age: 30) 231 | Location: test.nim; line 161 232 | 233 | [Failed] testComplex 234 | Condition: check(self.doStuff(a, s) == "5stuff" and self.doStuff(a, self.doStuff(a, self.doStuff(y, s))) == "something?") 235 | Where: 236 | self.doStuff(a, s) -> 5stuff 237 | a -> 5 238 | self -> ref MoreInheritance(testObj: 12345, name: MoreInheritance, currentTestName: testComplex, testsPassed: 3, numTests: 12) 239 | self.doStuff(a, self.doStuff(a, self.doStuff(y, s))) -> 5545stuff 240 | y -> 45 241 | s -> stuff 242 | self.doStuff(y, s) -> 45stuff 243 | self.doStuff(a, self.doStuff(y, s)) -> 545stuff 244 | Location: test.nim; line 169 245 | 246 | 247 | [3/12] tests passed for MoreInheritance. --------------------------------------- 248 | 249 | 250 | [Summary] 251 | 252 | [8/24] tests passed. 253 | ``` 254 | 255 | Notice that on failure, the test runner gives some useful information about the test in question. This is useful for determining why the test failed. 256 | -------------------------------------------------------------------------------- /einheit.nimble: -------------------------------------------------------------------------------- 1 | # Package 2 | 3 | version = "0.2.0" 4 | author = "Joey Payne" 5 | description = "Tool for providing unit tests. Einheit is German for Unit." 6 | license = "MIT" 7 | 8 | srcDir = "src" 9 | 10 | # Deps 11 | requires "nim >= 0.18.0" 12 | 13 | task test, "Run tests": 14 | exec "nim c -r tests/test.nim" 15 | 16 | task testjs, "Run tests on Node.js": 17 | exec "nim js -d:nodejs -r tests/test.nim" 18 | -------------------------------------------------------------------------------- /src/einheit.nim: -------------------------------------------------------------------------------- 1 | ## :Author: Joey Payne 2 | ## This module is an alternate implementation of 3 | ## the unittest module in Nim. Inspired by the python 4 | ## unit test module. 5 | ## 6 | ## Example: 7 | ## 8 | ## .. code:: nim 9 | ## 10 | ## testSuite UnitTests: 11 | ## proc thisIsATest() = 12 | ## self.check(1 == 1) 13 | ## self.checkRaises(OSError, newException(OSError, "OS is exploding!")) 14 | ## 15 | import macros 16 | import unicode except split 17 | import strutils except toLower 18 | import tables 19 | import typetraits 20 | when defined(ECMAScript): 21 | const noColors = true 22 | else: 23 | const noColors = defined(noColors) 24 | import terminal 25 | import einheit/utils 26 | 27 | # ----------------- Helper Procs and Macros ----------------------------------- 28 | 29 | proc `$`*[T](ar: openarray[T]): string = 30 | ## Converts an array into a string 31 | result = "[" 32 | if ar.len() > 0: 33 | result &= $ar[0] 34 | for i in 1..ar.len()-1: 35 | result &= ", " & $ar[i] 36 | result &= "]" 37 | 38 | proc typeToStr*[T](some:typedesc[T]): string = name(T) 39 | 40 | template tupleObjToStr(obj): string {.dirty.} = 41 | var res = typeToStr(type(obj)) 42 | template helper(n) {.gensym.} = 43 | res.add("(") 44 | var firstElement = true 45 | for name, value in n.fieldPairs(): 46 | when compiles(value): 47 | if not firstElement: 48 | res.add(", ") 49 | res.add(name) 50 | res.add(": ") 51 | when (value is object or value is tuple): 52 | when (value is tuple): 53 | res.add("tuple " & typeToStr(type(value))) 54 | else: 55 | res.add(typeToStr(type(value))) 56 | helper(value) 57 | elif (value is string): 58 | res.add("\"" & $value & "\"") 59 | else: 60 | res.add($value) 61 | firstElement = false 62 | res.add(")") 63 | helper(obj) 64 | res 65 | 66 | proc `$`*(s: ref object): string = 67 | result = "ref " & tupleObjToStr(s[]).replace(":ObjectType", "") 68 | 69 | proc objToStr*[T: object](obj: var T): string = 70 | tupleObjToStr(obj) 71 | 72 | proc objToStr*[T: tuple](obj: T): string = 73 | result = "tuple " & tupleObjToStr(obj) 74 | 75 | macro toString*(obj: typed): untyped = 76 | ## this macro is to work around not being 77 | ## able to override system.`$` 78 | ## 79 | ## Basically, I want to use my proc to print 80 | ## objects and tuples, but the regular $ for 81 | ## everything else 82 | let kind = obj.getType().typeKind 83 | case kind: 84 | of ntyTuple, ntyObject: 85 | template toStrAst(obj): string = 86 | einheit.objToStr(obj) 87 | result = getAst(toStrAst(obj)) 88 | of ntyString: 89 | template toStrAst(obj): string = 90 | "\"" & $(obj) & "\"" 91 | result = getAst(toStrAst(obj)) 92 | else: 93 | template toStrAst(obj): string = 94 | $(obj) 95 | result = getAst(toStrAst(obj)) 96 | 97 | # ----------------------- Test Suite Types ------------------------------------ 98 | type 99 | TestSuite* = ref object of RootObj 100 | ## The base TestSuite 101 | name: string 102 | currentTestName: string 103 | testsPassed: int 104 | numTests: int 105 | lastTestFailed: bool 106 | 107 | TestAssertError = object of Exception 108 | ## check and other check* statements will raise 109 | ## this exception when the condition fails 110 | lineNumber: int 111 | fileName: string 112 | codeSnip: string 113 | testName: string 114 | checkFuncName: string 115 | valTable: Table[string, string] 116 | 117 | 118 | # -- Methods for the TestSuite base -- 119 | 120 | method setup*(suite: TestSuite) {.base.} = 121 | ## Base method for setup code 122 | discard 123 | 124 | method tearDown*(suite: TestSuite) {.base.} = 125 | ## Base method for tearDown code 126 | discard 127 | 128 | method runTests*(suite: TestSuite) {.base.} = 129 | ## Base method for running tests 130 | discard 131 | 132 | # ------------------------------------ 133 | 134 | template returnException(name, tName, snip, vals, pos, posRel) = 135 | ## private template for raising an exception 136 | var 137 | filename = posRel.filename 138 | line = pos.line 139 | var message = "\l" 140 | message &= " Condition: $2($1)\l".format(snip, name) 141 | message &= " Where:\l" 142 | for k, v in vals.pairs: 143 | message &= " $1 -> $2\l".format(k, v) 144 | 145 | message &= " Location: $1; line $2".format(filename, line) 146 | 147 | var exc = newException(TestAssertError, message) 148 | exc.fileName = filename 149 | exc.lineNumber = line 150 | exc.codeSnip = snip 151 | exc.testName = tName 152 | exc.valTable = vals 153 | exc.checkFuncName = name 154 | raise exc 155 | 156 | # ------------------------ Templates for checking ---------------------------- 157 | 158 | template checkRaises*(self: untyped, error: untyped, 159 | code: untyped): untyped = 160 | ## Raises a TestAssertError when the exception "error" is 161 | ## not thrown in the code 162 | let 163 | pos = instantiationInfo(fullpaths=true) 164 | posRel = instantiationInfo() 165 | 166 | try: 167 | code 168 | let 169 | codeStr = astToStr(code).split().join(" ") 170 | snip = "$1, $2".format(astToStr(error), codeStr) 171 | vals = [(codeStr, "No Exception Raised")].toTable() 172 | testName = self.currentTestName 173 | returnException("checkRaises", testName, snip, vals, pos, posRel) 174 | 175 | except error: 176 | discard 177 | except TestAssertError: 178 | raise 179 | except Exception: 180 | let 181 | e = getCurrentException() 182 | codeStr = astToStr(code).split().join(" ") 183 | snip = "$1, $2".format(astToStr(error), codeStr) 184 | vals = [(codeStr, $e.name)].toTable() 185 | testName = self.currentTestName 186 | 187 | returnException("checkRaises", testName, snip, vals, pos, posRel) 188 | 189 | template recursive(node, action): untyped {.dirty.} = 190 | ## recursively iterate over AST nodes and perform an 191 | ## action on them 192 | proc helper(child: NimNode): NimNode {.gensym.} = 193 | action 194 | result = child.copy() 195 | for c in child.children: 196 | if child.kind == nnkCall and c.kind == nnkDotExpr: 197 | # ignore dot expressions that are also calls 198 | continue 199 | result.add helper(c) 200 | discard helper(node) 201 | 202 | proc getNode(nodeKind: NimNodeKind, node: NimNode): NimNode = 203 | ## Gets the first node with nodeKind 204 | var stack: seq[NimNode] = @[node] 205 | 206 | while stack.len() > 0: 207 | let newNode = stack.pop() 208 | for i in 0 ..< newNode.len(): 209 | let child = newNode[i] 210 | if child.kind == nodeKind: 211 | return child 212 | else: 213 | stack.add(child) 214 | 215 | return newEmptyNode() 216 | 217 | template strRep(n: NimNode): untyped = 218 | toString(n) 219 | 220 | template tableEntry(n: NimNode): untyped = 221 | newNimNode(nnkExprColonExpr).add(n.toStrLit(), getAst(strRep(n))) 222 | 223 | macro getSyms(code:untyped): untyped = 224 | ## This macro gets all symbols and values of an expression 225 | ## into a table 226 | ## 227 | ## Table[string, string] -> symbolName, value 228 | ## 229 | var 230 | tableCall = newNimNode(nnkCall).add(ident("toTable")) 231 | tableConstr = newNimNode(nnkTableConstr) 232 | 233 | recursive(code): 234 | let ch1 = child 235 | case ch1.kind: 236 | of nnkInfix: 237 | if child[1].kind == nnkIdent: 238 | tableConstr.add(tableEntry(child[1])) 239 | if child[2].kind == nnkIdent: 240 | tableConstr.add(tableEntry(child[2])) 241 | of nnkExprColonExpr: 242 | if child[0].kind == nnkIdent: 243 | tableConstr.add(tableEntry(child[0])) 244 | if child[1].kind == nnkIdent: 245 | tableConstr.add(tableEntry(child[1])) 246 | of nnkCall, nnkCommand: 247 | tableConstr.add(tableEntry(ch1)) 248 | if ch1.len() > 0 and ch1[0].kind == nnkDotExpr: 249 | tableConstr.add(tableEntry(ch1[0][0])) 250 | for i in 1 ..< ch1.len(): 251 | tableConstr.add(tableEntry(ch1[i])) 252 | of nnkDotExpr: 253 | tableConstr.add(tableEntry(ch1)) 254 | else: 255 | discard 256 | if tableConstr.len() != 0: 257 | tableCall.add(tableConstr) 258 | result = tableCall 259 | else: 260 | template emptyTable() = 261 | initTable[string, string]() 262 | result = getAst(emptyTable()) 263 | 264 | template check*(self: untyped, code: untyped)= 265 | ## Assertions for tests 266 | if not code: 267 | # These need to be here to capture the actual info 268 | let 269 | pos = instantiationInfo(fullpaths=true) 270 | posRel = instantiationInfo() 271 | 272 | var 273 | snip = "" 274 | testName = self.currentTestName 275 | 276 | var vals = getSyms(code) 277 | # get ast string with extra spaces ignored 278 | snip = astToStr(code).split().join(" ") 279 | 280 | returnException("check", testName, snip, vals, pos, posRel) 281 | 282 | # ----------------------------------------------------------------------------- 283 | 284 | 285 | # A list to hold all test suites that are created 286 | var testSuites: seq[TestSuite] = @[] 287 | 288 | macro testSuite*(head: untyped, body: untyped): untyped = 289 | ## Compile-time macro that allows a user to define tests and run them 290 | ## 291 | ## Methods are used for inheritance, so if you want to derive a test 292 | ## suite, then you have to make sure the base suite uses methods 293 | ## for the tests that you want to derive. 294 | ## 295 | ## If you don't want inheritance, you can just use procs. 296 | ## 297 | ## A special proc/method is called setup(). The macro will inject 298 | ## this if it doesn't exist and it will be called before running 299 | ## the test suite. 300 | ## 301 | ## Test methods/procs to be run are prefixed with "test" in the 302 | ## method/proc name. This is so that you can write tests that call 303 | ## procs that do other things and won't be run as a test. 304 | ## 305 | ## For each suite method/proc, an implicit variable called "self" 306 | ## is added. This lets you access the testSuite in an OO kind 307 | ## of way. 308 | ## 309 | ## Usage: 310 | ## 311 | ## .. code:: nim 312 | ## 313 | ## testSuite SuiteName of TestSuite: 314 | ## 315 | ## var 316 | ## suiteVar: string 317 | ## 318 | ## method setup() = 319 | ## ## do setup code here 320 | ## self.suiteVar = "Testing" 321 | ## 322 | ## method testAddingString() = 323 | ## ## adds a string to the suiteVar 324 | ## self.suiteVar &= " 123" 325 | ## self.check(self.suiteVar == "Testing 123") 326 | ## 327 | ## when isMainModule: 328 | ## einheit.runTests() 329 | ## 330 | 331 | 332 | # object reference name inside methods. 333 | # ie: self, self 334 | let objReference = "self" 335 | var exportClass: bool = false 336 | 337 | template importRequiredLibs() = 338 | import strutils 339 | import tables 340 | import typetraits 341 | when not defined(ECMAScript): 342 | import terminal 343 | 344 | var typeName, baseName: NimNode 345 | 346 | if head.kind == nnkIdent: 347 | # `head` is expression `typeName` 348 | # echo head.treeRepr 349 | # -------------------- 350 | # Ident !"UnitTests" 351 | typeName = head 352 | 353 | elif head.kind == nnkInfix and $head[0] == "of": 354 | # `head` is expression `typeName of baseClass` 355 | # echo head.treeRepr 356 | # -------------------- 357 | # Infix 358 | # Ident !"of" 359 | # Ident !"UnitTests" 360 | # Ident !"RootObj" 361 | typeName = head[1] 362 | baseName = head[2] 363 | 364 | elif head.kind == nnkInfix and $head[0] == "*" and $head[1] == "of": 365 | # echo head.treeRepr 366 | # ----------- 367 | # Infix 368 | # Ident !"*" 369 | # Ident !"UnitTests 370 | # Prefix 371 | # Ident !"of" 372 | # Ident !"RootObj" 373 | exportClass = true 374 | typeName = head[1] 375 | baseName = head[2][1] 376 | elif head.kind == nnkInfix and $head[0] == "*": 377 | exportClass = true 378 | typeName = head[1] 379 | else: 380 | quit "Invalid node: " & head.lispRepr 381 | 382 | 383 | # echo treeRepr(body) 384 | # -------------------- 385 | # StmtList 386 | # VarSection 387 | # IdentDefs 388 | # Ident !"name" 389 | # Ident !"string" 390 | # Empty 391 | # IdentDefs 392 | # Ident !"age" 393 | # Ident !"int" 394 | # Empty 395 | # MethodDef 396 | # Ident !"vocalize" 397 | # Empty 398 | # Empty 399 | # FormalParams 400 | # Ident !"string" 401 | # Empty 402 | # Empty 403 | # StmtList 404 | # StrLit ... 405 | # MethodDef 406 | # Ident !"ageHumanYrs" 407 | # Empty 408 | # Empty 409 | # FormalParams 410 | # Ident !"int" 411 | # Empty 412 | # Empty 413 | # StmtList 414 | # DotExpr 415 | # Ident !"self" 416 | # Ident !"age" 417 | 418 | # create a new stmtList for the result 419 | result = newStmtList() 420 | 421 | # var declarations will be turned into object fields 422 | var recList = newNimNode(nnkRecList) 423 | 424 | # add a super function to simulate OOP 425 | # inheritance tree (Doesn't do what is expected because of dynamic binding) 426 | #if not isNil(`baseName`): 427 | # var super = quote do: 428 | # proc super(self: `typeName`): `baseName`= 429 | # return `baseName`(self) 430 | # result.add(super) 431 | 432 | template setNodeName(n2, procName, typeName) = 433 | if n2.name.kind == nnkIdent: 434 | procName = $(n2.name.toStrLit()) 435 | n2.name = ident(procName & typeName) 436 | elif n2.name.kind == nnkPostFix: 437 | if n2.name[1].kind == nnkIdent: 438 | procName = $(n2.name[1].toStrLit()) 439 | n2.name[1] = ident(procName & typeName) 440 | elif n2.name[1].kind == nnkAccQuoted: 441 | procName = $(n2.name[1][0].toStrLit()) 442 | n2.name[1][0] = ident(procName & typeName) 443 | elif n2.name.kind == nnkAccQuoted: 444 | procName = $(n2.name[0].toStrLit()) 445 | n2.name[0] = ident(procName & typeName) 446 | result.add(n2) 447 | 448 | 449 | template runTestsProc(self, typeName, baseMethod, typeMethod) = 450 | method typeMethod(self: typeName) {.base.} = 451 | when compiles(self.baseMethod()): 452 | self.baseMethod() 453 | 454 | method runTests(self: typeName) = 455 | self.typeMethod() 456 | 457 | var baseMethodName = ident("runTests" & $baseName.toStrLit()) 458 | var typeMethodName = ident("runTests" & $typeName.toStrLit()) 459 | 460 | var runTests = getAst( 461 | runTestsProc( 462 | ident(objReference), typeName, 463 | baseMethodName, typeMethodName 464 | ) 465 | ) 466 | 467 | var 468 | foundSetup = false 469 | foundTeardown = false 470 | 471 | # {.push warning[UseBase]: off.} 472 | result.add( 473 | newNimNode(nnkPragma).add( 474 | ident("push"), 475 | newNimNode(nnkExprColonExpr).add( 476 | newNimNode(nnkBracketExpr).add( 477 | ident("warning"), 478 | ident("UseBase") 479 | ), 480 | ident("off") 481 | ) 482 | ) 483 | ) 484 | 485 | # Make forward declarations so that function order 486 | # does not matter, just like in real OOP! 487 | for node in body.children: 488 | case node.kind: 489 | of nnkMethodDef, nnkProcDef: 490 | # inject `self: T` into the arguments 491 | let n = copyNimTree(node) 492 | n.params.insert(1, newIdentDefs(ident(objReference), typeName)) 493 | # clear the body so we only get a 494 | # declaration 495 | n.body = newEmptyNode() 496 | result.add(n) 497 | 498 | # forward declare the inheritable method 499 | let n2 = copyNimTree(n) 500 | let typeName = $(typeName.toStrLit()) 501 | var procName = "" 502 | 503 | setNodeName(n2, procName, typeName) 504 | 505 | if procName.toLower() == "setup": 506 | foundSetup = true 507 | if procName.toLower() == "teardown": 508 | foundTeardown = true 509 | else: 510 | discard 511 | 512 | # {.pop.} 513 | result.add( 514 | newNimNode(nnkPragma).add( 515 | ident("pop") 516 | ) 517 | ) 518 | 519 | if not foundSetup: 520 | template setupProc(self, typeName, setupProc) = 521 | method setup(self: typeName) 522 | method setupProc(self: typeName) {.base.} 523 | 524 | template setupDecl(self, baseMethod) = 525 | method setup() = 526 | when compiles(self.baseMethod()): 527 | self.baseMethod() 528 | 529 | 530 | var setupProcTypename = ident("setup" & $typeName.toStrLit()) 531 | var baseMethodName = ident("setup" & $baseName.toStrLit()) 532 | result.add(getAst(setupProc(ident(objReference), typeName, setupProcTypename))) 533 | body.add(getAst(setupDecl(ident(objReference), baseMethodName))) 534 | 535 | if not foundTeardown: 536 | template teardownProc(self, typeName, tdProc) = 537 | method tearDown(self: typeName) 538 | method tdProc(self: typeName) {.base.} 539 | 540 | template teardownDecl(self, baseMethod) = 541 | method tearDown() = 542 | when compiles(self.baseMethod()): 543 | self.baseMethod() 544 | 545 | var teardownProcTypename = ident("tearDown" & $typeName.toStrLit()) 546 | var baseTearMethodName = ident("tearDown" & $baseName.toStrLit()) 547 | result.add(getAst(teardownProc(ident(objReference), 548 | typeName, 549 | teardownProcTypename))) 550 | body.add(getAst(teardownDecl(ident(objReference), 551 | baseTearMethodName))) 552 | 553 | template setTestName(self, procName) = 554 | self.currentTestName = procName 555 | 556 | template tryBlock(self, testCall) = 557 | self.numTests += 1 558 | try: 559 | testCall 560 | when defined(quiet): 561 | when noColors: 562 | stdout.write(".") 563 | else: 564 | setForegroundColor(fgGreen) 565 | writeStyled(".", {styleBright}) 566 | setForegroundColor(fgWhite) 567 | else: 568 | var okStr = "[OK]" 569 | if self.lastTestFailed: 570 | okStr = "\l" & okStr 571 | 572 | when not noColors: 573 | styledEcho(styleBright, fgGreen, okStr, 574 | fgWhite, " ", self.currentTestName) 575 | else: 576 | echo "$1 $2".format(okStr, self.currentTestName) 577 | 578 | self.testsPassed += 1 579 | self.lastTestFailed = false 580 | except TestAssertError: 581 | let e = (ref TestAssertError)(getCurrentException()) 582 | 583 | when defined(quiet): 584 | when noColors: 585 | stdout.write("F") 586 | else: 587 | setForegroundColor(fgRed) 588 | writeStyled("F", {styleBright}) 589 | setForegroundColor(fgWhite) 590 | else: 591 | when not noColors: 592 | styledEcho(styleBright, 593 | fgRed, "\l[Failed]", 594 | fgWhite, " ", self.currentTestName) 595 | else: 596 | echo "\l[Failed] $1".format(self.currentTestName) 597 | 598 | let 599 | name = e.checkFuncName 600 | snip = e.codeSnip 601 | line = e.lineNumber 602 | filename = e.fileName 603 | vals = e.valTable 604 | 605 | when not noColors: 606 | styledEcho(styleDim, fgWhite, " Condition: $2($1)\l".format(snip, name), " Where:") 607 | for k, v in vals.pairs: 608 | styledEcho(styleDim, fgCyan, " ", k, 609 | fgWhite, " -> ", 610 | fgGreen, v) 611 | styledEcho(styleDim, fgWhite, " Location: $1; line $2".format(filename, line)) 612 | else: 613 | echo " Condition: $2($1)".format(snip, name) 614 | echo " Where:" 615 | for k, v in vals.pairs: 616 | echo " ", k, " -> ", v 617 | 618 | echo " Location: $1; line $2".format(filename, line) 619 | self.lastTestFailed = true 620 | 621 | # Iterate over the statements, adding `self: T` 622 | # to the parameters of functions 623 | for node in body.children: 624 | case node.kind: 625 | of nnkMethodDef, nnkProcDef: 626 | # inject `self: T` into the arguments 627 | let n = copyNimTree(node) 628 | n.params.insert(1, newIdentDefs(ident(objReference), typeName)) 629 | 630 | # Copy the proc or method for inheritance 631 | # ie: procNameClassName() 632 | let n2 = copyNimTree(node) 633 | n2.params.insert(1, newIdentDefs(ident(objReference), typeName)) 634 | 635 | let typeName = $(typeName.toStrLit()) 636 | var procName = $(n2.name.toStrLit()) 637 | var isAssignment = procName.contains("=") 638 | 639 | setNodeName(n2, procName, typeName) 640 | 641 | if procName.toLower() == "setup": 642 | let dotName = newDotExpr(ident(objReference), ident("name")) 643 | let setName = newAssignment(dotName, newLit(typeName)) 644 | n2.body.add(setName) 645 | let dotRan = newDotExpr(ident(objReference), ident("lastTestFailed")) 646 | let setRan = newAssignment(dotRan, ident("true")) 647 | n2.body.add(setRan) 648 | elif procName.toLower() == "teardown": 649 | discard 650 | elif procName.startswith("test"): 651 | let procCall = newDotExpr(ident(objReference), 652 | ident(procName & typeName)) 653 | 654 | runTests[0][6].add(getAst(setTestName(ident(objReference), procName))) 655 | runTests[0][6].add(getAst(tryBlock(ident(objReference), procCall))) 656 | 657 | # simply call the class method from here 658 | # proc procName= 659 | # procName_ClassName() 660 | var p: seq[NimNode] = @[] 661 | for i in 1..n.params.len-1: 662 | p.add(n.params[i][0]) 663 | if isAssignment: 664 | let dot = newDotExpr(ident(objReference), ident(procName & typeName)) 665 | n.body = newStmtList(newAssignment(dot, p[1])) 666 | else: 667 | n.body = newStmtList(newCall(procName & typeName, p)) 668 | 669 | result.add(n) 670 | 671 | of nnkVarSection: 672 | # variables get turned into fields of the type. 673 | for n in node.children: 674 | recList.add(n) 675 | else: 676 | result.add(node) 677 | 678 | # The following prints out the AST structure: 679 | # 680 | # import macros 681 | # dumptree: 682 | # type X = ref object of Y 683 | # z: int 684 | # -------------------- 685 | # TypeSection 686 | # TypeDef 687 | # Ident !"X" 688 | # Empty 689 | # RefTy 690 | # ObjectTy 691 | # Empty 692 | # OfInherit 693 | # Ident !"Y" 694 | # RecList 695 | # IdentDefs 696 | # Ident !"z" 697 | # Ident !"int" 698 | # Empty 699 | 700 | var typeDecl: NimNode 701 | 702 | template declareTypeExport(tname, bname) = 703 | type tname* = ref object of bname 704 | template declareType(tname, bname) = 705 | type tname = ref object of bname 706 | 707 | if baseName == nil: 708 | if exportClass: 709 | typeDecl = getAst(declareTypeExport(typeName, TestSuite)) 710 | else: 711 | typeDecl = getAst(declareType(typeName, TestSuite)) 712 | else: 713 | if exportClass: 714 | typeDecl = getAst(declareTypeExport(typeName, baseName)) 715 | else: 716 | typeDecl = getAst(declareType(typeName, baseName)) 717 | 718 | # Inspect the tree structure: 719 | # 720 | # echo typeDecl.treeRepr 721 | # -------------------- 722 | # StmtList 723 | # TypeSection 724 | # TypeDef 725 | # Ident !"UnitTests" 726 | # Empty 727 | # RefTy 728 | # ObjectTy 729 | # Empty 730 | # OfInherit 731 | # Ident !"RootObj" 732 | # Empty <= We want to replace this 733 | 734 | var objTyNode = getNode(nnkObjectTy, typeDecl) 735 | objTyNode[2] = recList 736 | 737 | # insert the type declaration 738 | result.insert(0, typeDecl) 739 | 740 | # insert libs needed 741 | result.insert(0, getAst(importRequiredLibs())) 742 | 743 | result.add(runTests) 744 | 745 | template addTestSuite(typeName) = 746 | testSuites.add(typeName()) 747 | 748 | result.add(getAst(addTestSuite(typeName))) 749 | 750 | 751 | proc printRunning(suite: TestSuite) = 752 | let termSize = getTermSize() 753 | var 754 | numTicks = termSize[1] 755 | ticks = "" 756 | 757 | for i in 0..".}: uint 66 | 67 | proc ioctl*(f: int, device: uint, w: var winsize): int {.importc: "ioctl", 68 | header: "", varargs, tags: [WriteIOEffect].} 69 | 70 | proc getTermSize*(): (int, int) = 71 | var w: winsize 72 | let ret = ioctl(STDOUT_FILENO, TIOCGWINSZ, addr(w)) 73 | 74 | if ret == -1: 75 | return (-1, -1) 76 | 77 | return (w.ws_row.int, w.ws_col.int) 78 | -------------------------------------------------------------------------------- /tests/nim.cfg: -------------------------------------------------------------------------------- 1 | --path:"../src/" 2 | -------------------------------------------------------------------------------- /tests/test.nim: -------------------------------------------------------------------------------- 1 | import einheit 2 | 3 | testSuite UnitTests: 4 | var 5 | testObj: int 6 | testArray: array[4, int] 7 | 8 | proc doThings() = 9 | # This proc won't be invoked as a test 10 | self.testObj = 400 11 | self.check(self.testObj == 400) 12 | 13 | method setup() = 14 | self.testObj = 90 15 | for i in 0 ..< self.testArray.len(): 16 | self.testArray[i] = i 17 | 18 | method tearDown() = 19 | self.testObj = 0 20 | 21 | method testForB() = 22 | var b = 4 23 | self.doThings() 24 | self.check(b == 4) 25 | 26 | method testArrayAssert() = 27 | self.check(self.testArray == [0,1,2]) 28 | 29 | method testForC() = 30 | var c = 0 31 | # supposed to fail 32 | self.check(c == 1) 33 | 34 | 35 | testSuite UnitTestsNew: 36 | var 37 | testObj: int 38 | 39 | method setup() = 40 | self.testObj = 90 41 | 42 | method tearDown() = 43 | self.testObj = 0 44 | 45 | method testTestObj() = 46 | self.check(self.testObj == 90) 47 | 48 | method testStuff() = 49 | self.check("Stuff" == "Stuff") 50 | 51 | proc returnTrue(): bool= 52 | result = false 53 | 54 | method testMore() = 55 | var more = 23 56 | self.check(more == 1) 57 | 58 | method testMoreMore() = 59 | self.check(self.returnTrue()) 60 | 61 | method testValues() = 62 | proc foo : int = 63 | return 1 64 | 65 | proc bar : int = 66 | return 2 67 | let (a, b) = (123, 321) 68 | self.check(a == b and foo() == bar()) 69 | 70 | # Inheritance! 71 | testSuite TestInherit of UnitTestsNew: 72 | ## This will call every test defined in UnitTestsNew 73 | 74 | proc raisesOs() = 75 | # This proc won't be invoked as a test 76 | raise newException(CatchableError, "Oh no! OS malfunction!") 77 | 78 | method testRaises() = 79 | 80 | # Two ways of checking 81 | self.checkRaises OSError: 82 | self.raisesOs() 83 | 84 | self.checkRaises(OSError, self.raisesOs()) 85 | 86 | 87 | testSuite MoreInheritance of TestInherit: 88 | 89 | method setup() = 90 | # This must be called if overriding setup if you want 91 | # base class setup functionality. You can also call 92 | # self.setupTestInherit() to call the direct parent's 93 | # implementation 94 | self.setupUnitTestsNew() 95 | 96 | # This will make one of the tests inherited from UnitTestsNew 97 | # fail. This is expected. 98 | self.testObj = 12345 99 | 100 | method tearDown() = 101 | # Calling the direct parent's tearDown method 102 | self.tearDownTestInherit() 103 | self.testObj = 0 104 | 105 | method testTestObj() = 106 | # This method is overwritten. To call the base method, 107 | # simply use 108 | # self.testTestObj_UnitTestsNew() 109 | # However, currently this method will be run 110 | # IN ADDITION to the base class's method. 111 | # This one will pass, the other will fail 112 | self.check(self.testObj == 12345) 113 | 114 | method testNewObj() = 115 | self.check(self.testObj == 12345) 116 | 117 | proc doStuff(arg: int, arg2: string): string = 118 | result = $arg & arg2 119 | 120 | method testRefObject() = 121 | type 122 | TestObj = ref object 123 | t: int 124 | 125 | var 126 | d = TestObj(t: 3) 127 | k = TestObj(t: 30) 128 | 129 | proc `==`(d: TestObj, d2: TestObj): bool = 130 | result = d.t == d2.t 131 | 132 | self.check(d == k) 133 | 134 | method testObject() = 135 | type 136 | TestObj = object 137 | t: int 138 | 139 | var 140 | d = TestObj(t: 3) 141 | k = TestObj(t: 30) 142 | 143 | proc `==`(d: TestObj, d2: TestObj): bool = 144 | result = d.t == d2.t 145 | 146 | self.check(d != k) 147 | self.check(d == k) 148 | 149 | method testComplexObject() = 150 | type 151 | Obj1 = object 152 | e: string 153 | Obj2 = object 154 | d: Obj1 155 | var x = Obj2(d:Obj1(e: "Hey")) 156 | var p = 4 157 | proc isObj(obj: Obj2, q: int): bool = 158 | result = false 159 | self.check(x.isObj(p)) 160 | 161 | method testTuple() = 162 | type 163 | Person = tuple[name: string, age: int] 164 | 165 | var 166 | t: Person = (name: "Peter", age: 30) 167 | r: Person = (name: "P", age: 3) 168 | 169 | self.check(t != r) 170 | self.check(t == r) 171 | 172 | method testComplex() = 173 | var 174 | a = 5 175 | s = "stuff" 176 | y = 45 177 | 178 | self.check(self.doStuff(a, s) == "5stuff" and self.doStuff(a, self.doStuff(a, self.doStuff(y, s))) == "something?") 179 | 180 | 181 | when isMainModule: 182 | runTests() 183 | --------------------------------------------------------------------------------