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