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         testClass.create = ()
821         {
822             mixin("return new " ~ typeof(this).stringof ~ "();");
823         };
824         testClass.beforeAll = ()
825         {
826             mixin(_staticSequence(_members!(typeof(this), BeforeAll)));
827         };
828         testClass.beforeEach = (Object o)
829         {
830             mixin(_sequence(_members!(typeof(this), BeforeEach)));
831         };
832         testClass.test = (Object o, string name)
833         {
834             mixin(_choice(_members!(typeof(this), Test)));
835         };
836         testClass.afterEach = (Object o)
837         {
838             mixin(_sequence(_members!(typeof(this), AfterEach)));
839         };
840         testClass.afterAll = ()
841         {
842             mixin(_staticSequence(_members!(typeof(this), AfterAll)));
843         };
844 
845         testClasses ~= testClass;
846     }
847 
848     private static string _choice(in string[] memberFunctions)
849     {
850         string block = "auto testObject = cast(" ~ typeof(this).stringof ~ ") o;\n";
851 
852         block ~= "switch (name)\n{\n";
853         foreach (memberFunction; memberFunctions)
854             block ~= `case "` ~ memberFunction ~ `": testObject.` ~ memberFunction ~ "(); break;\n";
855         block ~= "default: break;\n}\n";
856         return block;
857     }
858 
859     private static string _staticSequence(in string[] memberFunctions)
860     {
861         string block = null;
862 
863         foreach (memberFunction; memberFunctions)
864             block ~= memberFunction ~ "();\n";
865         return block;
866     }
867 
868     private static string _sequence(in string[] memberFunctions)
869     {
870         string block = "auto testObject = cast(" ~ typeof(this).stringof ~ ") o;\n";
871 
872         foreach (memberFunction; memberFunctions)
873             block ~= "testObject." ~ memberFunction ~ "();\n";
874         return block;
875     }
876 
877     template _members(T, alias attribute)
878     {
879         static string[] helper()
880         {
881             import std.meta : AliasSeq;
882             import std.traits : hasUDA;
883 
884             string[] members;
885 
886             foreach (name; __traits(allMembers, T))
887             {
888                 static if (__traits(compiles, __traits(getMember, T, name)))
889                 {
890                     alias member = AliasSeq!(__traits(getMember, T, name));
891 
892                     static if (__traits(compiles, hasUDA!(member, attribute)))
893                     {
894                         static if (hasUDA!(member, attribute))
895                             members ~= name;
896                     }
897                 }
898             }
899             return members;
900         }
901 
902         enum _members = helper;
903     }
904 
905     template _attributeByMember(T, Attribute)
906     {
907         static Attribute[string] helper()
908         {
909             import std.format : format;
910             import std.meta : AliasSeq;
911 
912             Attribute[string] attributeByMember;
913 
914             foreach (name; __traits(allMembers, T))
915             {
916                 static if (__traits(compiles, __traits(getMember, T, name)))
917                 {
918                     alias member = AliasSeq!(__traits(getMember, T, name));
919 
920                     static if (__traits(compiles, _getUDAs!(member, Attribute)))
921                     {
922                         alias attributes = _getUDAs!(member, Attribute);
923 
924                         static if (attributes.length > 0)
925                         {
926                             static assert(attributes.length == 1,
927                                 format("%s.%s should not have more than one attribute @%s",
928                                     T.stringof, name, Attribute.stringof));
929 
930                             attributeByMember[name] = attributes[0];
931                         }
932                     }
933                 }
934             }
935             return attributeByMember;
936         }
937 
938         enum _attributeByMember = helper;
939     }
940 
941     template _attributesByMember(T, Attribute)
942     {
943         static Attribute[][string] helper()
944         {
945             import std.meta : AliasSeq;
946 
947             Attribute[][string] attributesByMember;
948 
949             foreach (name; __traits(allMembers, T))
950             {
951                 static if (__traits(compiles, __traits(getMember, T, name)))
952                 {
953                     alias member = AliasSeq!(__traits(getMember, T, name));
954 
955                     static if (__traits(compiles, _getUDAs!(member, Attribute)))
956                     {
957                         alias attributes = _getUDAs!(member, Attribute);
958 
959                         static if (attributes.length > 0)
960                             attributesByMember[name] = attributes;
961                     }
962                 }
963             }
964             return attributesByMember;
965         }
966 
967         enum _attributesByMember = helper;
968     }
969 
970     // Gets user-defined attributes, but also gets Attribute.init for @Attribute.
971     template _getUDAs(alias member, Attribute)
972     {
973         static auto helper()
974         {
975             Attribute[] attributes;
976 
977             static if (__traits(compiles, __traits(getAttributes, member)))
978             {
979                 foreach (attribute; __traits(getAttributes, member))
980                 {
981                     static if (is(attribute == Attribute))
982                         attributes ~= Attribute.init;
983                     static if (is(typeof(attribute) == Attribute))
984                         attributes ~= attribute;
985                 }
986             }
987             return attributes;
988         }
989 
990         enum _getUDAs = helper;
991     }
992 }