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 }