1 /++ 2 A module containing the discovery logic for spec tests 3 4 Copyright: © 2017 Szabo Bogdan 5 License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. 6 Authors: Szabo Bogdan 7 +/ 8 module trial.discovery.spec; 9 10 import std.algorithm; 11 import std.stdio; 12 import std.array; 13 import std.traits; 14 import std..string; 15 16 import trial.interfaces; 17 import trial.discovery.code; 18 19 alias SetupFunction = void delegate() @system; 20 21 private string[] suitePath; 22 private ulong[string] testsPerSuite; 23 private TestCase[] testCases; 24 private SetupFunction[] beforeList; 25 private SetupFunction[] afterList; 26 27 /// Define a Spec test suite 28 void describe(T)(string name, T description) 29 { 30 if (suitePath.length == 0) 31 { 32 suitePath = [moduleName!description]; 33 } 34 35 auto beforeListIndex = beforeList.length; 36 auto afterListIndex = afterList.length; 37 38 suitePath ~= name; 39 40 description(); 41 42 beforeList = beforeList[0 .. beforeListIndex]; 43 afterList = afterList[0 .. afterListIndex]; 44 45 suitePath = suitePath[0 .. $ - 1]; 46 } 47 48 /// Define a function that will be ran before all the tests 49 void before(T)(T setup) 50 { 51 bool wasRun; 52 beforeList ~= { 53 if (!wasRun) 54 { 55 setup(); 56 wasRun = true; 57 } 58 }; 59 } 60 61 /// Define a function that will be ran before each test 62 void beforeEach(T)(T setup) 63 { 64 beforeList ~= { setup(); }; 65 } 66 67 /// Define a function that will be ran after each test 68 void afterEach(T)(T setup) 69 { 70 afterList ~= { setup(); }; 71 } 72 73 /// Define a function that will be ran after all the tests were ran 74 void after(T)(T setup) 75 { 76 string suiteName = suitePath.join("."); 77 long executedTests; 78 bool wasRun; 79 80 afterList ~= { 81 if (wasRun) 82 { 83 return; 84 } 85 86 executedTests++; 87 88 if (testsPerSuite[suiteName] < executedTests) 89 { 90 setup(); 91 wasRun = true; 92 } 93 }; 94 } 95 96 private void updateTestCounter(string[] path, long value) 97 { 98 string tmp; 99 string glue; 100 101 foreach (key; path) 102 { 103 tmp ~= glue ~ key; 104 glue = "."; 105 106 testsPerSuite[tmp] += value; 107 } 108 } 109 110 /// Define a Spec 111 void it(T)(string name, T test, string file = __FILE__, size_t line = __LINE__) 112 { 113 auto path = suitePath.dup; 114 115 updateTestCounter(path, 1); 116 117 import std.stdio; 118 auto before = beforeList; 119 auto after = afterList; 120 121 auto testCase = TestCase(suitePath.join("."), name, ({ 122 foreach(a; before) { 123 a(); 124 } 125 126 test(); 127 128 updateTestCounter(path, -1); 129 130 foreach_reverse(a; after) { 131 a(); 132 } 133 })); 134 135 testCase.location = SourceLocation(file, line); 136 137 testCases ~= testCase; 138 } 139 140 141 /// Define a pending Spec 142 void it(string name, string file = __FILE__, size_t line = __LINE__) 143 { 144 auto path = suitePath.dup; 145 146 updateTestCounter(path, 1); 147 148 auto testCase = TestCase(suitePath.join("."), name, ({ throw new PendingTestException(); })); 149 150 testCase.location = SourceLocation(file, line); 151 152 testCases ~= testCase; 153 } 154 155 /// The main spec container 156 template Spec(alias definition) 157 { 158 shared static this() 159 { 160 suitePath = [moduleName!definition]; 161 definition(); 162 } 163 } 164 165 /// The default test discovery looks for unit test sections and groups them by module 166 class SpecTestDiscovery : ITestDiscovery 167 { 168 /// Returns all the Specs as TestCase structure 169 TestCase[] getTestCases() 170 { 171 return testCases; 172 } 173 174 /// It does nothing... 175 void addModule(string file, string moduleName)() 176 { 177 } 178 179 private void noTest() 180 { 181 assert(false, "you can not run this test"); 182 } 183 184 version (Have_libdparse) 185 { 186 private TestCase[] getTestCasesFromSpec(string file, string suite, const(Token)[] tokens) { 187 TestCase[] testCases; 188 auto iterator = TokenIterator(tokens); 189 190 foreach(token; iterator) { 191 if(token.text == "describe") { 192 iterator.skipOne.skipWsAndComments; 193 194 if(str(iterator.currentToken.type) == "(") { 195 iterator.skipUntilType("stringLiteral"); 196 string suiteName = iterator.currentToken.text.parseString.strip; 197 198 auto block = iterator.readNextBlock; 199 testCases ~= getTestCasesFromSpec(file, suite ~ "." ~ suiteName, block); 200 } 201 } 202 203 if(token.text == "it") { 204 iterator.skipOne.skipWsAndComments; 205 auto location = SourceLocation(file, iterator.currentToken.line); 206 207 if(str(iterator.currentToken.type) == "(") { 208 iterator.skipUntilType("stringLiteral"); 209 string testName = iterator.currentToken.text.parseString; 210 211 testCases ~= TestCase(suite, testName, &this.noTest, [], location); 212 } 213 } 214 } 215 216 return testCases; 217 } 218 } 219 220 TestCase[] discoverTestCases(string file) 221 { 222 TestCase[] testCases = []; 223 224 version (Have_fluent_asserts) 225 version (Have_libdparse) 226 { 227 import fluentasserts.core.results; 228 229 auto tokens = fileToDTokens(file); 230 231 auto iterator = TokenIterator(tokens); 232 auto moduleName = iterator.skipUntilType("module").skipOne.readUntilType(";").strip; 233 234 string lastName; 235 DLangAttribute[] attributes; 236 237 foreach (token; iterator) 238 { 239 auto type = str(token.type); 240 241 if(token.text == "Spec") { 242 iterator.skipOne.skipWsAndComments; 243 244 if(str(iterator.currentToken.type) == "!") { 245 iterator.skipOne.skipWsAndComments; 246 247 if(str(iterator.currentToken.type) == "(") { 248 auto block = iterator.readNextBlock; 249 250 testCases ~= getTestCasesFromSpec(file, moduleName, block); 251 } 252 } 253 } 254 } 255 } 256 257 return testCases; 258 } 259 } 260 261 /// 262 string parseString(string someString) { 263 if(someString == ""){ 264 return ""; 265 } 266 267 if(someString[0] == '"') { 268 return someString[1..$-1].replace(`\"`, `"`); 269 } 270 271 return someString[1..$-1]; 272 } 273 274 /// resolve the string tokens 275 unittest 276 { 277 `"string token"`.parseString.should.equal("string token"); 278 `"string \" token"`.parseString.should.equal("string \" token"); 279 "`string token`".parseString.should.equal("string token"); 280 } 281 282 version (unittest) 283 { 284 version(Have_fluent_asserts): 285 286 import fluent.asserts; 287 288 private static string trace; 289 290 private alias suite = Spec /* some comment*/ ! /* some comment*/ ( /* some comment*/ { 291 describe("Algorithm", { 292 it("should return false when the value is not present", { 293 [1, 2, 3].canFind(4).should.equal(false); 294 }); 295 }); 296 297 describe /* some comment*/ ("Nested describes", { 298 describe("level 1", { describe("level 2", { it( /* some comment*/ "test name", { }); }); }); 299 300 describe("other level 1", { describe("level 2", { it("test name", { }); }); }); 301 }); 302 303 describe("Before all", { 304 before({ trace ~= "before1"; }); 305 306 describe("level 2", { 307 before({ trace ~= " before2"; }); 308 309 it("should run the hooks", { trace ~= " test1"; }); 310 311 it("should run the hooks", { trace ~= " test2"; }); 312 }); 313 314 describe("level 2 bis", { 315 before({ trace ~= "before2-bis"; }); 316 317 it("should run the hooks", { trace ~= " test3"; }); 318 }); 319 }); 320 321 describe("Before each", { 322 beforeEach({ trace ~= "before1 "; }); 323 324 it("should run the hooks", { trace ~= "test1 "; }); 325 326 describe("level 2", { 327 beforeEach({ trace ~= "before2 "; }); 328 329 it("should run the hooks", { trace ~= "test2 "; }); 330 }); 331 332 describe("level 2 bis", { 333 beforeEach({ trace ~= "before2-bis "; }); 334 335 it("should run the hooks", { trace ~= "test3"; }); 336 }); 337 }); 338 339 describe("After all", { 340 after({ trace ~= "after1"; }); 341 342 describe("level 2", { 343 after({ trace ~= " after2 "; }); 344 345 it("should run the hooks", { trace ~= "test1"; }); 346 347 it("should run the hooks", { trace ~= " test2"; }); 348 }); 349 350 describe("level 2 bis", { 351 after({ trace ~= "after2-bis"; }); 352 353 it("should run the hooks", { trace ~= "test3 "; }); 354 }); 355 }); 356 357 describe("After each", { 358 afterEach({ trace ~= " after1"; }); 359 360 it("should run the hooks", { trace ~= "test1"; }); 361 362 describe("level 2", { 363 afterEach({ trace ~= " after2"; }); 364 365 it("should run the hooks", { trace ~= " test2"; }); 366 }); 367 368 describe("level 2 bis", { 369 afterEach({ trace ~= " after2-bis"; }); 370 371 it("should run the hooks", { trace ~= "test3"; }); 372 }); 373 }); 374 }); 375 } 376 377 /// getTestCases should find the spec suite 378 unittest 379 { 380 auto specDiscovery = new SpecTestDiscovery; 381 auto tests = specDiscovery.getTestCases.filter!( 382 a => a.suiteName == "trial.discovery.spec.Algorithm").array; 383 384 tests.length.should.equal(1).because("the Spec suite defined is in this file"); 385 tests[0].name.should.equal("should return false when the value is not present"); 386 } 387 388 /// discoverTestCases should find the spec suite 389 unittest 390 { 391 auto specDiscovery = new SpecTestDiscovery; 392 auto tests = specDiscovery.discoverTestCases(__FILE__).filter!( 393 a => a.suiteName == "trial.discovery.spec.Algorithm").array; 394 395 tests.length.should.equal(1).because("the Spec suite defined is in this file"); 396 tests[0].name.should.equal("should return false when the value is not present"); 397 } 398 399 /// getTestCases should find the spec suite 400 unittest 401 { 402 auto specDiscovery = new SpecTestDiscovery; 403 auto tests = specDiscovery.getTestCases.filter!( 404 a => a.suiteName == "trial.discovery.spec.Algorithm").array; 405 406 tests.length.should.equal(1).because("the Spec suite defined is in this file"); 407 tests[0].name.should.equal("should return false when the value is not present"); 408 } 409 410 /// getTestCases should find nested spec suites 411 unittest 412 { 413 auto specDiscovery = new SpecTestDiscovery; 414 auto suites = specDiscovery.getTestCases.map!(a => a.suiteName).array; 415 416 suites.should.contain(["trial.discovery.spec.Nested describes.level 1.level 2", 417 "trial.discovery.spec.Nested describes.other level 1.level 2"]).because( 418 "the Spec suites are defined in this file"); 419 } 420 421 /// It should execute the spec before all hooks 422 unittest 423 { 424 auto specDiscovery = new SpecTestDiscovery; 425 auto tests = specDiscovery.getTestCases.filter!( 426 a => a.suiteName.startsWith("trial.discovery.spec.Before all")).array; 427 428 trace = ""; 429 tests[0].func(); 430 tests[1].func(); 431 432 trace.should.equal("before1 before2 test1 test2"); 433 434 trace = ""; 435 tests[2].func(); 436 437 trace.should.equal("before2-bis test3"); 438 } 439 440 /// It should execute the spec after all hooks 441 unittest 442 { 443 auto specDiscovery = new SpecTestDiscovery; 444 auto tests = specDiscovery.getTestCases.filter!( 445 a => a.suiteName.startsWith("trial.discovery.spec.After all")).array; 446 447 trace = ""; 448 tests[0].func(); 449 tests[1].func(); 450 451 trace.should.equal("test1 test2 after2 after1"); 452 453 trace = ""; 454 tests[2].func(); 455 456 trace.should.equal("test3 after2-bis"); 457 } 458 459 /// It should execute the spec before hooks 460 unittest 461 { 462 auto specDiscovery = new SpecTestDiscovery; 463 auto tests = specDiscovery.getTestCases.filter!( 464 a => a.suiteName.startsWith("trial.discovery.spec.Before each")).array; 465 466 trace = ""; 467 tests[0].func(); 468 tests[1].func(); 469 470 trace.should.equal("before1 test1 before1 before2 test2 "); 471 472 trace = ""; 473 tests[2].func(); 474 475 trace.should.equal("before1 before2-bis test3"); 476 } 477 478 /// It should execute the spec after hooks 479 unittest 480 { 481 auto specDiscovery = new SpecTestDiscovery; 482 auto tests = specDiscovery.getTestCases.filter!( 483 a => a.suiteName.startsWith("trial.discovery.spec.After each")).array; 484 485 trace = ""; 486 tests[0].func(); 487 tests[1].func(); 488 489 trace.should.equal("test1 after1 test2 after2 after1"); 490 491 trace = ""; 492 tests[2].func(); 493 494 trace.should.equal("test3 after2-bis after1"); 495 } 496 497 /// discoverTestCases should find the same tests like testCases 498 unittest 499 { 500 auto testDiscovery = new SpecTestDiscovery; 501 502 testDiscovery 503 .discoverTestCases(__FILE__).map!(a => a.toString).join("\n") 504 .should.equal( 505 testDiscovery.getTestCases 506 .filter!(a => a.location.fileName.canFind(__FILE__)) 507 .map!(a => a.toString).join("\n")); 508 }