1 /**
2    Compile-time reflection to find unit tests and set their properties.
3  */
4 module unit_threaded.reflection;
5 
6 import unit_threaded.from;
7 /*
8    These standard library imports contain something important for the code below.
9    Unfortunately I don't know what they are so they're to prevent breakage.
10  */
11 import std.traits;
12 import std.algorithm;
13 import std.array;
14 
15 
16 ///
17 alias void delegate() TestFunction;
18 
19 /**
20  * Common data for test functions and test classes
21  */
22 struct TestData {
23     string name;
24     TestFunction testFunction; ///only used for functions, null for classes
25     bool hidden;
26     bool shouldFail;
27     bool singleThreaded;
28     bool builtin;
29     string suffix; // append to end of getPath
30     string[] tags;
31     TypeInfo exceptionTypeInfo; // for ShouldFailWith
32     int flakyRetries = 0;
33 
34     /// The test's name
35     string getPath() const pure nothrow {
36         string path = name.dup;
37         import std.array: empty;
38         if(!suffix.empty) path ~= "." ~ suffix;
39         return path;
40     }
41 
42     /// If the test is a class
43     bool isTestClass() @safe const pure nothrow {
44         return testFunction is null;
45     }
46 }
47 
48 
49 /**
50  * Finds all test cases (functions, classes, built-in unittest blocks)
51  * Template parameters are module strings
52  */
53 const(TestData)[] allTestData(MOD_STRINGS...)()
54     if(from!"std.meta".allSatisfy!(from!"std.traits".isSomeString, typeof(MOD_STRINGS)))
55 {
56     import std.array: join;
57     import std.range : iota;
58     import std.format : format;
59     import std.algorithm : map;
60 
61     string getModulesString() {
62         string[] modules;
63         foreach(i, module_; MOD_STRINGS) modules ~= "module%d = %s".format(i, module_);
64         return modules.join(", ");
65     }
66 
67     enum modulesString = getModulesString;
68     mixin("import " ~ modulesString ~ ";");
69     mixin("return allTestData!(" ~
70           MOD_STRINGS.length.iota.map!(i => "module%d".format(i)).join(", ") ~
71           ");");
72 }
73 
74 
75 /**
76  * Finds all test cases (functions, classes, built-in unittest blocks)
77  * Template parameters are module symbols
78  */
79 const(TestData)[] allTestData(MOD_SYMBOLS...)()
80     if(!from!"std.meta".anySatisfy!(from!"std.traits".isSomeString, typeof(MOD_SYMBOLS)))
81 {
82     auto allTestsWithFunc(string expr)() pure {
83         import std.traits: ReturnType;
84         import std.meta: AliasSeq;
85         //tests is whatever type expr returns
86         ReturnType!(mixin(expr ~ q{!(MOD_SYMBOLS[0])})) tests;
87         foreach(module_; AliasSeq!MOD_SYMBOLS) {
88             tests ~= mixin(expr ~ q{!module_()}); //e.g. tests ~= moduleTestClasses!module_
89         }
90         return tests;
91     }
92 
93     return allTestsWithFunc!"moduleTestClasses" ~
94            allTestsWithFunc!"moduleTestFunctions" ~
95            allTestsWithFunc!"moduleUnitTests";
96 }
97 
98 
99 private template Identity(T...) if(T.length > 0) {
100     static if(__traits(compiles, { alias x = T[0]; }))
101         alias Identity = T[0];
102     else
103         enum Identity = T[0];
104 }
105 
106 
107 /**
108  * Finds all built-in unittest blocks in the given module.
109  * Recurses into structs, classes, and unions of the module.
110  *
111  * @return An array of TestData structs
112  */
113 TestData[] moduleUnitTests(alias module_)() pure nothrow {
114 
115     // Return a name for a unittest block. If no @Name UDA is found a name is
116     // created automatically, else the UDA is used.
117     // the weird name for the first template parameter is so that it doesn't clash
118     // with a package name
119     string unittestName(alias _theUnitTest, int index)() @safe nothrow {
120         import std.conv: text, to;
121         import std.traits: fullyQualifiedName;
122         import std.traits: getUDAs;
123         import std.meta: Filter;
124         import unit_threaded.attrs: Name;
125 
126         mixin("import " ~ fullyQualifiedName!module_ ~ ";"); //so it's visible
127 
128         enum nameAttrs = getUDAs!(_theUnitTest, Name);
129         static assert(nameAttrs.length == 0 || nameAttrs.length == 1, "Found multiple Name UDAs on unittest");
130 
131         enum strAttrs = Filter!(isStringUDA, __traits(getAttributes, _theUnitTest));
132         enum hasName = nameAttrs.length || strAttrs.length == 1;
133         enum prefix = fullyQualifiedName!(__traits(parent, _theUnitTest)) ~ ".";
134 
135         static if(hasName) {
136             static if(nameAttrs.length == 1)
137                 return prefix ~ nameAttrs[0].value;
138             else
139                 return prefix ~ strAttrs[0];
140         } else {
141             string name;
142             try {
143                 return prefix ~ "unittest" ~ (index).to!string;
144             } catch(Exception) {
145                 assert(false, text("Error converting ", index, " to string"));
146             }
147         }
148     }
149 
150     void function() getUDAFunction(alias composite, alias uda)() pure nothrow {
151         import std.traits: fullyQualifiedName, moduleName, isSomeFunction, hasUDA;
152 
153         // Due to:
154         // https://issues.dlang.org/show_bug.cgi?id=17441
155         // moduleName!composite might fail, so we try to import that only if
156         // if compiles, then try again with fullyQualifiedName
157         enum moduleNameStr = `import ` ~ moduleName!composite ~ `;`;
158         enum fullyQualifiedStr = `import ` ~ fullyQualifiedName!composite ~ `;`;
159 
160         static if(__traits(compiles, mixin(moduleNameStr)))
161             mixin(moduleNameStr);
162         else static if(__traits(compiles, mixin(fullyQualifiedStr)))
163             mixin(fullyQualifiedStr);
164 
165         void function()[] ret;
166         foreach(memberStr; __traits(allMembers, composite)) {
167             static if(__traits(compiles, Identity!(__traits(getMember, composite, memberStr)))) {
168                 alias member = Identity!(__traits(getMember, composite, memberStr));
169                 static if(__traits(compiles, &member)) {
170                     static if(isSomeFunction!member && hasUDA!(member, uda)) {
171                         ret ~= &member;
172                     }
173                 }
174             }
175         }
176 
177         return ret.length ? ret[0] : null;
178     }
179 
180     TestData[] testData;
181 
182     void addMemberUnittests(alias composite)() pure nothrow {
183 
184         import unit_threaded.attrs;
185         import unit_threaded.uda: hasUtUDA;
186         import std.traits: hasUDA;
187         import std.meta: Filter, aliasSeqOf;
188         import std.algorithm: map, cartesianProduct;
189 
190         foreach(index, eLtEstO; __traits(getUnitTests, composite)) {
191 
192             enum dontTest = hasUDA!(eLtEstO, DontTest);
193 
194             static if(!dontTest) {
195 
196                 enum name = unittestName!(eLtEstO, index);
197                 enum hidden = hasUDA!(eLtEstO, HiddenTest);
198                 enum shouldFail = hasUDA!(eLtEstO, ShouldFail) || hasUtUDA!(eLtEstO, ShouldFailWith);
199                 enum singleThreaded = hasUDA!(eLtEstO, Serial);
200                 enum builtin = true;
201                 enum suffix = "";
202 
203                 // let's check for @Values UDAs, which are actually of type ValuesImpl
204                 enum isValues(alias T) = is(typeof(T)) && is(typeof(T):ValuesImpl!U, U);
205                 alias valuesUDAs = Filter!(isValues, __traits(getAttributes, eLtEstO));
206 
207                 enum isTags(alias T) = is(typeof(T)) && is(typeof(T) == Tags);
208                 enum tags = tagsFromAttrs!(Filter!(isTags, __traits(getAttributes, eLtEstO)));
209                 enum exceptionTypeInfo = getExceptionTypeInfo!eLtEstO;
210                 enum flakyRetries = getFlakyRetries!(eLtEstO);
211 
212                 static if(valuesUDAs.length == 0) {
213                     testData ~= TestData(name,
214                                          () {
215                                              auto setup = getUDAFunction!(composite, Setup);
216                                              auto shutdown = getUDAFunction!(composite, Shutdown);
217 
218                                              if(setup) setup();
219                                              scope(exit) if(shutdown) shutdown();
220 
221                                              eLtEstO();
222                                          },
223                                          hidden,
224                                          shouldFail,
225                                          singleThreaded,
226                                          builtin,
227                                          suffix,
228                                          tags,
229                                          exceptionTypeInfo,
230                                          flakyRetries);
231                 } else {
232                     import std.range;
233 
234                     // cartesianProduct doesn't work with only one range, so in the usual case
235                     // of only one @Values UDA, we bind to prod with a range of tuples, just
236                     // as returned by cartesianProduct.
237 
238                     static if(valuesUDAs.length == 1) {
239                         import std.typecons;
240                         enum prod = valuesUDAs[0].values.map!(a => tuple(a));
241                     } else {
242                         mixin(`enum prod = cartesianProduct(` ~ valuesUDAs.length.iota.map!
243                               (a => `valuesUDAs[` ~ guaranteedToString(a) ~ `].values`).join(", ") ~ `);`);
244                     }
245 
246                     foreach(comb; aliasSeqOf!prod) {
247                         enum valuesName = valuesName(comb);
248 
249                         static if(hasUDA!(eLtEstO, AutoTags))
250                             enum realTags = tags ~ valuesName.split(".").array;
251                         else
252                             enum realTags = tags;
253 
254                         testData ~= TestData(name ~ "." ~ valuesName,
255                                              () {
256                                                  foreach(i; aliasSeqOf!(comb.length.iota))
257                                                      ValueHolder!(typeof(comb[i])).values[i] = comb[i];
258                                                  eLtEstO();
259                                              },
260                                              hidden,
261                                              shouldFail,
262                                              singleThreaded,
263                                              builtin,
264                                              suffix,
265                                              realTags,
266                                              exceptionTypeInfo,
267                                              flakyRetries);
268                     }
269                 }
270             }
271         }
272     }
273 
274 
275     // Keeps track of mangled names of everything visited.
276     bool[string] visitedMembers;
277 
278     void addUnitTestsRecursively(alias composite)() pure nothrow {
279         import std.traits: fullyQualifiedName;
280 
281         mixin("import " ~ fullyQualifiedName!module_ ~ ";"); //so it's visible
282 
283         if (composite.mangleof in visitedMembers)
284             return;
285         visitedMembers[composite.mangleof] = true;
286         addMemberUnittests!composite();
287         foreach(member; __traits(allMembers, composite)){
288             enum notPrivate = __traits(compiles, mixin(member)); //only way I know to check if private
289             static if (
290                 notPrivate &&
291                 // If visibility of the member is deprecated, the next line still returns true
292                 // and yet spills deprecation warning. If deprecation is turned into error,
293                 // all works as intended.
294                 __traits(compiles, __traits(getMember, composite, member)) &&
295                 __traits(compiles, __traits(allMembers, __traits(getMember, composite, member))) &&
296                 __traits(compiles, recurse!(__traits(getMember, composite, member)))
297             ) {
298                 recurse!(__traits(getMember, composite, member));
299             }
300         }
301     }
302 
303     void recurse(child)() pure nothrow {
304         enum notPrivate = __traits(compiles, child.init); //only way I know to check if private
305         static if (is(child == class) || is(child == struct) || is(child == union)) {
306             addUnitTestsRecursively!child;
307         }
308     }
309 
310     addUnitTestsRecursively!module_();
311     return testData;
312 }
313 
314 private TypeInfo getExceptionTypeInfo(alias Test)() {
315     import unit_threaded.uda: hasUtUDA, getUtUDAs;
316     import unit_threaded.attrs: ShouldFailWith;
317 
318     static if(hasUtUDA!(Test, ShouldFailWith)) {
319         alias uda = getUtUDAs!(Test, ShouldFailWith)[0];
320         return typeid(uda.Type);
321     } else
322         return null;
323 }
324 
325 
326 private string valuesName(T)(T tuple) {
327     import std.range: iota;
328     import std.meta: aliasSeqOf;
329     import std.array: join;
330 
331     string[] parts;
332     foreach(a; aliasSeqOf!(tuple.length.iota))
333         parts ~= guaranteedToString(tuple[a]);
334     return parts.join(".");
335 }
336 
337 private string guaranteedToString(T)(T value) nothrow pure @safe {
338     import std.conv;
339     try
340         return value.to!string;
341     catch(Exception ex)
342         assert(0, "Could not convert value to string");
343 }
344 
345 private string getValueAsString(T)(T value) nothrow pure @safe {
346     import std.conv;
347     try
348         return value.to!string;
349     catch(Exception ex)
350         assert(0, "Could not convert value to string");
351 }
352 
353 
354 private template isStringUDA(alias T) {
355     import std.traits: isSomeString;
356     static if(__traits(compiles, isSomeString!(typeof(T))))
357         enum isStringUDA = isSomeString!(typeof(T));
358     else
359         enum isStringUDA = false;
360 }
361 
362 @safe pure unittest {
363     static assert(isStringUDA!"foo");
364     static assert(!isStringUDA!5);
365 }
366 
367 private template isPrivate(alias module_, string moduleMember) {
368     import unit_threaded.uda: HasTypes;
369 
370     alias ut_mmbr__ = Identity!(__traits(getMember, module_, moduleMember));
371 
372     static if(__traits(compiles, isSomeFunction!(ut_mmbr__))) {
373         static if(__traits(compiles, &ut_mmbr__))
374             enum isPrivate = false;
375         else static if(__traits(compiles, new ut_mmbr__))
376             enum isPrivate = false;
377         else static if(__traits(compiles, HasTypes!ut_mmbr__))
378             enum isPrivate = !HasTypes!ut_mmbr__;
379         else
380             enum isPrivate = true;
381     } else {
382         enum isPrivate = true;
383     }
384 }
385 
386 
387 // if this member is a test function or class, given the predicate
388 private template PassesTestPred(alias module_, alias pred, string moduleMember) {
389     import std.traits: fullyQualifiedName;
390     import unit_threaded.meta: importMember;
391     import unit_threaded.uda: HasAttribute;
392     import unit_threaded.attrs: DontTest;
393 
394     //should be the line below instead but a compiler bug prevents it
395     //mixin(importMember!module_(moduleMember));
396     mixin("import " ~ fullyQualifiedName!module_ ~ ";");
397     alias I(T...) = T;
398     static if(!__traits(compiles, I!(__traits(getMember, module_, moduleMember)))) {
399         enum PassesTestPred = false;
400     } else {
401         alias member = I!(__traits(getMember, module_, moduleMember));
402 
403         template canCheckIfSomeFunction(T...) {
404             enum canCheckIfSomeFunction = T.length == 1 && __traits(compiles, isSomeFunction!(T[0]));
405         }
406 
407         private string funcCallMixin(alias T)() {
408             import std.conv: to;
409             string[] args;
410             foreach(i, ParamType; Parameters!T) {
411                 args ~= `arg` ~ i.to!string;
412             }
413 
414             return moduleMember ~ `(` ~ args.join(`,`) ~ `);`;
415         }
416 
417         private string argsMixin(alias T)() {
418             import std.conv: to;
419             string[] args;
420             foreach(i, ParamType; Parameters!T) {
421                 args ~= ParamType.stringof ~ ` arg` ~ i.to!string ~ `;`;
422             }
423 
424             return args.join("\n");
425         }
426 
427         template canCallMember() {
428             void _f() {
429                 mixin(argsMixin!member);
430                 mixin(funcCallMixin!member);
431             }
432         }
433 
434         template canInstantiate() {
435             void _f() {
436                 mixin(`auto _ = new ` ~ moduleMember ~ `;`);
437             }
438         }
439 
440         template isPrivate() {
441             static if(!canCheckIfSomeFunction!member) {
442                 enum isPrivate = !__traits(compiles, __traits(getMember, module_, moduleMember));
443             } else {
444                 static if(isSomeFunction!member) {
445                     enum isPrivate = !__traits(compiles, canCallMember!());
446                 } else static if(is(member)) {
447                     static if(isAggregateType!member)
448                         enum isPrivate = !__traits(compiles, canInstantiate!());
449                     else
450                         enum isPrivate = !__traits(compiles, __traits(getMember, module_, moduleMember));
451                 } else {
452                     enum isPrivate = !__traits(compiles, __traits(getMember, module_, moduleMember));
453                 }
454             }
455         }
456 
457         enum notPrivate = !isPrivate!();
458         enum PassesTestPred = !isPrivate!() && pred!(module_, moduleMember) &&
459             !HasAttribute!(module_, moduleMember, DontTest);
460     }
461 }
462 
463 
464 /**
465  * Finds all test classes (classes implementing a test() function)
466  * in the given module
467  */
468 TestData[] moduleTestClasses(alias module_)() pure nothrow {
469 
470     template isTestClass(alias module_, string moduleMember) {
471         import unit_threaded.meta: importMember;
472         import unit_threaded.uda: HasAttribute;
473         import unit_threaded.attrs: UnitTest;
474         import std.traits: isAggregateType;
475 
476         alias member = Identity!(__traits(getMember, module_, moduleMember));
477 
478         static if(.isPrivate!(module_, moduleMember)) {
479             enum isTestClass = false;
480         } else static if(!__traits(compiles, isAggregateType!(member))) {
481             enum isTestClass = false;
482         } else static if(!isAggregateType!(member)) {
483             enum isTestClass = false;
484         } else static if(!__traits(compiles, { return new member; })) {
485             enum isTestClass = false; //can't new it, can't use it
486         } else {
487             enum hasUnitTest = HasAttribute!(module_, moduleMember, UnitTest);
488             enum hasTestMethod = __traits(hasMember, member, "test");
489 
490             enum isTestClass = is(member == class) && (hasTestMethod || hasUnitTest);
491         }
492     }
493 
494     return moduleTestData!(module_, isTestClass, memberTestData);
495 }
496 
497 
498 /**
499  * Finds all test functions in the given module.
500  * Returns an array of TestData structs
501  */
502 TestData[] moduleTestFunctions(alias module_)() pure {
503 
504     import unit_threaded.uda: isTypesAttr;
505 
506     template isTestFunction(alias module_, string moduleMember) {
507         import unit_threaded.meta: importMember;
508         import unit_threaded.attrs: UnitTest;
509         import unit_threaded.uda: HasAttribute, GetTypes;
510         import std.meta: AliasSeq;
511         import std.traits: isSomeFunction;
512 
513         alias member = Identity!(__traits(getMember, module_, moduleMember));
514 
515         static if(.isPrivate!(module_, moduleMember)) {
516             enum isTestFunction = false;
517         } else static if(AliasSeq!(member).length != 1) {
518             enum isTestFunction = false;
519         } else static if(isSomeFunction!member) {
520             enum isTestFunction = hasTestPrefix!(module_, moduleMember) ||
521                                   HasAttribute!(module_, moduleMember, UnitTest);
522         } else static if(__traits(compiles, __traits(getAttributes, member))) {
523             // in this case we handle the possibility of a template function with
524             // the @Types UDA attached to it
525             alias types = GetTypes!member;
526             enum isTestFunction = hasTestPrefix!(module_, moduleMember) &&
527                                   types.length > 0;
528         } else {
529             enum isTestFunction = false;
530         }
531 
532     }
533 
534     template hasTestPrefix(alias module_, string memberName) {
535         import std.uni: isUpper;
536         import unit_threaded.meta: importMember;
537 
538         alias member = Identity!(__traits(getMember, module_, memberName));
539 
540         enum prefix = "test";
541         enum minSize = prefix.length + 1;
542 
543         static if(memberName.length >= minSize &&
544                   memberName[0 .. prefix.length] == prefix &&
545                   isUpper(memberName[prefix.length])) {
546             enum hasTestPrefix = true;
547         } else {
548             enum hasTestPrefix = false;
549         }
550     }
551 
552     return moduleTestData!(module_, isTestFunction, createFuncTestData);
553 }
554 
555 private TestData[] createFuncTestData(alias module_, string moduleMember)() {
556     import unit_threaded.meta: importMember;
557     import unit_threaded.uda: GetAttributes, HasAttribute, GetTypes, HasTypes;
558     import unit_threaded.attrs;
559     import std.meta: aliasSeqOf;
560 
561     mixin(importMember!module_(moduleMember));
562     /*
563       Get all the test functions for this module member. There might be more than one
564       when using parametrized unit tests.
565 
566       Examples:
567       ------
568       void testFoo() {} // -> the array contains one element, testFoo
569       @(1, 2, 3) void testBar(int) {} // The array contains 3 elements, one for each UDA value
570       @Types!(int, float) void testBaz(T)() {} //The array contains 2 elements, one for each type
571       ------
572     */
573     // if the predicate returned true (which is always the case here), then it's either
574     // a regular function or a templated one. If regular we can get a pointer to it
575     enum isRegularFunction = __traits(compiles, &__traits(getMember, module_, moduleMember));
576 
577     static if(isRegularFunction) {
578 
579         enum func = &__traits(getMember, module_, moduleMember);
580         enum arity = arity!func;
581 
582         static if(arity == 0)
583             // the reason we're creating a lambda to call the function is that test functions
584             // are ordinary functions, but we're storing delegates
585             return [ memberTestData!(module_, moduleMember)(() { func(); }) ]; //simple case, just call the function
586         else {
587 
588             // the function has parameters, check if it has UDAs for value parameters to be passed to it
589             alias params = Parameters!func;
590 
591             import std.range: iota;
592             import std.algorithm: any;
593             import std.typecons: tuple, Tuple;
594 
595             bool hasAttributesForAllParams() {
596                 auto ret = true;
597                 foreach(p; params) {
598                     if(tuple(GetAttributes!(module_, moduleMember, p)).length == 0) {
599                         ret = false;
600                     }
601                 }
602                 return ret;
603             }
604 
605             static if(!hasAttributesForAllParams) {
606                 import std.conv: text;
607                 pragma(msg, text("Warning: ", moduleMember, " passes the criteria for a value-parameterized test function",
608                                  " but doesn't have the appropriate value UDAs.\n",
609                                  "         Consider changing its name or annotating it with @DontTest"));
610                 return [];
611             } else {
612 
613                 static if(arity == 1) {
614                     // bind a range of tuples to prod just as cartesianProduct returns
615                     enum prod = [GetAttributes!(module_, moduleMember, params[0])].map!(a => tuple(a));
616                 } else {
617                     import std.conv: text;
618 
619                     mixin(`enum prod = cartesianProduct(` ~ params.length.iota.map!
620                           (a => `[GetAttributes!(module_, moduleMember, params[` ~ guaranteedToString(a) ~ `])]`).join(", ") ~ `);`);
621                 }
622 
623                 TestData[] testData;
624                 foreach(comb; aliasSeqOf!prod) {
625                     enum valuesName = valuesName(comb);
626 
627                     static if(HasAttribute!(module_, moduleMember, AutoTags))
628                         enum extraTags = valuesName.split(".").array;
629                     else
630                         enum string[] extraTags = [];
631 
632 
633                     testData ~= memberTestData!(module_, moduleMember, extraTags)(
634                         // func(value0, value1, ...)
635                         () { func(comb.expand); },
636                         valuesName);
637                 }
638 
639                 return testData;
640             }
641         }
642     } else static if(HasTypes!(mixin(moduleMember))) { //template function with @Types
643         alias types = GetTypes!(mixin(moduleMember));
644         TestData[] testData;
645         foreach(type; types) {
646 
647             static if(HasAttribute!(module_, moduleMember, AutoTags))
648                 enum extraTags = [type.stringof];
649             else
650                 enum string[] extraTags = [];
651 
652             alias member = Identity!(mixin(moduleMember));
653 
654             testData ~= memberTestData!(module_, moduleMember, extraTags)(
655                 () { member!type(); },
656                 type.stringof);
657         }
658         return testData;
659     } else {
660         return [];
661     }
662 }
663 
664 
665 
666 // this funtion returns TestData for either classes or test functions
667 // built-in unittest modules are handled by moduleUnitTests
668 // pred determines what qualifies as a test
669 // createTestData must return TestData[]
670 private TestData[] moduleTestData(alias module_, alias pred, alias createTestData)() pure {
671     import std.traits: fullyQualifiedName;
672     mixin("import " ~ fullyQualifiedName!module_ ~ ";"); //so it's visible
673 
674     TestData[] testData;
675 
676     foreach(moduleMember; __traits(allMembers, module_)) {
677 
678         static if(PassesTestPred!(module_, pred, moduleMember))
679             testData ~= createTestData!(module_, moduleMember);
680     }
681 
682     return testData;
683 
684 }
685 
686 // TestData for a member of a module (either a test function or test class)
687 private TestData memberTestData(alias module_, string moduleMember, string[] extraTags = [])
688     (TestFunction testFunction = null, string suffix = "") {
689 
690     import unit_threaded.uda: HasAttribute, GetAttributes, hasUtUDA;
691     import unit_threaded.attrs;
692     import std.traits: fullyQualifiedName;
693 
694     mixin("import " ~ fullyQualifiedName!module_ ~ ";"); //so it's visible
695 
696     immutable singleThreaded = HasAttribute!(module_, moduleMember, Serial);
697     enum builtin = false;
698     enum tags = tagsFromAttrs!(GetAttributes!(module_, moduleMember, Tags));
699     enum exceptionTypeInfo = getExceptionTypeInfo!(mixin(moduleMember));
700     enum shouldFail =
701         HasAttribute!(module_, moduleMember, ShouldFail) ||
702         hasUtUDA!(mixin(moduleMember), ShouldFailWith);
703     enum flakyRetries = getFlakyRetries!(mixin(moduleMember));
704 
705     return TestData(fullyQualifiedName!module_~ "." ~ moduleMember,
706                     testFunction,
707                     HasAttribute!(module_, moduleMember, HiddenTest),
708                     shouldFail,
709                     singleThreaded,
710                     builtin,
711                     suffix,
712                     tags ~ extraTags,
713                     exceptionTypeInfo,
714                     flakyRetries);
715 }
716 
717 private int getFlakyRetries(alias test)() {
718     import unit_threaded.attrs: Flaky;
719     import std.traits: getUDAs;
720     import std.conv: text;
721 
722     alias flakies = getUDAs!(test, Flaky);
723 
724     static assert(flakies.length == 0 || flakies.length == 1,
725                   text("Only 1 @Flaky allowed, found ", flakies.length, " on ",
726                        __traits(identifier, test)));
727 
728     static if(flakies.length == 1) {
729         static if(is(flakies[0]))
730             return Flaky.defaultRetries;
731         else
732             return flakies[0].retries;
733     } else
734         return 0;
735 }
736 
737 string[] tagsFromAttrs(T...)() {
738     static assert(T.length <= 1, "@Tags can only be applied once");
739     static if(T.length)
740         return T[0].values;
741     else
742         return [];
743 }