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