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  * @return An array of TestData structs
78  */
79 TestData[] moduleUnitTests(alias module_)() pure nothrow {
80 
81     // Return a name for a unittest block. If no @Name UDA is found a name is
82     // created automatically, else the UDA is used.
83     string unittestName(alias test, int index)() @safe nothrow {
84         import std.conv;
85         mixin("import " ~ fullyQualifiedName!module_ ~ ";"); //so it's visible
86 
87         enum nameAttrs = getUDAs!(test, Name);
88         static assert(nameAttrs.length == 0 || nameAttrs.length == 1, "Found multiple Name UDAs on unittest");
89 
90         enum strAttrs = Filter!(isStringUDA, __traits(getAttributes, test));
91         enum hasName = nameAttrs.length || strAttrs.length == 1;
92         enum prefix = fullyQualifiedName!module_ ~ ".";
93 
94         static if(hasName) {
95             static if(nameAttrs.length == 1)
96                 return prefix ~ nameAttrs[0].value;
97             else
98                 return prefix ~ strAttrs[0];
99         } else {
100             string name;
101             try {
102                 return prefix ~ "unittest" ~ (index).to!string;
103             } catch(Exception) {
104                 assert(false, text("Error converting ", index, " to string"));
105             }
106         }
107     }
108 
109     TestData[] testData;
110     foreach(index, test; __traits(getUnitTests, module_)) {
111         enum name = unittestName!(test, index);
112         enum hidden = hasUDA!(test, HiddenTest);
113         enum shouldFail = hasUDA!(test, ShouldFail);
114         enum singleThreaded = hasUDA!(test, Serial);
115         enum builtin = true;
116         enum suffix = "";
117 
118         // let's check for @Values UDAs, which are actually of type ValuesImpl
119         enum isValues(alias T) = is(typeof(T)) && is(typeof(T):ValuesImpl!U, U);
120         enum valuesUDAs = Filter!(isValues, __traits(getAttributes, test));
121 
122         enum isTags(alias T) = is(typeof(T)) && is(typeof(T) == Tags);
123         enum tags = tagsFromAttrs!(Filter!(isTags, __traits(getAttributes, test)));
124 
125         static if(valuesUDAs.length == 0) {
126             testData ~= TestData(name, (){ test(); }, hidden, shouldFail, singleThreaded, builtin, suffix, tags);
127         } else {
128             static assert(valuesUDAs.length == 1, "Can only use @Values once");
129 
130             foreach(value; aliasSeqOf!(valuesUDAs[0].values)) {
131                 // force single threaded so a composite test case is created
132                 // we set a global static to the value the test expects then call the test function,
133                 // which can retrieve the value with getValue!T
134 
135                 enum valueAsString = getValueAsString(value);
136 
137                 static if(hasUDA!(test, AutoTags))
138                     enum realTags = tags ~ valueAsString;
139                 else
140                     enum realTags = tags;
141 
142                 testData ~= TestData(name ~ "." ~ valueAsString,
143                                      () {
144                                          ValueHolder!(typeof(value)).value = value;
145                                          test();
146                                      },
147                                      hidden, shouldFail, true /*serial*/, builtin, suffix, realTags);
148             }
149         }
150     }
151     return testData;
152 }
153 
154 private string getValueAsString(T)(T value) nothrow pure @safe {
155     import std.conv;
156     try
157         return value.to!string;
158     catch(Exception ex)
159         assert(0, "Could not convert value to string");
160 }
161 
162 
163 private template isStringUDA(alias T) {
164     static if(__traits(compiles, isSomeString!(typeof(T))))
165         enum isStringUDA = isSomeString!(typeof(T));
166     else
167         enum isStringUDA = false;
168 }
169 
170 unittest {
171     static assert(isStringUDA!"foo");
172     static assert(!isStringUDA!5);
173 }
174 
175 private template isPrivate(alias module_, string moduleMember) {
176     mixin(`import ` ~ fullyQualifiedName!module_ ~ `: ` ~ moduleMember ~ `;`);
177     static if(__traits(compiles, isSomeFunction!(mixin(moduleMember)))) {
178         enum isPrivate = false;
179     } else {
180         enum isPrivate = true;
181     }
182 }
183 
184 
185 // if this member is a test function or class, given the predicate
186 private template PassesTestPred(alias module_, alias pred, string moduleMember) {
187     //should be the line below instead but a compiler bug prevents it
188     //mixin(importMember!module_(moduleMember));
189     mixin("import " ~ fullyQualifiedName!module_ ~ ";");
190     enum notPrivate = __traits(compiles, mixin(moduleMember)); //only way I know to check if private
191     //enum notPrivate = !isPrivate!(module_, moduleMember);
192     static if(notPrivate)
193         enum PassesTestPred = notPrivate && pred!(module_, moduleMember) &&
194                               !HasAttribute!(module_, moduleMember, DontTest);
195     else
196         enum PassesTestPred = false;
197 }
198 
199 
200 /**
201  * Finds all test classes (classes implementing a test() function)
202  * in the given module
203  */
204 TestData[] moduleTestClasses(alias module_)() pure nothrow {
205 
206     template isTestClass(alias module_, string moduleMember) {
207         mixin(importMember!module_(moduleMember));
208         static if(isPrivate!(module_, moduleMember)) {
209             enum isTestClass = false;
210         } else static if(!__traits(compiles, isAggregateType!(mixin(moduleMember)))) {
211             enum isTestClass = false;
212         } else static if(!isAggregateType!(mixin(moduleMember))) {
213             enum isTestClass = false;
214         } else static if(!__traits(compiles, mixin("new " ~ moduleMember))) {
215             enum isTestClass = false; //can't new it, can't use it
216         } else {
217             enum hasUnitTest = HasAttribute!(module_, moduleMember, UnitTest);
218             enum hasTestMethod = __traits(hasMember, mixin(moduleMember), "test");
219             enum isTestClass = hasTestMethod || hasUnitTest;
220         }
221     }
222 
223 
224     return moduleTestData!(module_, isTestClass, memberTestData);
225 }
226 
227 
228 /**
229  * Finds all test functions in the given module.
230  * Returns an array of TestData structs
231  */
232 TestData[] moduleTestFunctions(alias module_)() pure {
233 
234     enum isTypesAttr(alias T) = is(T) && is(T:Types!U, U...);
235 
236     template isTestFunction(alias module_, string moduleMember) {
237         mixin(importMember!module_(moduleMember));
238 
239         static if(isPrivate!(module_, moduleMember)) {
240             enum isTestFunction = false;
241         } else static if(AliasSeq!(mixin(moduleMember)).length != 1) {
242             enum isTestFunction = false;
243         } else static if(isSomeFunction!(mixin(moduleMember))) {
244             enum isTestFunction = hasTestPrefix!(module_, moduleMember) ||
245                                   HasAttribute!(module_, moduleMember, UnitTest);
246         } else {
247             // in this case we handle the possibility of a template function with
248             // the @Types UDA attached to it
249             alias types = GetTypes!(mixin(moduleMember));
250             enum isTestFunction = hasTestPrefix!(module_, moduleMember) &&
251                                   types.length > 0 &&
252                                   is(typeof(() {
253                                       mixin(moduleMember ~ `!` ~ types[0].stringof ~ `;`);
254                                   }));
255         }
256     }
257 
258     template hasTestPrefix(alias module_, string member) {
259         import std.uni: isUpper;
260         mixin(importMember!module_(member));
261 
262         enum prefix = "test";
263         enum minSize = prefix.length + 1;
264 
265         static if(member.length >= minSize && member[0 .. prefix.length] == prefix &&
266                   isUpper(member[prefix.length])) {
267             enum hasTestPrefix = true;
268         } else {
269             enum hasTestPrefix = false;
270         }
271     }
272 
273 
274     return moduleTestData!(module_, isTestFunction, createFuncTestData);
275 }
276 
277 private TestData[] createFuncTestData(alias module_, string moduleMember)() {
278     mixin(importMember!module_(moduleMember));
279     /*
280       Get all the test functions for this module member. There might be more than one
281       when using parametrized unit tests.
282 
283       Examples:
284       ------
285       void testFoo() {} // -> the array contains one element, testFoo
286       @(1, 2, 3) void testBar(int) {} // The array contains 3 elements, one for each UDA value
287       @Types!(int, float) void testBaz(T)() {} //The array contains 2 elements, one for each type
288       ------
289     */
290     // if the predicate returned true (which is always the case here), then it's either
291     // a regular function or a templated one. If regular is has a pointer to it
292     enum isRegularFunction = __traits(compiles, &__traits(getMember, module_, moduleMember));
293 
294     static if(isRegularFunction) {
295 
296         enum func = &__traits(getMember, module_, moduleMember);
297         enum arity = arity!func;
298 
299         static assert(arity == 0 || arity == 1, "Test functions may take at most one parameter");
300 
301         static if(arity == 0)
302             // the reason we're creating a lambda to call the function is that test functions
303             // are ordinary functions, but we're storing delegates
304             return [ memberTestData!(module_, moduleMember)(() { func(); }) ]; //simple case, just call the function
305         else {
306 
307             // the function takes a parameter, check if it has UDAs for value parameters to be passed to it
308             alias params = Parameters!func;
309             static assert(params.length == 1, "Test functions may take at most one parameter");
310 
311             alias values = GetAttributes!(module_, moduleMember, params[0]);
312 
313             import std.conv;
314             static assert(values.length > 0,
315                           text("Test functions with a parameter of type <", params[0].stringof,
316                                "> must have value UDAs of the same type"));
317 
318             TestData[] testData;
319             foreach(v; values) {
320                 static if(HasAttribute!(module_, moduleMember, AutoTags))
321                     enum extraTags = [getValueAsString(v)];
322                 else
323                     enum string[] extraTags = [];
324                 testData ~= memberTestData!(module_, moduleMember, extraTags)(
325                     () { func(v); },
326                     v.to!string
327                 );
328             }
329 
330             return testData;
331         }
332     } else static if(HasTypes!(mixin(moduleMember))) { //template function with @Types
333         alias types = GetTypes!(mixin(moduleMember));
334         TestData[] testData;
335         foreach(type; types) {
336             static if(HasAttribute!(module_, moduleMember, AutoTags))
337                 enum extraTags = [type.stringof];
338             else
339                 enum string[] extraTags = [];
340 
341             testData ~= memberTestData!(module_, moduleMember, extraTags)(
342                 () {
343                     mixin(moduleMember ~ `!(` ~ type.stringof ~ `)();`);
344                 },
345                 type.stringof);
346         }
347         return testData;
348     } else {
349         return [];
350     }
351 }
352 
353 
354 
355 // this funtion returns TestData for either classes or test functions
356 // built-in unittest modules are handled by moduleUnitTests
357 // pred determines what qualifies as a test
358 // createTestData must return TestData[]
359 private TestData[] moduleTestData(alias module_, alias pred, alias createTestData)() pure {
360     mixin("import " ~ fullyQualifiedName!module_ ~ ";"); //so it's visible
361     TestData[] testData;
362     foreach(moduleMember; __traits(allMembers, module_)) {
363 
364         static if(PassesTestPred!(module_, pred, moduleMember))
365             testData ~= createTestData!(module_, moduleMember);
366     }
367 
368     return testData;
369 
370 }
371 
372 // TestData for a member of a module (either a test function or test class)
373 private TestData memberTestData(alias module_, string moduleMember, string[] extraTags = [])
374     (TestFunction testFunction = null, string suffix = "") {
375     //if there is a suffix, all tests sharing that suffix are single threaded with multiple values per "real" test
376     //this is slightly hackish but works and actually makes sense - it causes unit_threaded.factory to make
377     //a CompositeTestCase out of them
378     immutable singleThreaded = HasAttribute!(module_, moduleMember, Serial) || suffix != "";
379     enum builtin = false;
380     enum tags = tagsFromAttrs!(GetAttributes!(module_, moduleMember, Tags));
381 
382     return TestData(fullyQualifiedName!module_~ "." ~ moduleMember,
383                     testFunction,
384                     HasAttribute!(module_, moduleMember, HiddenTest),
385                     HasAttribute!(module_, moduleMember, ShouldFail),
386                     singleThreaded,
387                     builtin,
388                     suffix,
389                     tags ~ extraTags);
390 }
391 
392 string[] tagsFromAttrs(T...)() {
393     static assert(T.length <= 1, "@Tags can only be applied once");
394     static if(T.length)
395         return T[0].values;
396     else
397         return [];
398 }
399 
400 version(unittest) {
401 
402     import unit_threaded.tests.module_with_tests; //defines tests and non-tests
403     import unit_threaded.asserts;
404     import std.algorithm;
405     import std.array;
406 
407     //helper function for the unittest blocks below
408     private auto addModPrefix(string[] elements,
409                               string module_ = "unit_threaded.tests.module_with_tests") nothrow {
410         return elements.map!(a => module_ ~ "." ~ a).array;
411     }
412 }
413 
414 unittest {
415     const expected = addModPrefix([ "FooTest", "BarTest", "Blergh"]);
416     const actual = moduleTestClasses!(unit_threaded.tests.module_with_tests).
417         map!(a => a.name).array;
418     assertEqual(actual, expected);
419 }
420 
421 unittest {
422     const expected = addModPrefix([ "testFoo", "testBar", "funcThatShouldShowUpCosOfAttr"]);
423     const actual = moduleTestFunctions!(unit_threaded.tests.module_with_tests).
424         map!(a => a.getPath).array;
425     assertEqual(actual, expected);
426 }
427 
428 
429 unittest {
430     const expected = addModPrefix(["unittest0", "unittest1", "myUnitTest"]);
431     const actual = moduleUnitTests!(unit_threaded.tests.module_with_tests).
432         map!(a => a.name).array;
433     assertEqual(actual, expected);
434 }
435 
436 version(unittest) {
437     import unit_threaded.testcase: TestCase;
438     private void assertFail(TestCase test, string file = __FILE__, ulong line = __LINE__) {
439         import core.exception;
440         import std.conv;
441 
442         try {
443             test.silence;
444             assert(test() != [], file ~ ":" ~ line.to!string ~ " Test was expected to fail but didn't");
445             assert(false, file ~ ":" ~ line.to!string ~ " Expected test case " ~ test.getPath ~
446                    " to fail with AssertError but it didn't");
447         } catch(AssertError) {}
448     }
449 
450     private void assertPass(TestCase test, string file = __FILE__, ulong line = __LINE__) {
451         assertEqual(test(), [], file, line);
452     }
453 }
454 
455 @("Test that parametrized value tests work")
456 unittest {
457     import unit_threaded.factory;
458     import unit_threaded.testcase;
459 
460     const testData = allTestData!(unit_threaded.tests.parametrized).
461         filter!(a => a.name.endsWith("testValues")).array;
462 
463     // there should only be on test case which is a composite of the 3 values in testValues
464     auto composite = cast(CompositeTestCase)createTestCases(testData)[0];
465     assert(composite !is null, "Wrong dynamic type for TestCase");
466     auto tests = composite.tests;
467     assertEqual(tests.length, 3);
468 
469     // the first and third test should pass, the second should fail
470     assertPass(tests[0]);
471     assertPass(tests[2]);
472 
473     assertFail(tests[1]);
474 }
475 
476 
477 @("Test that parametrized type tests work")
478 unittest {
479     import unit_threaded.factory;
480     import unit_threaded.testcase;
481 
482     const testData = allTestData!(unit_threaded.tests.parametrized).
483         filter!(a => a.name.endsWith("testTypes")).array;
484     const expected = addModPrefix(["testTypes.float", "testTypes.int"],
485                                   "unit_threaded.tests.parametrized");
486     const actual = testData.map!(a => a.getPath).array;
487     assertEqual(actual, expected);
488 
489     // there should only be on test case which is a composite of the 2 testTypes
490     auto composite = cast(CompositeTestCase)createTestCases(testData)[0];
491     assert(composite !is null, "Wrong dynamic type for TestCase");
492     auto tests = composite.tests;
493     assertEqual(tests.map!(a => a.getPath).array, expected);
494 
495     assertPass(tests[1]);
496     assertFail(tests[0]);
497 }
498 
499 @("Test that value parametrized built-in unittest blocks work")
500 unittest {
501     import unit_threaded.factory;
502     import unit_threaded.testcase;
503 
504     const testData = allTestData!(unit_threaded.tests.parametrized).
505         filter!(a => a.name.canFind("builtinIntValues")).array;
506 
507     // there should only be on test case which is a composite of the 4 values
508     auto composite = cast(CompositeTestCase)createTestCases(testData)[0];
509     assert(composite !is null, "Wrong dynamic type for TestCase");
510     auto tests = composite.tests;
511     assertEqual(tests.length, 4);
512 
513     // these should be ok
514     assertPass(tests[1]);
515 
516     //these should fail
517     assertFail(tests[0]);
518     assertFail(tests[2]);
519     assertFail(tests[3]);
520 }
521 
522 
523 @("Tests can be selected by tags") unittest {
524     import unit_threaded.factory;
525     import unit_threaded.testcase;
526 
527     const testData = allTestData!(unit_threaded.tests.tags).array;
528     auto testsNoTags = createTestCases(testData);
529     assertEqual(testsNoTags.length, 4);
530     assertPass(testsNoTags[0]);
531     assertFail(testsNoTags[1]);
532     assertFail(testsNoTags[2]);
533     assertFail(testsNoTags[3]);
534 
535     auto testsNinja = createTestCases(testData, ["@ninja"]);
536     assertEqual(testsNinja.length, 1);
537     assertPass(testsNinja[0]);
538 
539     auto testsMake = createTestCases(testData, ["@make"]);
540     assertEqual(testsMake.length, 3);
541     assertPass(testsMake.find!(a => a.getPath.canFind("testMake")).front);
542     assertPass(testsMake.find!(a => a.getPath.canFind("unittest0")).front);
543     assertFail(testsMake.find!(a => a.getPath.canFind("unittest2")).front);
544 
545     auto testsNotNinja = createTestCases(testData, ["~@ninja"]);
546     assertEqual(testsNotNinja.length, 3);
547     assertPass(testsNotNinja.find!(a => a.getPath.canFind("testMake")).front);
548     assertFail(testsNotNinja.find!(a => a.getPath.canFind("unittest1")).front);
549     assertFail(testsNotNinja.find!(a => a.getPath.canFind("unittest2")).front);
550 
551     assertEqual(createTestCases(testData, ["unit_threaded.tests.tags.testMake", "@ninja"]).length, 0);
552 }
553 
554 @("Parametrized built-in tests with @AutoTags get tagged by value")
555 unittest {
556     import unit_threaded.factory;
557     import unit_threaded.testcase;
558 
559     const testData = allTestData!(unit_threaded.tests.parametrized).
560         filter!(a => a.name.canFind("builtinIntValues")).array;
561 
562     auto compositeTwo = cast(CompositeTestCase)createTestCases(testData, ["@2"])[0];
563     assert(compositeTwo !is null, "Wrong dynamic type for TestCase");
564     auto two = compositeTwo.tests;
565 
566     assertEqual(two.length, 1);
567     assertFail(two[0]);
568 
569     auto compositeThree = cast(CompositeTestCase)createTestCases(testData, ["@3"])[0];
570     assert(compositeThree !is null, "Wrong dynamic type for TestCase");
571     auto three = compositeThree.tests;
572     assertEqual(three.length, 1);
573     assertPass(three[0]);
574 }
575 
576 @("Value parametrized function tests with @AutoTags get tagged by value")
577 unittest {
578     import unit_threaded.factory;
579     import unit_threaded.testcase;
580 
581     const testData = allTestData!(unit_threaded.tests.parametrized).
582         filter!(a => a.name.canFind("testValues")).array;
583 
584     auto compositeTwo = cast(CompositeTestCase)createTestCases(testData, ["@2"])[0];
585     assert(compositeTwo !is null, "Wrong dynamic type for TestCase");
586     auto two = compositeTwo.tests;
587     assertEqual(two.length, 1);
588     assertFail(two[0]);
589 }
590 
591 @("Type parameterized tests with @AutoTags get tagged by type")
592 unittest {
593     import unit_threaded.factory;
594     import unit_threaded.testcase;
595 
596     const testData = allTestData!(unit_threaded.tests.parametrized).
597         filter!(a => a.name.canFind("testTypes")).array;
598 
599     auto composite = cast(CompositeTestCase)createTestCases(testData, ["@int"])[0];
600     assert(composite !is null, "Wrong dynamic type for TestCase");
601     auto tests = composite.tests;
602     assertEqual(tests.length, 1);
603     assertPass(tests[0]);
604 }