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         enum strAttrs = Filter!(isStringUDA, __traits(getAttributes, test));
78         enum hasName = nameAttrs.length || strAttrs.length == 1;
79         enum prefix = fullyQualifiedName!module_ ~ ".";
80 
81         static if(hasName) {
82             static if(nameAttrs.length == 1)
83                 return prefix ~ nameAttrs[0].value;
84             else
85                 return prefix ~ strAttrs[0];
86         } else {
87             string name;
88             try {
89                 return prefix ~ "unittest" ~ (index).to!string;
90             } catch(Exception) {
91                 assert(false, text("Error converting ", index, " to string"));
92             }
93         }
94     }
95 
96     TestData[] testData;
97     foreach(index, test; __traits(getUnitTests, module_)) {
98         enum name = unittestName!(test, index);
99         enum hidden = hasUDA!(test, HiddenTest);
100         enum shouldFail = hasUDA!(test, ShouldFail);
101         enum singleThreaded = hasUDA!(test, Serial);
102         enum builtin = true;
103         testData ~= TestData(name, (){ test(); }, hidden, shouldFail, singleThreaded, builtin);
104     }
105     return testData;
106 }
107 
108 private template isStringUDA(alias T) {
109     static if(__traits(compiles, isSomeString!(typeof(T))))
110         enum isStringUDA = isSomeString!(typeof(T));
111     else
112         enum isStringUDA = false;
113 }
114 
115 unittest {
116     static assert(isStringUDA!"foo");
117     static assert(!isStringUDA!5);
118 }
119 
120 
121 /**
122  * Finds all test classes (classes implementing a test() function)
123  * in the given module
124  */
125 TestData[] moduleTestClasses(alias module_)() pure nothrow {
126 
127     template isTestClass(alias module_, string moduleMember) {
128         mixin("import " ~ fullyQualifiedName!module_ ~ ";"); //so it's visible
129         static if(!__traits(compiles, isAggregateType!(mixin(moduleMember)))) {
130             enum isTestClass = false;
131         } else static if(!isAggregateType!(mixin(moduleMember))) {
132             enum isTestClass = false;
133         } else static if(!__traits(compiles, mixin("new " ~ moduleMember))) {
134             enum isTestClass = false; //can't new it, can't use it
135         } else {
136             enum hasUnitTest = HasAttribute!(module_, moduleMember, UnitTest);
137             enum hasTestMethod = __traits(hasMember, mixin(moduleMember), "test");
138             enum isTestClass = hasTestMethod || hasUnitTest;
139         }
140     }
141 
142     return moduleTestData!(module_, isTestClass);
143 }
144 
145 /**
146  * Finds all test functions in the given module.
147  * Returns an array of TestData structs
148  */
149 TestData[] moduleTestFunctions(alias module_)() pure {
150 
151     template isTestFunction(alias module_, string moduleMember) {
152         mixin("import " ~ fullyQualifiedName!module_ ~ ";"); //so it's visible
153         static if(isSomeFunction!(mixin(moduleMember))) {
154             enum isTestFunction = hasTestPrefix!(module_, moduleMember) ||
155                 HasAttribute!(module_, moduleMember, UnitTest);
156         } else {
157             enum isTestFunction = false;
158         }
159     }
160 
161     template hasTestPrefix(alias module_, string member) {
162         import std.uni: isUpper;
163         mixin("import " ~ fullyQualifiedName!module_ ~ ";"); //so it's visible
164 
165         enum prefix = "test";
166         enum minSize = prefix.length + 1;
167 
168         static if(isSomeFunction!(mixin(member)) &&
169                   member.length >= minSize && member[0 .. prefix.length] == prefix &&
170                   isUpper(member[prefix.length])) {
171             enum hasTestPrefix = true;
172         } else {
173             enum hasTestPrefix = false;
174         }
175     }
176 
177     return moduleTestData!(module_, isTestFunction);
178 }
179 
180 private struct TestFunctionSuffix {
181     TestFunction testFunction;
182     string suffix;
183 }
184 
185 
186 private TestData[] moduleTestData(alias module_, alias pred)() pure {
187     mixin("import " ~ fullyQualifiedName!module_ ~ ";"); //so it's visible
188     TestData[] testData;
189     foreach(moduleMember; __traits(allMembers, module_)) {
190 
191         enum notPrivate = __traits(compiles, mixin(moduleMember)); //only way I know to check if private
192 
193         static if(notPrivate && pred!(module_, moduleMember) &&
194                   !HasAttribute!(module_, moduleMember, DontTest)) {
195 
196             TestFunctionSuffix[] getTestFunctions(alias module_, string moduleMember)() {
197                 //returns delegates for test functions, null for test classes
198                 static if(__traits(compiles, &__traits(getMember, module_, moduleMember))) {
199                     enum func = &__traits(getMember, module_, moduleMember);
200                     enum arity = arity!func;
201 
202                     static assert(arity == 0 || arity == 1, "Test functions may take at most one parameter");
203 
204                     static if(arity == 0)
205                         return [ TestFunctionSuffix((){ func(); }) ]; //simple case, just call it
206                     else {
207                         //check to see if the function has UDAs to call it with
208                         alias params = Parameters!func;
209                         static assert(params.length == 1, "Test functions may take at most one parameter");
210 
211                         alias values = GetAttributes!(module_, moduleMember, params[0]);
212                         import std.conv;
213                         static assert(values.length > 0,
214                                       text("Test functions with a parameter of type <", params[0].stringof,
215                                        "> must have value UDAs of the same type"));
216 
217                         TestFunctionSuffix[] functions;
218                         foreach(v; values) functions ~= TestFunctionSuffix((){ func(v); }, v.to!string);
219                         return functions;
220                     }
221                 } else {
222                     //test class
223                     return [TestFunctionSuffix(null)];
224                 }
225             }
226 
227             auto functions = getTestFunctions!(module_, moduleMember);
228             foreach(f; functions) {
229                 //if there is more than one function, they're all single threaded - multiple values per test call.
230                 immutable singleThreaded = functions.length > 1 || HasAttribute!(module_, moduleMember, Serial);
231                 immutable builtin = false;
232                 testData ~= TestData(fullyQualifiedName!module_~ "." ~ moduleMember,
233                                      f.testFunction,
234                                      HasAttribute!(module_, moduleMember, HiddenTest),
235                                      HasAttribute!(module_, moduleMember, ShouldFail),
236                                      singleThreaded,
237                                      builtin,
238                                      f.suffix);
239             }
240         }
241     }
242 
243     return testData;
244 }
245 
246 
247 
248 import unit_threaded.tests.module_with_tests; //defines tests and non-tests
249 import unit_threaded.asserts;
250 import std.algorithm;
251 import std.array;
252 
253 //helper function for the unittest blocks below
254 private auto addModPrefix(string[] elements, string module_ = "unit_threaded.tests.module_with_tests") nothrow {
255     return elements.map!(a => module_ ~ "." ~ a).array;
256 }
257 
258 unittest {
259     const expected = addModPrefix([ "FooTest", "BarTest", "Blergh"]);
260     const actual = moduleTestClasses!(unit_threaded.tests.module_with_tests).map!(a => a.name).array;
261     assertEqual(actual, expected);
262 }
263 
264 unittest {
265     const expected = addModPrefix([ "testFoo", "testBar", "funcThatShouldShowUpCosOfAttr" ]);
266     const actual = moduleTestFunctions!(unit_threaded.tests.module_with_tests).map!(a => a.name).array;
267     assertEqual(actual, expected);
268 }
269 
270 
271 unittest {
272     const expected = addModPrefix(["unittest0", "unittest1", "myUnitTest"]);
273     const actual = moduleUnitTests!(unit_threaded.tests.module_with_tests).map!(a => a.name).array;
274     assertEqual(actual, expected);
275 }