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