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 function() TestFunction;
12 struct TestData {
13     string name;
14     bool hidden;
15     bool shouldFail;
16     TestFunction testFunction; ///only used for functions, null for classes
17     bool singleThreaded;
18     bool builtin;
19 }
20 
21 
22 /**
23  * Finds all test cases (functions, classes, built-in unittest blocks)
24  * Template parameters are module strings
25  */
26 const(TestData)[] allTestCaseData(MOD_STRINGS...)() if(allSatisfy!(isSomeString, typeof(MOD_STRINGS))) {
27 
28     string getModulesString() {
29         import std.array: join;
30         string[] modules;
31         foreach(module_; MOD_STRINGS) modules ~= module_;
32         return modules.join(", ");
33     }
34 
35     enum modulesString =  getModulesString;
36     mixin("import " ~ modulesString ~ ";");
37     mixin("return allTestCaseData!(" ~ modulesString ~ ");");
38 }
39 
40 
41 /**
42  * Finds all test cases (functions, classes, built-in unittest blocks)
43  * Template parameters are module symbols
44  */
45 const(TestData)[] allTestCaseData(MOD_SYMBOLS...)() if(!anySatisfy!(isSomeString, typeof(MOD_SYMBOLS))) {
46     auto allTestsWithFunc(string expr, MOD_SYMBOLS...)() pure nothrow {
47         //tests is whatever type expr returns
48         ReturnType!(mixin(expr ~ q{!(MOD_SYMBOLS[0])})) tests;
49         foreach(module_; TypeTuple!MOD_SYMBOLS) {
50             tests ~= mixin(expr ~ q{!module_()}); //e.g. tests ~= moduleTestClasses!module_
51         }
52         return tests;
53     }
54 
55     return allTestsWithFunc!(q{moduleTestClasses}, MOD_SYMBOLS) ~
56            allTestsWithFunc!(q{moduleTestFunctions}, MOD_SYMBOLS) ~
57            allTestsWithFunc!(q{moduleUnitTests}, MOD_SYMBOLS);
58 }
59 
60 
61 /**
62  * Finds all built-in unittest blocks in the given module.
63  * @return An array of TestData structs
64  */
65 auto moduleUnitTests(alias module_)() pure nothrow {
66 
67     // Return a name for a unittest block. If no @Name UDA is found a name is
68     // created automatically, else the UDA is used.
69     string unittestName(alias test, int index)() @safe nothrow {
70         import std.conv;
71         mixin("import " ~ fullyQualifiedName!module_ ~ ";"); //so it's visible
72 
73         enum isName(alias T) = is(typeof(T)) && is(typeof(T) == Name);
74         alias names = Filter!(isName, __traits(getAttributes, test));
75         static assert(names.length == 0 || names.length == 1, "Found multiple Name UDAs on unittest");
76         enum prefix = fullyQualifiedName!module_ ~ ".";
77 
78         static if(names.length == 1) {
79             return prefix ~ names[0].value;
80         } else {
81             string name;
82             try {
83                 return prefix ~ "unittest" ~ (index).to!string;
84             } catch(Exception) {
85                 assert(false, text("Error converting ", index, " to string"));
86             }
87         }
88     }
89 
90     TestData[] testData;
91     foreach(index, test; __traits(getUnitTests, module_)) {
92         enum name = unittestName!(test, index);
93         enum hidden = false;
94         enum shouldFail = false;
95         enum singleThreaded = false;
96         enum builtin = true;
97         testData ~= TestData(name, hidden, shouldFail, &test, singleThreaded, builtin);
98     }
99     return testData;
100 }
101 
102 
103 /**
104  * Finds all test classes (classes implementing a test() function)
105  * in the given module
106  */
107 auto moduleTestClasses(alias module_)() pure nothrow {
108 
109     template isTestClass(alias module_, string moduleMember) {
110         mixin("import " ~ fullyQualifiedName!module_ ~ ";"); //so it's visible
111         static if(!__traits(compiles, isAggregateType!(mixin(moduleMember)))) {
112             enum isTestClass = false;
113         } else static if(!isAggregateType!(mixin(moduleMember))) {
114             enum isTestClass = false;
115         } else {
116             enum hasUnitTest = HasAttribute!(module_, moduleMember, UnitTest);
117             enum hasTestMethod = __traits(hasMember, mixin(moduleMember), "test");
118             enum isTestClass = hasTestMethod || hasUnitTest;
119         }
120     }
121 
122     return moduleTestCases!(module_, isTestClass);
123 }
124 
125 /**
126  * Finds all test functions in the given module.
127  * Returns an array of TestData structs
128  */
129 auto moduleTestFunctions(alias module_)() pure nothrow {
130 
131     template isTestFunction(alias module_, string moduleMember) {
132         mixin("import " ~ fullyQualifiedName!module_ ~ ";"); //so it's visible
133         static if(isSomeFunction!(mixin(moduleMember))) {
134             enum isTestFunction = hasTestPrefix!(module_, moduleMember) ||
135                 HasAttribute!(module_, moduleMember, UnitTest);
136         } else {
137             enum isTestFunction = false;
138         }
139     }
140 
141     template hasTestPrefix(alias module_, string member) {
142         import std.uni: isUpper;
143         mixin("import " ~ fullyQualifiedName!module_ ~ ";"); //so it's visible
144 
145         enum prefix = "test";
146         enum minSize = prefix.length + 1;
147 
148         static if(isSomeFunction!(mixin(member)) &&
149                   member.length >= minSize && member[0 .. prefix.length] == prefix &&
150                   isUpper(member[prefix.length])) {
151             enum hasTestPrefix = true;
152         } else {
153             enum hasTestPrefix = false;
154         }
155     }
156 
157     return moduleTestCases!(module_, isTestFunction);
158 }
159 
160 
161 private auto moduleTestCases(alias module_, alias pred)() pure nothrow {
162     mixin("import " ~ fullyQualifiedName!module_ ~ ";"); //so it's visible
163     TestData[] testData;
164     foreach(moduleMember; __traits(allMembers, module_)) {
165 
166         enum notPrivate = __traits(compiles, mixin(moduleMember)); //only way I know to check if private
167 
168         static if(notPrivate && pred!(module_, moduleMember) &&
169                   !HasAttribute!(module_, moduleMember, DontTest)) {
170 
171             TestFunction getTestFunction(alias module_, string moduleMember)() pure nothrow {
172                 //returns a function pointer for test functions, null for test classes
173                 static if(__traits(compiles, &__traits(getMember, module_, moduleMember))) {
174                     return &__traits(getMember, module_, moduleMember);
175                 } else {
176                     return null;
177                 }
178             }
179 
180             testData ~= TestData(fullyQualifiedName!module_~ "." ~ moduleMember,
181                                  HasAttribute!(module_, moduleMember, HiddenTest),
182                                  HasAttribute!(module_, moduleMember, ShouldFail),
183                                  getTestFunction!(module_, moduleMember),
184                                  HasAttribute!(module_, moduleMember, SingleThreaded));
185         }
186     }
187 
188     return testData;
189 }
190 
191 
192 
193 import unit_threaded.tests.module_with_tests; //defines tests and non-tests
194 import unit_threaded.asserts;
195 import std.algorithm;
196 import std.array;
197 
198 //helper function for the unittest blocks below
199 private auto addModPrefix(string[] elements, string module_ = "unit_threaded.tests.module_with_tests") nothrow {
200     return elements.map!(a => module_ ~ "." ~ a).array;
201 }
202 
203 unittest {
204     const expected = addModPrefix([ "FooTest", "BarTest", "Blergh"]);
205     const actual = moduleTestClasses!(unit_threaded.tests.module_with_tests).map!(a => a.name).array;
206     assertEqual(actual, expected);
207 }
208 
209 unittest {
210     const expected = addModPrefix([ "testFoo", "testBar", "funcThatShouldShowUpCosOfAttr" ]);
211     const actual = moduleTestFunctions!(unit_threaded.tests.module_with_tests).map!(a => a.name).array;
212     assertEqual(actual, expected);
213 }
214 
215 
216 unittest {
217     const expected = addModPrefix(["unittest0", "unittest1", "myUnitTest"]);
218     const actual = moduleUnitTests!(unit_threaded.tests.module_with_tests).map!(a => a.name).array;
219     assertEqual(actual, expected);
220 }