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 {
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             enum isTestFunction = hasTestPrefix!(module_, moduleMember) &&
318                                   types.length > 0 &&
319                                   is(typeof(() {
320                                       mixin(moduleMember ~ `!` ~ types[0].stringof ~ `;`);
321                                   }));
322         }
323     }
324 
325     template hasTestPrefix(alias module_, string member) {
326         import std.uni: isUpper;
327         mixin(importMember!module_(member));
328 
329         enum prefix = "test";
330         enum minSize = prefix.length + 1;
331 
332         static if(member.length >= minSize && member[0 .. prefix.length] == prefix &&
333                   isUpper(member[prefix.length])) {
334             enum hasTestPrefix = true;
335         } else {
336             enum hasTestPrefix = false;
337         }
338     }
339 
340 
341     return moduleTestData!(module_, isTestFunction, createFuncTestData);
342 }
343 
344 private TestData[] createFuncTestData(alias module_, string moduleMember)() {
345     mixin(importMember!module_(moduleMember));
346     /*
347       Get all the test functions for this module member. There might be more than one
348       when using parametrized unit tests.
349 
350       Examples:
351       ------
352       void testFoo() {} // -> the array contains one element, testFoo
353       @(1, 2, 3) void testBar(int) {} // The array contains 3 elements, one for each UDA value
354       @Types!(int, float) void testBaz(T)() {} //The array contains 2 elements, one for each type
355       ------
356     */
357     // if the predicate returned true (which is always the case here), then it's either
358     // a regular function or a templated one. If regular is has a pointer to it
359     enum isRegularFunction = __traits(compiles, &__traits(getMember, module_, moduleMember));
360 
361     static if(isRegularFunction) {
362 
363         enum func = &__traits(getMember, module_, moduleMember);
364         enum arity = arity!func;
365 
366         static if(arity == 0)
367             // the reason we're creating a lambda to call the function is that test functions
368             // are ordinary functions, but we're storing delegates
369             return [ memberTestData!(module_, moduleMember)(() { func(); }) ]; //simple case, just call the function
370         else {
371 
372             // the function has parameters, check if it has UDAs for value parameters to be passed to it
373             alias params = Parameters!func;
374 
375             static if(arity == 1) {
376                 import std.typecons;
377                 // bind a range of tuples to prod just as cartesianProduct returns
378                 enum prod = [GetAttributes!(module_, moduleMember, params[0])].map!(a => tuple(a));
379             } else {
380                 import std.range;
381                 mixin(`enum prod = cartesianProduct(` ~ params.length.iota.map!
382                       (a => `[GetAttributes!(module_, moduleMember, params[` ~ guaranteedToString(a) ~ `])]`).join(", ") ~ `);`);
383             }
384 
385             TestData[] testData;
386             foreach(comb; aliasSeqOf!prod) {
387                 enum valuesName = valuesName(comb);
388 
389                 static if(HasAttribute!(module_, moduleMember, AutoTags))
390                     enum extraTags = valuesName.split(".").array;
391                 else
392                     enum string[] extraTags = [];
393 
394 
395                 testData ~= memberTestData!(module_, moduleMember, extraTags)(
396                     // func(value0, value1, ...)
397                     () { func(comb.expand); },
398                     valuesName);
399             }
400 
401             return testData;
402         }
403     } else static if(HasTypes!(mixin(moduleMember))) { //template function with @Types
404         alias types = GetTypes!(mixin(moduleMember));
405         TestData[] testData;
406         foreach(type; types) {
407             static if(HasAttribute!(module_, moduleMember, AutoTags))
408                 enum extraTags = [type.stringof];
409             else
410                 enum string[] extraTags = [];
411 
412             testData ~= memberTestData!(module_, moduleMember, extraTags)(
413                 () {
414                     mixin(moduleMember ~ `!(` ~ type.stringof ~ `)();`);
415                 },
416                 type.stringof);
417         }
418         return testData;
419     } else {
420         return [];
421     }
422 }
423 
424 
425 
426 // this funtion returns TestData for either classes or test functions
427 // built-in unittest modules are handled by moduleUnitTests
428 // pred determines what qualifies as a test
429 // createTestData must return TestData[]
430 private TestData[] moduleTestData(alias module_, alias pred, alias createTestData)() pure {
431     mixin("import " ~ fullyQualifiedName!module_ ~ ";"); //so it's visible
432     TestData[] testData;
433     foreach(moduleMember; __traits(allMembers, module_)) {
434 
435         static if(PassesTestPred!(module_, pred, moduleMember))
436             testData ~= createTestData!(module_, moduleMember);
437     }
438 
439     return testData;
440 
441 }
442 
443 // TestData for a member of a module (either a test function or test class)
444 private TestData memberTestData(alias module_, string moduleMember, string[] extraTags = [])
445     (TestFunction testFunction = null, string suffix = "") {
446     immutable singleThreaded = HasAttribute!(module_, moduleMember, Serial);
447     enum builtin = false;
448     enum tags = tagsFromAttrs!(GetAttributes!(module_, moduleMember, Tags));
449 
450     return TestData(fullyQualifiedName!module_~ "." ~ moduleMember,
451                     testFunction,
452                     HasAttribute!(module_, moduleMember, HiddenTest),
453                     HasAttribute!(module_, moduleMember, ShouldFail),
454                     singleThreaded,
455                     builtin,
456                     suffix,
457                     tags ~ extraTags);
458 }
459 
460 string[] tagsFromAttrs(T...)() {
461     static assert(T.length <= 1, "@Tags can only be applied once");
462     static if(T.length)
463         return T[0].values;
464     else
465         return [];
466 }
467 
468 version(unittest) {
469 
470     import unit_threaded.tests.module_with_tests; //defines tests and non-tests
471     import unit_threaded.asserts;
472     import std.algorithm;
473     import std.array;
474 
475     //helper function for the unittest blocks below
476     private auto addModPrefix(string[] elements,
477                               string module_ = "unit_threaded.tests.module_with_tests") nothrow {
478         return elements.map!(a => module_ ~ "." ~ a).array;
479     }
480 }
481 
482 unittest {
483     const expected = addModPrefix([ "FooTest", "BarTest", "Blergh"]);
484     const actual = moduleTestClasses!(unit_threaded.tests.module_with_tests).
485         map!(a => a.name).array;
486     assertEqual(actual, expected);
487 }
488 
489 unittest {
490     const expected = addModPrefix([ "testFoo", "testBar", "funcThatShouldShowUpCosOfAttr"]);
491     const actual = moduleTestFunctions!(unit_threaded.tests.module_with_tests).
492         map!(a => a.getPath).array;
493     assertEqual(actual, expected);
494 }
495 
496 
497 unittest {
498     const expected = addModPrefix(["unittest0", "unittest1", "myUnitTest",
499                                    "StructWithUnitTests.InStruct", "StructWithUnitTests.unittest1"]);
500     const actual = moduleUnitTests!(unit_threaded.tests.module_with_tests).
501         map!(a => a.name).array;
502     assertEqual(actual, expected);
503 }
504 
505 version(unittest) {
506     import unit_threaded.testcase: TestCase;
507     private void assertFail(TestCase test, string file = __FILE__, ulong line = __LINE__) {
508         import core.exception;
509         import std.conv;
510 
511         try {
512             test.silence;
513             assert(test() != [], file ~ ":" ~ line.to!string ~ " Test was expected to fail but didn't");
514             assert(false, file ~ ":" ~ line.to!string ~ " Expected test case " ~ test.getPath ~
515                    " to fail with AssertError but it didn't");
516         } catch(AssertError) {}
517     }
518 
519     private void assertPass(TestCase test, string file = __FILE__, ulong line = __LINE__) {
520         assertEqual(test(), [], file, line);
521     }
522 }
523 
524 @("Test that parametrized value tests work")
525 unittest {
526     import unit_threaded.factory;
527     import unit_threaded.testcase;
528 
529     const testData = allTestData!(unit_threaded.tests.parametrized).
530         filter!(a => a.name.endsWith("testValues")).array;
531 
532     auto tests = createTestCases(testData);
533     assertEqual(tests.length, 3);
534 
535     // the first and third test should pass, the second should fail
536     assertPass(tests[0]);
537     assertPass(tests[2]);
538 
539     assertFail(tests[1]);
540 }
541 
542 
543 @("Test that parametrized type tests work")
544 unittest {
545     import unit_threaded.factory;
546     import unit_threaded.testcase;
547 
548     const testData = allTestData!(unit_threaded.tests.parametrized).
549         filter!(a => a.name.endsWith("testTypes")).array;
550     const expected = addModPrefix(["testTypes.float", "testTypes.int"],
551                                   "unit_threaded.tests.parametrized");
552     const actual = testData.map!(a => a.getPath).array;
553     assertEqual(actual, expected);
554 
555     auto tests = createTestCases(testData);
556     assertEqual(tests.map!(a => a.getPath).array, expected);
557 
558     assertPass(tests[1]);
559     assertFail(tests[0]);
560 }
561 
562 @("Value parametrized built-in unittests")
563 unittest {
564     import unit_threaded.factory;
565     import unit_threaded.testcase;
566 
567     const testData = allTestData!(unit_threaded.tests.parametrized).
568         filter!(a => a.name.canFind("builtinIntValues")).array;
569 
570     auto tests = createTestCases(testData);
571     assertEqual(tests.length, 4);
572 
573     // these should be ok
574     assertPass(tests[1]);
575 
576     //these should fail
577     assertFail(tests[0]);
578     assertFail(tests[2]);
579     assertFail(tests[3]);
580 }
581 
582 
583 @("Tests can be selected by tags") unittest {
584     import unit_threaded.factory;
585     import unit_threaded.testcase;
586 
587     const testData = allTestData!(unit_threaded.tests.tags).array;
588     auto testsNoTags = createTestCases(testData);
589     assertEqual(testsNoTags.length, 4);
590     assertPass(testsNoTags[0]);
591     assertFail(testsNoTags[1]);
592     assertFail(testsNoTags[2]);
593     assertFail(testsNoTags[3]);
594 
595     auto testsNinja = createTestCases(testData, ["@ninja"]);
596     assertEqual(testsNinja.length, 1);
597     assertPass(testsNinja[0]);
598 
599     auto testsMake = createTestCases(testData, ["@make"]);
600     assertEqual(testsMake.length, 3);
601     assertPass(testsMake.find!(a => a.getPath.canFind("testMake")).front);
602     assertPass(testsMake.find!(a => a.getPath.canFind("unittest0")).front);
603     assertFail(testsMake.find!(a => a.getPath.canFind("unittest2")).front);
604 
605     auto testsNotNinja = createTestCases(testData, ["~@ninja"]);
606     assertEqual(testsNotNinja.length, 3);
607     assertPass(testsNotNinja.find!(a => a.getPath.canFind("testMake")).front);
608     assertFail(testsNotNinja.find!(a => a.getPath.canFind("unittest1")).front);
609     assertFail(testsNotNinja.find!(a => a.getPath.canFind("unittest2")).front);
610 
611     assertEqual(createTestCases(testData, ["unit_threaded.tests.tags.testMake", "@ninja"]).length, 0);
612 }
613 
614 @("Parametrized built-in tests with @AutoTags get tagged by value")
615 unittest {
616     import unit_threaded.factory;
617     import unit_threaded.testcase;
618 
619     const testData = allTestData!(unit_threaded.tests.parametrized).
620         filter!(a => a.name.canFind("builtinIntValues")).array;
621 
622     auto two = createTestCases(testData, ["@2"]);
623 
624     assertEqual(two.length, 1);
625     assertFail(two[0]);
626 
627     auto three = createTestCases(testData, ["@3"]);
628     assertEqual(three.length, 1);
629     assertPass(three[0]);
630 }
631 
632 @("Value parametrized function tests with @AutoTags get tagged by value")
633 unittest {
634     import unit_threaded.factory;
635     import unit_threaded.testcase;
636 
637     const testData = allTestData!(unit_threaded.tests.parametrized).
638         filter!(a => a.name.canFind("testValues")).array;
639 
640     auto two = createTestCases(testData, ["@2"]);
641     assertEqual(two.length, 1);
642     assertFail(two[0]);
643 }
644 
645 @("Type parameterized tests with @AutoTags get tagged by type")
646 unittest {
647     import unit_threaded.factory;
648     import unit_threaded.testcase;
649 
650     const testData = allTestData!(unit_threaded.tests.parametrized).
651         filter!(a => a.name.canFind("testTypes")).array;
652 
653     auto tests = createTestCases(testData, ["@int"]);
654     assertEqual(tests.length, 1);
655     assertPass(tests[0]);
656 }
657 
658 @("Cartesian parameterized built-in values") unittest {
659     import unit_threaded.factory;
660     import unit_threaded.testcase;
661     import unit_threaded.should;
662 
663     const testData = allTestData!(unit_threaded.tests.parametrized).
664         filter!(a => a.name.canFind("cartesianBuiltinNoAutoTags")).array;
665 
666     auto tests = createTestCases(testData);
667     tests.map!(a => a.getPath).array.shouldBeSameSetAs(
668                 addModPrefix(["foo.red", "foo.blue", "foo.green", "bar.red", "bar.blue", "bar.green"].
669                              map!(a => "cartesianBuiltinNoAutoTags." ~ a).array,
670                              "unit_threaded.tests.parametrized"));
671     assertEqual(tests.length, 6);
672 
673     auto fooRed = tests.find!(a => a.getPath.canFind("foo.red")).front;
674     assertPass(fooRed);
675     assertEqual(getValue!(string, 0), "foo");
676     assertEqual(getValue!(string, 1), "red");
677     assertEqual(testData.find!(a => a.getPath.canFind("foo.red")).front.tags, []);
678 
679     auto barGreen = tests.find!(a => a.getPath.canFind("bar.green")).front;
680     assertFail(barGreen);
681     assertEqual(getValue!(string, 0), "bar");
682     assertEqual(getValue!(string, 1), "green");
683 
684     assertEqual(testData.find!(a => a.getPath.canFind("bar.green")).front.tags, []);
685     assertEqual(allTestData!(unit_threaded.tests.parametrized).
686                 filter!(a => a.name.canFind("cartesianBuiltinAutoTags")).array.
687                 find!(a => a.getPath.canFind("bar.green")).front.tags,
688                 ["bar", "green"]);
689 }
690 
691 @("Cartesian parameterized function values") unittest {
692     import unit_threaded.factory;
693     import unit_threaded.testcase;
694     import unit_threaded.should;
695 
696     const testData = allTestData!(unit_threaded.tests.parametrized).
697         filter!(a => a.name.canFind("CartesianFunction")).array;
698 
699     auto tests = createTestCases(testData);
700         tests.map!(a => a.getPath).array.shouldBeSameSetAs(
701             addModPrefix(["1.foo", "1.bar", "2.foo", "2.bar", "3.foo", "3.bar"].
702                              map!(a => "testCartesianFunction." ~ a).array,
703                              "unit_threaded.tests.parametrized"));
704 
705     foreach(test; tests) {
706         test.getPath.canFind("2.bar")
707             ? assertPass(test)
708             : assertFail(test);
709     }
710 
711     assertEqual(testData.find!(a => a.getPath.canFind("2.bar")).front.tags,
712                 ["2", "bar"]);
713 
714 }