1 // Example usage: dub -c unittest-runner -b unittest
2 module tests.runner;
3 
4 import core.stdc.stdio;
5 import core.stdc.string;
6 
7 import bubel.ecs.vector;
8 import bubel.ecs.simple_vector;
9 import bubel.ecs.std;
10 
11 import std.traits;
12 
13 import tests.time;
14 
15 version (LDC)
16 {
17     import ldc.attributes;
18 }
19 else
20 {
21     enum optStrategy = 0;
22 }
23 
24 enum int ASSERTED = 123;
25 enum string OUT_FILE = "test_report.xml";
26 
27 version (D_BetterC)
28 {
29     version(Posix)
30     {
31         import core.sys.posix.setjmp;
32     }
33     else version(Windows)
34     {
35         version(X86)
36             alias jmp_buf = ubyte[64];
37         else version(X86_64)
38             alias jmp_buf = ubyte[256];
39         else version(IA64)
40             alias jmp_buf = ubyte[512];
41 
42         extern (C) {
43             int _setjmp(ref jmp_buf);
44             void longjmp(ref jmp_buf, int);
45         }
46         alias setjmp = _setjmp;
47     }
48 
49     static jmp_buf gEnvBuffer;
50     static AssertInfo gAssertInfo;
51 
52     extern (C) void __assert(const char* msg, const char* file, int line)
53     {
54         gAssertInfo = AssertInfo(msg, file, line);
55         longjmp(gEnvBuffer, ASSERTED);
56     }
57 }
58 else version = notBetterC;
59 
60 struct AssertInfo
61 {
62     const(char)* msg;
63     const(char)* file;
64     int line;
65 }
66 
67 struct Test
68 {
69     string file;
70     string msg;
71     int file_line;
72     string name;
73     //string classname;
74     int time;
75     bool passed;
76 }
77 
78 struct TestSuite
79 {
80     string name;
81     uint passed = 0;
82     uint failed = 0;
83     uint skipped = 0;
84     Vector!Test tests;
85 }
86 
87 string copyString(const char* str)
88 {
89     auto length = strlen(str);
90     char[] arr = cast(char[]) str[0 .. length + 1];
91     return cast(string) Mallocator.makeArray(arr);
92 }
93 
94 string copyString(string str)
95 {
96     return cast(string) Mallocator.makeArray((cast(char*)str)[0 .. str.length + 1]);
97 }
98 
99 struct TestRunner(Args...)
100 {
101     void generateJUnit()
102     {
103         write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n");
104 
105         write("<testsuites tests=\"");
106         write(passed + failed);
107         write("\" failures=\"");
108         write(failed);
109         write("\">\n");
110 
111         foreach (ref TestSuite suite; suites)
112         {
113             write("\t<testsuite name=\"");
114             write(suite.name);
115             write("\" tests=\"");
116             write(suite.passed + suite.failed);
117             write("\" failures=\"");
118             write(suite.failed);
119             write("\" skipped=\"");
120             write(suite.skipped);
121             write("\">\n");
122 
123             foreach (ref Test test; suite.tests)
124             {
125                 write("\t\t<testcase name=\"");
126                 write(test.name);
127                 write("\" classname=\"");
128                 write(suite.name);
129                 write("\" time=\"");
130                 write(cast(double)test.time*0.000001);
131                 write("\">");
132                 if (test.msg)
133                 {
134                     write("\n\t\t\t<failure type=\"Fail\" message=\"");
135                     write(test.msg[0 .. $ - 1]);
136                     write("\">");
137                     write("Assert!  File: ");
138                     write(test.file[0 .. $ - 1]);
139                     write(":");
140                     write(test.file_line);
141                     write(" Message: ");
142                     write(test.msg[0 .. $ - 1]);
143                     write("</failure>\n");
144                     write("\t\t</testcase>\n");
145                 }
146                 else write("</testcase>\n");
147                 
148             }
149 
150             write("\t</testsuite>\n");
151         }
152 
153         write("</testsuites>");
154     }
155 
156     @(optStrategy,"none")
157     void runTests(string[] include = null, string[] exclude = null)
158     {
159         foreach (i, module_; Args)
160         {
161             TestSuite* suite = &suites[i];
162             suite.name = module_.stringof;
163 
164             void function() before;
165             void function() after;
166 
167             foreach (index, unittest_; __traits(getUnitTests, module_))
168             {
169                 enum attributes = __traits(getAttributes, unittest_);
170 
171                 static if (attributes.length != 0)
172                 {
173                     foreach(attr_id, attr; attributes)
174                     {
175                         static if(isFunctionPointer!(attr)){}
176                         else static if(attr == "_tr_before")
177                         {
178                             static assert(attr_id+1 < attributes.length);
179                             enum attr2 = attributes[attr_id + 1];
180                             //static assert(__traits(hasMember, module_, attr2));
181                             //alias func = __traits(getMember, module_, attr2);
182                             //attr2();
183                             before = attr2;
184                             //static assert(is(typeof(__traits(getMember, module_, attr2)) == void function()));
185                         }
186                         else
187                         {
188                             if (include.length > 0)
189                             {
190                                 bool matched = false;
191                                 foreach (str; include)
192                                 {
193                                     if (match(str, attr))
194                                     {
195                                         matched = true;
196                                         break;
197                                     }
198                                 }
199 
200                                 foreach (str; exclude)
201                                 {
202                                     if (match(str, attr))
203                                     {
204                                         matched = false;
205                                         break;
206                                     }
207                                 }
208 
209                                 if (!matched)
210                                 {
211                                     suite.skipped++;
212                                     continue;
213                                 }
214                             }
215                         }
216                     }
217                 }
218                 else if (include.length > 0)
219                 {
220                     suite.skipped++;
221                     continue;
222                 }
223 
224                 Test test;
225 
226                 static if (attributes.length == 0)
227                     test.name = "None";
228                 else
229                     test.name = attributes[0];
230 
231                 static if (__traits(hasMember, module_, "beforeEveryTest") && __traits(compiles, module_.beforeEveryTest()))
232                     module_.beforeEveryTest();
233                 if(before)before();
234 
235                 version(D_BetterC)
236                 {
237                     // Save calling environment for longjmp 
238                     int jmp_ret = setjmp(gEnvBuffer);
239 
240                     if (jmp_ret == ASSERTED)
241                     {
242                         test.passed = false;
243                         test.file = copyString(gAssertInfo.file);
244                         test.file_line = gAssertInfo.line;
245                         test.msg = copyString(gAssertInfo.msg);
246                     }
247                     else
248                     {
249                         long time = Time.getUSecTime();
250                         unittest_();
251                         test.passed = true;
252                         test.time = cast(int)(Time.getUSecTime() - time);
253                     }
254                 }
255                 else
256                 {
257                     import core.exception : AssertError, RangeError;
258                     try 
259                     {
260                         unittest_();
261                         test.passed = true;
262                     }
263                     catch(AssertError error)
264                     {
265                         test.passed = false;
266                         test.file = copyString(error.file);
267                         test.file_line = cast(int)error.line;
268                         test.msg = copyString(error.msg);
269                     }
270                     catch(RangeError error)
271                     {
272                         test.passed = false;
273                         test.file = copyString(error.file);
274                         test.file_line = cast(int)error.line;
275                         test.msg = copyString(error.msg);
276                     }
277                 }
278 
279                 if (test.passed)
280                     suite.passed++;
281                 else
282                     suite.failed++;
283                 suite.tests ~= test;
284                 static if (__traits(hasMember, module_, "afterEveryTest") && __traits(compiles, module_.beforeEveryTest()))
285                     module_.afterEveryTest();
286             }
287             passed += suite.passed;
288             failed += suite.failed;
289             skipped += suite.skipped;
290         }
291     }
292 
293     void writeFile()
294     {
295         if (junit.length == 0)
296             generateJUnit();
297         auto file = fopen(OUT_FILE, "w");
298         fwrite(junit.data.ptr, junit.length, 1, file);
299         fclose(file);
300     }
301 
302     void writeOutput()
303     {
304         foreach (ref TestSuite suite; suites)
305         {
306             printf("Suite: \"%s\" Passed: %u/%u Skipped: %u\n", suite.name.ptr,
307                     suite.passed, suite.passed + suite.failed, suite.skipped);
308             foreach (ref Test test; suite.tests)
309             {
310                 if (!test.passed)
311                 {
312                     printf("\tTest: \"%s\" Failed! Line: %u Message: %s\n",
313                             test.name.ptr, test.file_line, test.msg.ptr);
314                 }
315             }
316         }
317         printf("Passed %u/%u tests. Skipped: %u.\n", passed, passed + failed, skipped);
318     }
319 
320     bool match(string a, string b)
321     {
322         uint i = 0;
323         foreach (char c; b)
324         {
325             if (i > a.length)
326                 return false;
327             if (a[i] == c)
328                 i++;
329             else
330             {
331                 if (a[i] == '*')
332                 {
333                     if (i + 1 >= a.length)
334                         return true;
335                     else if (a[i + 1] == c)
336                         i += 2;
337                 }
338                 else
339                     return false;
340             }
341         }
342         return i == a.length;
343     }
344 
345     void write(string txt)
346     {
347         junit.add(cast(ubyte[]) txt);
348     }
349 
350     void write(double num)
351     {
352         ubyte[40] buffer;
353         int len;
354         len = sprintf(cast(char*) buffer.ptr, "%2.8lf", num);
355         junit.add(buffer[0 .. len]);
356     }
357 
358     void write(uint num)
359     {
360         ubyte[20] buffer;
361         int len;
362         len = sprintf(cast(char*) buffer.ptr, "%u", num);
363         junit.add(buffer[0 .. len]);
364     }
365 
366     TestSuite[Args.length] suites;
367     uint passed = 0;
368     uint failed = 0;
369     uint skipped = 0;
370 
371     SimpleVector junit;
372 }
373 
374 version (notBetterC)
375 {
376     extern (C) int rt_init();
377     extern (C) int rt_term();
378     version (D_Coverage) extern (C) void dmd_coverDestPath(string path);
379 }
380 
381 enum before = "_tr_before";
382 
383 void extractStrings(ref Vector!string container, string str)
384 {
385     uint s = 0;
386     foreach (j, char c; str)
387     {
388         if (c == ',')
389         {
390             if (s < j)
391             {
392                 container.add(str[s .. j]);
393             }
394             s = cast(uint) j + 1;
395         }
396     }
397     if (s < str.length)
398     {
399         container.add(str[s .. $]);
400     }
401 }
402 
403 extern (C) int main(int argc, char** args)
404 {
405     Vector!string include;
406     Vector!string exclude;
407 
408     version (notBetterC)
409     {
410         rt_init();
411         version (D_Coverage)
412             dmd_coverDestPath("reports");
413     }
414 
415     for (int i = 1; i < argc; i++)
416     {
417         string arg = cast(string) args[i][0 .. strlen(args[i])];
418         if (arg.length < 2)
419             continue;
420         else if (arg == "-i")
421         {
422             if (i + 1 >= argc)
423                 break;
424             i++;
425             arg = cast(string) args[i][0 .. strlen(args[i])];
426             extractStrings(include, arg);
427         }
428         else if (arg == "-e")
429         {
430             if (i + 1 >= argc)
431                 break;
432             i++;
433             arg = cast(string) args[i][0 .. strlen(args[i])];
434             extractStrings(exclude, arg);
435         }
436         else if (arg.length < 10)
437             continue;
438         else if (arg[0 .. 10] == "--include=")
439         {
440             extractStrings(include, arg[10 .. $]);
441         }
442         else if (arg[0 .. 10] == "--exclude=")
443         {
444             extractStrings(exclude, arg[10 .. $]);
445         }
446     }
447 
448     static import tests.id_manager;
449     static import tests.vector;
450     static import tests.basic;
451     static import tests.perf;
452     static import tests.access_perf;
453     static import tests.bugs;
454     static import tests.map;
455     TestRunner!(tests.id_manager, tests.vector, tests.basic, tests.perf, tests.access_perf, tests.bugs, tests.map) runner;
456 
457     runner.runTests(include[], exclude[]);
458 
459     runner.writeFile();
460 
461     runner.writeOutput();
462 
463     version (notBetterC)
464         rt_term();
465 
466     if (!runner.failed)
467         return 0;
468     else
469         return 1;
470 }
471 
472 version (D_BetterC)
473 {
474     version(LDC)
475     {
476         extern (C) __gshared int _d_eh_personality(int, int, size_t, void*, void*)
477         {
478             return 0;
479         }
480     }
481 }