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