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