1 //          Copyright Juan Manuel Cabo 2012.
2 //          Copyright Mario Kröplin 2018.
3 // Distributed under the Boost Software License, Version 1.0.
4 //    (See accompanying file LICENSE_1_0.txt or copy at
5 //          http://www.boost.org/LICENSE_1_0.txt)
6 
7 module dunit.framework;
8 
9 import dunit.assertion;
10 import dunit.attributes;
11 import dunit.color;
12 
13 import core.runtime;
14 import core.time;
15 import std.algorithm;
16 import std.array;
17 import std.conv;
18 import std.stdio;
19 import std..string;
20 public import std.typetuple;
21 
22 struct TestClass
23 {
24     string name;
25     string[] tests;
26     Disabled[string] disabled;
27     EnabledIf[string] enabledIf;
28     DisabledIf[string] disabledIf;
29     EnabledIfEnvironmentVariable[string] enabledIfEnvVar;
30     DisabledIfEnvironmentVariable[string] disabledIfEnvVar;
31     EnabledOnOs[string] enabledOnOs;
32     DisabledOnOs[string] disabledOnOs;
33     Tag[][string] tags;
34 
35     Object function() create;
36     void function() beforeAll;
37     void function(Object o) beforeEach;
38     void delegate(Object o, string test) test;
39     void function(Object o) afterEach;
40     void function() afterAll;
41 }
42 
43 TestClass[] testClasses;
44 
45 struct TestSelection
46 {
47     TestClass testClass;
48     string[] tests;
49 }
50 
51 mixin template Main()
52 {
53     int main (string[] args)
54     {
55         return dunit_main(args);
56     }
57 }
58 
59 /**
60  * Runs the tests according to the command-line arguments.
61  */
62 public int dunit_main(string[] args)
63 {
64     import std.getopt : config, defaultGetoptPrinter, getopt, GetoptResult;
65     import std.path : baseName;
66     import std.regex : matchFirst;
67 
68     GetoptResult result;
69     string[] filters = null;
70     string[] includeTags = null;
71     string[] excludeTags = null;
72     bool list = false;
73     string report = null;
74     bool verbose = false;
75     bool xml = false;
76     string testSuiteName = "dunit";
77 
78     try
79     {
80         result = getopt(args,
81             config.caseSensitive,
82             "list|l", "Display the test functions, then exit", &list,
83             "filter|f", "Select test functions matching the regular expression", &filters,
84             "include|t", "Provide a tag to be included in the test run", &includeTags,
85             "exclude|T", "Provide a tag to be excluded from the test run", &excludeTags,
86             "verbose|v", "Display more information as the tests are run", &verbose,
87             "xml", "Display progressive XML output", &xml,
88             "report", "Write JUnit-style XML test report", &report,
89             "testsuite", "Provide a test-suite name for the JUnit-style XML test report", &testSuiteName,
90             );
91     }
92     catch (Exception exception)
93     {
94         stderr.writeln("error: ", exception.msg);
95         return 1;
96     }
97 
98     if (result.helpWanted)
99     {
100         writefln("Usage: %s [options]", args.empty ? "testrunner" : baseName(args[0]));
101         writeln("Run the functions with @Test attribute of all classes that mix in UnitTest.");
102         defaultGetoptPrinter("Options:", result.options);
103         return 0;
104     }
105 
106     testClasses = unitTestFunctions ~ testClasses;
107 
108     TestSelection[] testSelections = null;
109 
110     if (filters is null)
111     {
112         foreach (testClass; testClasses)
113             testSelections ~= TestSelection(testClass, testClass.tests);
114     }
115     else
116     {
117         foreach (filter; filters)
118         {
119             foreach (testClass; testClasses)
120             {
121                 foreach (test; testClass.tests)
122                 {
123                     string fullyQualifiedName = testClass.name ~ '.' ~ test;
124 
125                     if (matchFirst(fullyQualifiedName, filter))
126                     {
127                         auto foundTestSelections = testSelections.find!"a.testClass.name == b"(testClass.name);
128 
129                         if (foundTestSelections.empty)
130                             testSelections ~= TestSelection(testClass, [test]);
131                         else
132                             foundTestSelections.front.tests ~= test;
133                     }
134                 }
135             }
136         }
137     }
138     if (!includeTags.empty)
139     {
140         testSelections = testSelections
141             .select!"!a.findAmong(b).empty"(includeTags)
142             .array;
143     }
144     if (!excludeTags.empty)
145     {
146         testSelections = testSelections
147             .select!"a.findAmong(b).empty"(excludeTags)
148             .array;
149     }
150 
151     if (list)
152     {
153         foreach (testSelection; testSelections) with (testSelection)
154         {
155             foreach (test; tests)
156             {
157                 string fullyQualifiedName = testClass.name ~ '.' ~ test;
158 
159                 writeln(fullyQualifiedName);
160             }
161         }
162         return 0;
163     }
164 
165     if (xml)
166     {
167         testListeners ~= new XmlReporter();
168     }
169     else
170     {
171         if (verbose)
172             testListeners ~= new DetailReporter();
173         else
174             testListeners ~= new IssueReporter();
175     }
176 
177     if (!report.empty)
178         testListeners ~= new ReportReporter(report, testSuiteName);
179 
180     auto reporter = new ResultReporter();
181 
182     testListeners ~= reporter;
183     runTests(testSelections, testListeners);
184     if (!xml)
185         reporter.write();
186     return (reporter.errors > 0) ? 1 : (reporter.failures > 0) ? 2 : 0;
187 }
188 
189 private auto select(alias pred)(TestSelection[] testSelections, string[] tags)
190 {
191     import std.functional : binaryFun;
192 
193     bool matches(TestClass testClass, string test)
194     {
195         auto testTags = testClass.tags.get(test, null)
196             .map!(tag => tag.name);
197 
198         return binaryFun!pred(testTags, tags);
199     }
200 
201     TestSelection select(TestSelection testSelection)
202     {
203         string[] tests = testSelection.tests
204             .filter!(test => matches(testSelection.testClass, test))
205             .array;
206 
207         return TestSelection(testSelection.testClass, tests);
208     }
209 
210     return testSelections
211         .map!(testSelection => select(testSelection))
212         .filter!(testSelection => !testSelection.tests.empty);
213 }
214 
215 private TestSelection[] restrict(alias pred)(TestSelection[] testSelections, string[] tags)
216 {
217     TestSelection restrict(TestSelection testSelection)
218     {
219         string[] tests = testSelection.tests
220             .filter!(test => pred(testSelection.testClass.tags.get(test, null), tags))
221             .array;
222 
223         return TestSelection(testSelection.testClass, tests);
224     }
225 
226     return testSelections
227         .map!(testSelection => restrict(testSelection))
228         .filter!(testSelection => !testSelection.tests.empty)
229         .array;
230 }
231 
232 public bool matches(Tag[] tags, string[] choices)
233 {
234     return tags.any!(tag => choices.canFind(tag.name));
235 }
236 
237 public void runTests(TestSelection[] testSelections, TestListener[] testListeners)
238 in
239 {
240     assert(all!"a !is null"(testListeners));
241 }
242 body
243 {
244     bool tryRun(string phase, void delegate() action)
245     {
246         try
247         {
248             version(Have_dshould)
249             {
250                 import dshould.ShouldType : FluentException;
251 
252                 try
253                 {
254                     action();
255                 }
256                 catch (FluentException exception)
257                 {
258                     // convert exception to "fix" the message format
259                     throw new AssertException('\n' ~ exception.msg,
260                         exception.file, exception.line, exception);
261                 }
262             }
263             else
264             {
265                 action();
266             }
267             return true;
268         }
269         catch (AssertException exception)
270         {
271             foreach (testListener; testListeners)
272                 testListener.addFailure(phase, exception);
273             return false;
274         }
275         catch (Throwable throwable)
276         {
277             foreach (testListener; testListeners)
278                 testListener.addError(phase, throwable);
279             return false;
280         }
281     }
282 
283     foreach (testSelection; testSelections) with (testSelection)
284     {
285         foreach (testListener; testListeners)
286             testListener.enterClass(testClass.name);
287 
288         bool initialized = false;
289         bool setUp = false;
290 
291         // run each @Test of the class
292         foreach (test; tests)
293         {
294             bool success = false;
295 
296             foreach (testListener; testListeners)
297                 testListener.enterTest(test);
298             scope (exit)
299                 foreach (testListener; testListeners)
300                     testListener.exitTest(success);
301 
302             const disablingCauses = findDisablingCauses(testClass, test);
303 
304             if ((initialized && !setUp) || !disablingCauses.empty)
305             {
306                 const reason = (initialized && !setUp)
307                     ? "running @BeforeAll failed for preceding test case"
308                     : disablingCauses.front.reason;
309 
310                 foreach (testListener; testListeners)
311                     testListener.skip(reason);
312                 continue;
313             }
314 
315             // use lazy initialization to run @BeforeAll
316             // (failure or error can only be reported for a given test)
317             if (!initialized)
318             {
319                 setUp = tryRun("@BeforeAll",
320                     { testClass.beforeAll(); });
321                 initialized = true;
322             }
323 
324             Object testObject = null;
325 
326             if (setUp)
327             {
328                 success = tryRun("this",
329                     { testObject = testClass.create(); });
330             }
331             if (success)
332             {
333                 success = tryRun("@BeforeEach",
334                     { testClass.beforeEach(testObject); });
335             }
336             if (success)
337             {
338                 success = tryRun("@Test",
339                     { testClass.test(testObject, test); });
340                 // run @AfterEach even if @Test failed
341                 success = tryRun("@AfterEach",
342                     { testClass.afterEach(testObject); })
343                     && success;
344             }
345         }
346         if (setUp)
347         {
348             tryRun("@AfterAll",
349                 { testClass.afterAll(); });
350         }
351     }
352 
353     foreach (testListener; testListeners)
354         testListener.exit();
355 }
356 
357 private Disabled[] findDisablingCauses(TestClass testClass, string test)
358 {
359     import std.process : environment;
360     import std.regex : regex, matchFirst;
361     import std.system : os, OS;
362 
363     Disabled[] disablingCauses;
364 
365     if (test in testClass.disabled)
366     {
367         disablingCauses ~= testClass.disabled[test];
368     }
369     if (test in testClass.disabledOnOs) with (testClass.disabledOnOs[test])
370     {
371         if (value.canFind(os))
372         {
373             const reason = format("operating system %s included in %s", os, value);
374 
375             disablingCauses ~= Disabled(reason);
376         }
377     }
378     if (test in testClass.enabledOnOs) with (testClass.enabledOnOs[test])
379     {
380         if (!value.canFind(os))
381         {
382             const reason = format("operating system %s not included in %s", os, value);
383 
384             disablingCauses ~= Disabled(reason);
385         }
386     }
387     if (test in testClass.disabledIfEnvVar) with (testClass.disabledIfEnvVar[test])
388     {
389         if (environment.get(named) !is null && matchFirst(environment.get(named), regex(matches)))
390         {
391             const reason = format(`value "%s" of environment variable %s matches %s`,
392                     environment.get(named), named, matches);
393 
394             disablingCauses ~= Disabled(reason);
395         }
396     }
397     if (test in testClass.enabledIfEnvVar) with (testClass.enabledIfEnvVar[test])
398     {
399         if (environment.get(named) is null)
400         {
401             const reason = format("environment variable %s not set", named);
402 
403             disablingCauses ~= Disabled(reason);
404         }
405         else if (!matchFirst(environment.get(named), regex(matches)))
406         {
407             const reason = format(`value "%s" of environment variable %s does not match %s`,
408                     environment.get(named), named, matches);
409 
410             disablingCauses ~= Disabled(reason);
411         }
412     }
413     if (test in testClass.disabledIf) with (testClass.disabledIf[test])
414     {
415         if (condition())
416             disablingCauses ~= Disabled(reason);
417     }
418     if (test in testClass.enabledIf) with (testClass.enabledIf[test])
419     {
420         if (!condition())
421             disablingCauses ~= Disabled(reason);
422     }
423     return disablingCauses;
424 }
425 
426 private __gshared TestListener[] testListeners = null;
427 
428 /**
429  * Registered implementations of this interface will be notified
430  * about events that occur during the test run.
431  */
432 interface TestListener
433 {
434     public void enterClass(string className);
435     public void enterTest(string test);
436     public void skip(string reason);
437     public void addFailure(string phase, AssertException exception);
438     public void addError(string phase, Throwable throwable);
439     public void exitTest(bool success);
440     public void exit();
441 
442     public static string prettyOrigin(string className, string test, string phase)
443     {
444         const origin = prettyOrigin(test, phase);
445 
446         if (origin.startsWith('@'))
447             return className ~ origin;
448         else
449             return className ~ '.' ~ origin;
450     }
451 
452     public static string prettyOrigin(string test, string phase)
453     {
454         switch (phase)
455         {
456             case "@Test":
457                 return test;
458             case "this":
459             case "@BeforeAll":
460             case "@AfterAll":
461                 return phase;
462             default:
463                 return test ~ phase;
464         }
465     }
466 }
467 
468 /**
469  * Writes a "progress bar", followed by the errors and the failures.
470  */
471 class IssueReporter : TestListener
472 {
473     private struct Issue
474     {
475         string testClass;
476         string test;
477         string phase;
478         Throwable throwable;
479     }
480 
481     private Issue[] failures = null;
482     private Issue[] errors = null;
483     private string className;
484     private string test;
485 
486     public override void enterClass(string className)
487     {
488         this.className = className;
489     }
490 
491     public override void enterTest(string test)
492     {
493         this.test = test;
494     }
495 
496     public override void skip(string reason)
497     {
498         writec(Color.onYellow, "S");
499     }
500 
501     public override void addFailure(string phase, AssertException exception)
502     {
503         this.failures ~= Issue(this.className, this.test, phase, exception);
504         writec(Color.onRed, "F");
505     }
506 
507     public override void addError(string phase, Throwable throwable)
508     {
509         this.errors ~= Issue(this.className, this.test, phase, throwable);
510         writec(Color.onRed, "E");
511     }
512 
513     public override void exitTest(bool success)
514     {
515         if (success)
516             writec(Color.onGreen, ".");
517     }
518 
519     public override void exit()
520     {
521         writeln();
522 
523         // report errors
524         if (!this.errors.empty)
525         {
526             writeln();
527             if (this.errors.length == 1)
528                 writeln("There was 1 error:");
529             else
530                 writefln("There were %d errors:", this.errors.length);
531 
532             foreach (i, issue; this.errors)
533             {
534                 writefln("%d) %s", i + 1,
535                     prettyOrigin(issue.testClass, issue.test, issue.phase));
536                 writeln(issue.throwable.description);
537                 stdout.lockingTextWriter.description(issue.throwable.info);
538             }
539         }
540 
541         // report failures
542         if (!this.failures.empty)
543         {
544             writeln();
545             if (this.failures.length == 1)
546                 writeln("There was 1 failure:");
547             else
548                 writefln("There were %d failures:", this.failures.length);
549 
550             foreach (i, issue; this.failures)
551             {
552                 writefln("%d) %s", i + 1,
553                     prettyOrigin(issue.testClass, issue.test, issue.phase));
554                 writeln(issue.throwable.description);
555             }
556         }
557     }
558 }
559 
560 /**
561  * Writes a detailed test report.
562  */
563 class DetailReporter : TestListener
564 {
565     private string test;
566     private TickDuration startTime;
567 
568     public override void enterClass(string className)
569     {
570         writeln(className);
571     }
572 
573     public override void enterTest(string test)
574     {
575         this.test = test;
576         this.startTime = TickDuration.currSystemTick();
577     }
578 
579     public override void skip(string reason)
580     {
581         writec(Color.yellow, "    SKIP: ");
582         writeln(this.test);
583         if (!reason.empty)
584             writeln(indent(format(`"%s"`, reason)));
585     }
586 
587     public override void addFailure(string phase, AssertException exception)
588     {
589         writec(Color.red, "    FAILURE: ");
590         writeln(prettyOrigin(this.test, phase));
591         writeln(indent(exception.description));
592     }
593 
594     public override void addError(string phase, Throwable throwable)
595     {
596         writec(Color.red, "    ERROR: ");
597         writeln(prettyOrigin(this.test, phase));
598         writeln(indent(throwable.description));
599         stdout.lockingTextWriter.description(throwable.info);
600     }
601 
602     public override void exitTest(bool success)
603     {
604         if (success)
605         {
606             const elapsed = (TickDuration.currSystemTick() - this.startTime).usecs() / 1_000.0;
607 
608             writec(Color.green, "    OK: ");
609             writefln("%6.2f ms  %s", elapsed, this.test);
610         }
611     }
612 
613     public override void exit()
614     {
615         // do nothing
616     }
617 
618     private static string indent(string s, string indent = "        ")
619     {
620         return s.splitLines(KeepTerminator.yes).map!(line => indent ~ line).join;
621     }
622  }
623 
624 /**
625  * Writes a summary about the tests run.
626  */
627 class ResultReporter : TestListener
628 {
629     private uint tests = 0;
630     private uint failures = 0;
631     private uint errors = 0;
632     private uint skips = 0;
633 
634     public override void enterClass(string className)
635     {
636         // do nothing
637     }
638 
639     public override void enterTest(string test)
640     {
641         ++this.tests;
642     }
643 
644     public override void skip(string reason)
645     {
646         ++this.skips;
647     }
648 
649     public override void addFailure(string phase, AssertException exception)
650     {
651         ++this.failures;
652     }
653 
654     public override void addError(string phase, Throwable throwable)
655     {
656         ++this.errors;
657     }
658 
659     public override void exitTest(bool success)
660     {
661         // do nothing
662     }
663 
664     public override void exit()
665     {
666         // do nothing
667     }
668 
669     public void write() const
670     {
671         writeln();
672         writefln("Tests run: %d, Failures: %d, Errors: %d, Skips: %d",
673             this.tests, this.failures, this.errors, this.skips);
674 
675         if (this.failures + this.errors == 0)
676         {
677             writec(Color.onGreen, "OK");
678             writeln();
679         }
680         else
681         {
682             writec(Color.onRed, "NOT OK");
683             writeln();
684         }
685     }
686 }
687 
688 /**
689  * Writes progressive XML output.
690  */
691 class XmlReporter : TestListener
692 {
693     import undead.xml : Document, Element, Tag;
694 
695     private Document testCase;
696     private string className;
697     private TickDuration startTime;
698 
699     public override void enterClass(string className)
700     {
701         this.className = className;
702     }
703 
704     public override void enterTest(string test)
705     {
706         this.testCase = new Document(new Tag("testcase"));
707         this.testCase.tag.attr["classname"] = this.className;
708         this.testCase.tag.attr["name"] = test;
709         this.startTime = TickDuration.currSystemTick();
710     }
711 
712     public override void skip(string reason)
713     {
714         auto element = new Element("skipped");
715 
716         element.tag.attr["message"] = reason;
717         this.testCase ~= element;
718     }
719 
720     public override void addFailure(string phase, AssertException exception)
721     {
722         auto element = new Element("failure");
723         const message = format("%s %s", phase, exception.description);
724 
725         element.tag.attr["message"] = message;
726         this.testCase ~= element;
727     }
728 
729     public override void addError(string phase, Throwable throwable)
730     {
731         auto element = new Element("error", throwable.info.toString);
732         const message = format("%s %s", phase, throwable.description);
733 
734         element.tag.attr["message"] = message;
735         this.testCase ~= element;
736     }
737 
738     public override void exitTest(bool success)
739     {
740         const elapsed = (TickDuration.currSystemTick() - this.startTime).msecs() / 1_000.0;
741 
742         this.testCase.tag.attr["time"] = format("%.3f", elapsed);
743 
744         const report = join(this.testCase.pretty(4), "\n");
745 
746         writeln(report);
747     }
748 
749     public override void exit()
750     {
751         // do nothing
752     }
753 }
754 
755 /**
756  * Writes a JUnit-style XML test report.
757  */
758 class ReportReporter : TestListener
759 {
760     import undead.xml : Document, Element, Tag;
761 
762     private const string fileName;
763     private Document document;
764     private Element testSuite;
765     private Element testCase;
766     private string className;
767     private TickDuration startTime;
768 
769     public this(string fileName, string testSuiteName)
770     {
771         this.fileName = fileName;
772         this.document = new Document(new Tag("testsuites"));
773         this.testSuite = new Element("testsuite");
774         this.testSuite.tag.attr["name"] = testSuiteName;
775         this.document ~= this.testSuite;
776     }
777 
778     public override void enterClass(string className)
779     {
780         this.className = className;
781     }
782 
783     public override void enterTest(string test)
784     {
785         this.testCase = new Element("testcase");
786         this.testCase.tag.attr["classname"] = this.className;
787         this.testCase.tag.attr["name"] = test;
788         this.testSuite ~= this.testCase;
789         this.startTime = TickDuration.currSystemTick();
790     }
791 
792     public override void skip(string reason)
793     {
794         // avoid wrong interpretation of more than one child
795         if (this.testCase.elements.empty)
796         {
797             auto element = new Element("skipped");
798 
799             element.tag.attr["message"] = reason;
800             this.testCase ~= element;
801         }
802     }
803 
804     public override void addFailure(string phase, AssertException exception)
805     {
806         // avoid wrong interpretation of more than one child
807         if (this.testCase.elements.empty)
808         {
809             auto element = new Element("failure");
810             const message = format("%s %s", phase, exception.description);
811 
812             element.tag.attr["message"] = message;
813             this.testCase ~= element;
814         }
815     }
816 
817     public override void addError(string phase, Throwable throwable)
818     {
819         // avoid wrong interpretation of more than one child
820         if (this.testCase.elements.empty)
821         {
822             auto element = new Element("error", throwable.info.toString);
823             const message = format("%s %s", phase, throwable.description);
824 
825             element.tag.attr["message"] = message;
826             this.testCase ~= element;
827         }
828     }
829 
830     public override void exitTest(bool success)
831     {
832         const elapsed = (TickDuration.currSystemTick() - this.startTime).msecs() / 1_000.0;
833 
834         this.testCase.tag.attr["time"] = format("%.3f", elapsed);
835     }
836 
837     public override void exit()
838     {
839         import std.file : mkdirRecurse, write;
840         import std.path: dirName;
841 
842         const report = join(this.document.pretty(4), "\n") ~ "\n";
843 
844         mkdirRecurse(this.fileName.dirName);
845         write(this.fileName, report);
846     }
847 }
848 
849 shared static this()
850 {
851     Runtime.moduleUnitTester = () => true;
852 }
853 
854 private TestClass[] unitTestFunctions()
855 {
856     TestClass[] testClasses = null;
857     TestClass testClass;
858 
859     testClass.tests = ["unittest"];
860     testClass.create = () => null;
861     testClass.beforeAll = () {};
862     testClass.beforeEach = (o) {};
863     testClass.afterEach = (o) {};
864     testClass.afterAll = () {};
865 
866     foreach (moduleInfo; ModuleInfo)
867     {
868         if (moduleInfo)
869         {
870             auto unitTest = moduleInfo.unitTest;
871 
872             if (unitTest)
873             {
874                 testClass.name = moduleInfo.name;
875                 testClass.test = (o, test) { unitTest(); };
876                 testClasses ~= testClass;
877             }
878         }
879     }
880     return testClasses;
881 }
882 
883 /**
884  * Registers a class as a unit test.
885  */
886 mixin template UnitTest()
887 {
888     private static this()
889     {
890         TestClass testClass;
891 
892         testClass.name = this.classinfo.name;
893         testClass.tests = _members!(typeof(this), Test);
894         testClass.disabled = _attributeByMember!(typeof(this), Disabled);
895         testClass.enabledIf = _attributeByMember!(typeof(this), EnabledIf);
896         testClass.disabledIf = _attributeByMember!(typeof(this), DisabledIf);
897         testClass.enabledIfEnvVar = _attributeByMember!(typeof(this), EnabledIfEnvironmentVariable);
898         testClass.disabledIfEnvVar = _attributeByMember!(typeof(this), DisabledIfEnvironmentVariable);
899         testClass.enabledOnOs = _attributeByMember!(typeof(this), EnabledOnOs);
900         testClass.disabledOnOs = _attributeByMember!(typeof(this), DisabledOnOs);
901         testClass.tags = _attributesByMember!(typeof(this), Tag);
902 
903         testClass.create = ()
904         {
905             mixin("return new " ~ typeof(this).stringof ~ "();");
906         };
907         testClass.beforeAll = ()
908         {
909             mixin(_staticSequence(_members!(typeof(this), BeforeAll)));
910         };
911         testClass.beforeEach = (Object o)
912         {
913             mixin(_sequence(_members!(typeof(this), BeforeEach)));
914         };
915         testClass.test = (Object o, string name)
916         {
917             mixin(_choice(_members!(typeof(this), Test)));
918         };
919         testClass.afterEach = (Object o)
920         {
921             mixin(_sequence(_members!(typeof(this), AfterEach)));
922         };
923         testClass.afterAll = ()
924         {
925             mixin(_staticSequence(_members!(typeof(this), AfterAll)));
926         };
927 
928         testClasses ~= testClass;
929     }
930 
931     private static string _choice(in string[] memberFunctions)
932     {
933         string block = "auto testObject = cast(" ~ typeof(this).stringof ~ ") o;\n";
934 
935         block ~= "switch (name)\n{\n";
936         foreach (memberFunction; memberFunctions)
937             block ~= `case "` ~ memberFunction ~ `": testObject.` ~ memberFunction ~ "(); break;\n";
938         block ~= "default: break;\n}\n";
939         return block;
940     }
941 
942     private static string _staticSequence(in string[] memberFunctions)
943     {
944         string block = null;
945 
946         foreach (memberFunction; memberFunctions)
947             block ~= memberFunction ~ "();\n";
948         return block;
949     }
950 
951     private static string _sequence(in string[] memberFunctions)
952     {
953         string block = "auto testObject = cast(" ~ typeof(this).stringof ~ ") o;\n";
954 
955         foreach (memberFunction; memberFunctions)
956             block ~= "testObject." ~ memberFunction ~ "();\n";
957         return block;
958     }
959 
960     template _members(T, alias attribute)
961     {
962         static string[] helper()
963         {
964             import std.meta : AliasSeq;
965             import std.traits : hasUDA;
966 
967             string[] members;
968 
969             foreach (name; __traits(allMembers, T))
970             {
971                 static if (__traits(compiles, __traits(getMember, T, name)))
972                 {
973                     alias member = AliasSeq!(__traits(getMember, T, name));
974 
975                     static if (__traits(compiles, hasUDA!(member, attribute)))
976                     {
977                         static if (hasUDA!(member, attribute))
978                             members ~= name;
979                     }
980                 }
981             }
982             return members;
983         }
984 
985         enum _members = helper;
986     }
987 
988     template _attributeByMember(T, Attribute)
989     {
990         static Attribute[string] helper()
991         {
992             import std.format : format;
993             import std.meta : AliasSeq;
994 
995             Attribute[string] attributeByMember;
996 
997             foreach (name; __traits(allMembers, T))
998             {
999                 static if (__traits(compiles, __traits(getMember, T, name)))
1000                 {
1001                     alias member = AliasSeq!(__traits(getMember, T, name));
1002 
1003                     static if (__traits(compiles, _getUDAs!(member, Attribute)))
1004                     {
1005                         alias attributes = _getUDAs!(member, Attribute);
1006 
1007                         static if (attributes.length > 0)
1008                         {
1009                             static assert(attributes.length == 1,
1010                                 format("%s.%s should not have more than one attribute @%s",
1011                                     T.stringof, name, Attribute.stringof));
1012 
1013                             attributeByMember[name] = attributes[0];
1014                         }
1015                     }
1016                 }
1017             }
1018             return attributeByMember;
1019         }
1020 
1021         enum _attributeByMember = helper;
1022     }
1023 
1024     template _attributesByMember(T, Attribute)
1025     {
1026         static Attribute[][string] helper()
1027         {
1028             import std.meta : AliasSeq;
1029 
1030             Attribute[][string] attributesByMember;
1031 
1032             foreach (name; __traits(allMembers, T))
1033             {
1034                 static if (__traits(compiles, __traits(getMember, T, name)))
1035                 {
1036                     alias member = AliasSeq!(__traits(getMember, T, name));
1037 
1038                     static if (__traits(compiles, _getUDAs!(member, Attribute)))
1039                     {
1040                         alias attributes = _getUDAs!(member, Attribute);
1041 
1042                         static if (attributes.length > 0)
1043                             attributesByMember[name] = attributes;
1044                     }
1045                 }
1046             }
1047             return attributesByMember;
1048         }
1049 
1050         enum _attributesByMember = helper;
1051     }
1052 
1053     // Gets user-defined attributes, but also gets Attribute.init for @Attribute.
1054     template _getUDAs(alias member, Attribute)
1055     {
1056         static auto helper()
1057         {
1058             Attribute[] attributes;
1059 
1060             static if (__traits(compiles, __traits(getAttributes, member)))
1061             {
1062                 foreach (attribute; __traits(getAttributes, member))
1063                 {
1064                     static if (is(attribute == Attribute))
1065                         attributes ~= Attribute.init;
1066                     static if (is(typeof(attribute) == Attribute))
1067                         attributes ~= attribute;
1068                 }
1069             }
1070             return attributes;
1071         }
1072 
1073         enum _getUDAs = helper;
1074     }
1075 }