1 module unit_threaded.reflection;
2 
3 import unit_threaded.attrs;
4 import unit_threaded.uda;
5 import std.traits;
6 import std.typetuple;
7 
8 /**
9  * Common data for test functions and test classes
10  */
11 alias void delegate() TestFunction;
12 struct TestData {
13     string name;
14     TestFunction testFunction; ///only used for functions, null for classes
15     bool hidden;
16     bool shouldFail;
17     bool singleThreaded;
18     bool builtin;
19     string suffix; // append to end of getPath
20 }
21 
22 
23 /**
24  * Finds all test cases (functions, classes, built-in unittest blocks)
25  * Template parameters are module strings
26  */
27 const(TestData)[] allTestData(MOD_STRINGS...)() if(allSatisfy!(isSomeString, typeof(MOD_STRINGS))) {
28 
29     string getModulesString() {
30         import std.array: join;
31         string[] modules;
32         foreach(module_; MOD_STRINGS) modules ~= module_;
33         return modules.join(", ");
34     }
35 
36     enum modulesString =  getModulesString;
37     mixin("import " ~ modulesString ~ ";");
38     mixin("return allTestData!(" ~ modulesString ~ ");");
39 }
40 
41 
42 /**
43  * Finds all test cases (functions, classes, built-in unittest blocks)
44  * Template parameters are module symbols
45  */
46 const(TestData)[] allTestData(MOD_SYMBOLS...)() if(!anySatisfy!(isSomeString, typeof(MOD_SYMBOLS))) {
47     auto allTestsWithFunc(string expr, MOD_SYMBOLS...)() pure {
48         //tests is whatever type expr returns
49         ReturnType!(mixin(expr ~ q{!(MOD_SYMBOLS[0])})) tests;
50         foreach(module_; TypeTuple!MOD_SYMBOLS) {
51             tests ~= mixin(expr ~ q{!module_()}); //e.g. tests ~= moduleTestClasses!module_
52         }
53         return tests;
54     }
55 
56     return allTestsWithFunc!(q{moduleTestClasses}, MOD_SYMBOLS) ~
57            allTestsWithFunc!(q{moduleTestFunctions}, MOD_SYMBOLS) ~
58            allTestsWithFunc!(q{moduleUnitTests}, MOD_SYMBOLS);
59 }
60 
61 
62 /**
63  * Finds all built-in unittest blocks in the given module.
64  * @return An array of TestData structs
65  */
66 TestData[] moduleUnitTests(alias module_)() pure nothrow {
67 
68     // Return a name for a unittest block. If no @Name UDA is found a name is
69     // created automatically, else the UDA is used.
70     string unittestName(alias test, int index)() @safe nothrow {
71         import std.conv;
72         mixin("import " ~ fullyQualifiedName!module_ ~ ";"); //so it's visible
73 
74         enum nameAttrs = getUDAs!(test, Name);
75         static assert(nameAttrs.length == 0 || nameAttrs.length == 1, "Found multiple Name UDAs on unittest");
76 
77         template isStringUDA(alias T) {
78             static if(__traits(compiles, is(typeof(T)) && isSomeString!T))
79                 enum isStringUDA = is(typeof(T)) && isSomeString!T;
80             else
81                 enum isStringUDA = false;
82         }
83 
84         enum strAttrs = Filter!(isStringUDA, __traits(getAttributes, test));
85 
86         enum hasName = nameAttrs.length || strAttrs.length == 1;
87         enum prefix = fullyQualifiedName!module_ ~ ".";
88 
89         static if(hasName) {
90             static if(nameAttrs.length == 1)
91                 return prefix ~ nameAttrs[0].value;
92             else
93                 return prefix ~ strAttrs[0];
94         } else {
95             string name;
96             try {
97                 return prefix ~ "unittest" ~ (index).to!string;
98             } catch(Exception) {
99                 assert(false, text("Error converting ", index, " to string"));
100             }
101         }
102     }
103 
104     TestData[] testData;
105     foreach(index, test; __traits(getUnitTests, module_)) {
106         enum name = unittestName!(test, index);
107         enum hidden = hasUDA!(test, HiddenTest);
108         enum shouldFail = hasUDA!(test, ShouldFail);
109         enum singleThreaded = hasUDA!(test, Serial);
110         enum builtin = true;
111         testData ~= TestData(name, (){ test(); }, hidden, shouldFail, singleThreaded, builtin);
112     }
113     return testData;
114 }
115 
116 
117 /**
118  * Finds all test classes (classes implementing a test() function)
119  * in the given module
120  */
121 TestData[] moduleTestClasses(alias module_)() pure nothrow {
122 
123     template isTestClass(alias module_, string moduleMember) {
124         mixin("import " ~ fullyQualifiedName!module_ ~ ";"); //so it's visible
125         static if(!__traits(compiles, isAggregateType!(mixin(moduleMember)))) {
126             enum isTestClass = false;
127         } else static if(!isAggregateType!(mixin(moduleMember))) {
128             enum isTestClass = false;
129         } else static if(!__traits(compiles, mixin("new " ~ moduleMember))) {
130             enum isTestClass = false; //can't new it, can't use it
131         } else {
132             enum hasUnitTest = HasAttribute!(module_, moduleMember, UnitTest);
133             enum hasTestMethod = __traits(hasMember, mixin(moduleMember), "test");
134             enum isTestClass = hasTestMethod || hasUnitTest;
135         }
136     }
137 
138     return moduleTestData!(module_, isTestClass);
139 }
140 
141 /**
142  * Finds all test functions in the given module.
143  * Returns an array of TestData structs
144  */
145 TestData[] moduleTestFunctions(alias module_)() pure {
146 
147     template isTestFunction(alias module_, string moduleMember) {
148         mixin("import " ~ fullyQualifiedName!module_ ~ ";"); //so it's visible
149         static if(isSomeFunction!(mixin(moduleMember))) {
150             enum isTestFunction = hasTestPrefix!(module_, moduleMember) ||
151                 HasAttribute!(module_, moduleMember, UnitTest);
152         } else {
153             enum isTestFunction = false;
154         }
155     }
156 
157     template hasTestPrefix(alias module_, string member) {
158         import std.uni: isUpper;
159         mixin("import " ~ fullyQualifiedName!module_ ~ ";"); //so it's visible
160 
161         enum prefix = "test";
162         enum minSize = prefix.length + 1;
163 
164         static if(isSomeFunction!(mixin(member)) &&
165                   member.length >= minSize && member[0 .. prefix.length] == prefix &&
166                   isUpper(member[prefix.length])) {
167             enum hasTestPrefix = true;
168         } else {
169             enum hasTestPrefix = false;
170         }
171     }
172 
173     return moduleTestData!(module_, isTestFunction);
174 }
175 
176 private struct TestFunctionSuffix {
177     TestFunction testFunction;
178     string suffix;
179 }
180 
181 
182 private TestData[] moduleTestData(alias module_, alias pred)() pure {
183     mixin("import " ~ fullyQualifiedName!module_ ~ ";"); //so it's visible
184     TestData[] testData;
185     foreach(moduleMember; __traits(allMembers, module_)) {
186 
187         enum notPrivate = __traits(compiles, mixin(moduleMember)); //only way I know to check if private
188 
189         static if(notPrivate && pred!(module_, moduleMember) &&
190                   !HasAttribute!(module_, moduleMember, DontTest)) {
191 
192             TestFunctionSuffix[] getTestFunctions(alias module_, string moduleMember)() {
193                 //returns delegates for test functions, null for test classes
194                 static if(__traits(compiles, &__traits(getMember, module_, moduleMember))) {
195                     enum func = &__traits(getMember, module_, moduleMember);
196                     enum arity = arity!func;
197 
198                     static assert(arity == 0 || arity == 1, "Test functions may take at most one parameter");
199 
200                     static if(arity == 0)
201                         return [ TestFunctionSuffix((){ func(); }) ]; //simple case, just call it
202                     else {
203                         //check to see if the function has UDAs to call it with
204                         alias params = Parameters!func;
205                         static assert(params.length == 1, "Test functions may take at most one parameter");
206 
207                         alias values = GetAttributes!(module_, moduleMember, params[0]);
208                         import std.conv;
209                         static assert(values.length > 0,
210                                       text("Test functions with a parameter of type <", params[0].stringof,
211                                        "> must have value UDAs of the same type"));
212 
213                         TestFunctionSuffix[] functions;
214                         foreach(v; values) functions ~= TestFunctionSuffix((){ func(v); }, v.to!string);
215                         return functions;
216                     }
217                 } else {
218                     //test class
219                     return [TestFunctionSuffix(null)];
220                 }
221             }
222 
223             auto functions = getTestFunctions!(module_, moduleMember);
224             foreach(f; functions) {
225                 //if there is more than one function, they're all single threaded - multiple values per test call.
226                 immutable singleThreaded = functions.length > 1 || HasAttribute!(module_, moduleMember, Serial);
227                 immutable builtin = false;
228                 testData ~= TestData(fullyQualifiedName!module_~ "." ~ moduleMember,
229                                      f.testFunction,
230                                      HasAttribute!(module_, moduleMember, HiddenTest),
231                                      HasAttribute!(module_, moduleMember, ShouldFail),
232                                      singleThreaded,
233                                      builtin,
234                                      f.suffix);
235             }
236         }
237     }
238 
239     return testData;
240 }
241 
242 
243 
244 import unit_threaded.tests.module_with_tests; //defines tests and non-tests
245 import unit_threaded.asserts;
246 import std.algorithm;
247 import std.array;
248 
249 //helper function for the unittest blocks below
250 private auto addModPrefix(string[] elements, string module_ = "unit_threaded.tests.module_with_tests") nothrow {
251     return elements.map!(a => module_ ~ "." ~ a).array;
252 }
253 
254 unittest {
255     const expected = addModPrefix([ "FooTest", "BarTest", "Blergh"]);
256     const actual = moduleTestClasses!(unit_threaded.tests.module_with_tests).map!(a => a.name).array;
257     assertEqual(actual, expected);
258 }
259 
260 unittest {
261     const expected = addModPrefix([ "testFoo", "testBar", "funcThatShouldShowUpCosOfAttr" ]);
262     const actual = moduleTestFunctions!(unit_threaded.tests.module_with_tests).map!(a => a.name).array;
263     assertEqual(actual, expected);
264 }
265 
266 
267 unittest {
268     const expected = addModPrefix(["unittest0", "unittest1", "myUnitTest"]);
269     const actual = moduleUnitTests!(unit_threaded.tests.module_with_tests).map!(a => a.name).array;
270     assertEqual(actual, expected);
271 }