1 /**
2    Compile-time reflection to find unit tests and set their properties.
3  */
4 module unit_threaded.runner.reflection;
5 
6 
7 import unit_threaded.from;
8 
9 /*
10    These standard library imports contain something important for the code below.
11    Unfortunately I don't know what they are so they're to prevent breakage.
12  */
13 import std.traits;
14 import std.algorithm;
15 import std.array;
16 
17 
18 ///
19 alias TestFunction = void delegate();
20 
21 /**
22  * Attributes of each test.
23  */
24 struct TestData {
25     string moduleName;
26     string name;
27     TestFunction testFunction;
28     bool hidden;
29     bool shouldFail;
30     bool singleThreaded;
31     bool builtin;
32     string suffix; // append to end of getPath
33     string[] tags;
34     TypeInfo exceptionTypeInfo; // for ShouldFailWith
35     int flakyRetries = 0;
36 
37     /// The test's name
38     string getPath() @safe const pure nothrow {
39         string path = name.dup;
40         import std.array: empty;
41         if(!suffix.empty) path ~= "." ~ suffix;
42         return path;
43     }
44 }
45 
46 
47 /**
48  * Finds all test cases.
49  * Template parameters are module strings.
50  */
51 const(TestData)[] allTestData(MOD_STRINGS...)()
52     if(from!"std.meta".allSatisfy!(from!"std.traits".isSomeString, typeof(MOD_STRINGS)))
53 {
54     import std.array: join;
55     import std.range : iota;
56     import std.format : format;
57     import std.algorithm : map;
58 
59     string getModulesString() {
60         string[] modules;
61         foreach(i, module_; MOD_STRINGS) modules ~= "module%d = %s".format(i, module_);
62         return modules.join(", ");
63     }
64 
65     enum modulesString = getModulesString;
66     mixin("import " ~ modulesString ~ ";");
67     mixin("return allTestData!(" ~
68           MOD_STRINGS.length.iota.map!(i => "module%d".format(i)).join(", ") ~
69           ");");
70 }
71 
72 
73 /**
74  * Finds all test cases.
75  * Template parameters are module symbols.
76  */
77 const(TestData)[] allTestData(MOD_SYMBOLS...)()
78     if(!from!"std.meta".anySatisfy!(from!"std.traits".isSomeString, typeof(MOD_SYMBOLS)))
79 {
80     return moduleUnitTests!MOD_SYMBOLS;
81 }
82 
83 
84 private template Identity(T...) if(T.length > 0) {
85     static if(__traits(compiles, { alias x = T[0]; }))
86         alias Identity = T[0];
87     else
88         enum Identity = T[0];
89 }
90 
91 
92 /**
93    Names a test function / built-in unittest based on @Name or string UDAs
94    on it. If none are found, "returns" an empty string
95  */
96 template TestNameFromAttr(alias testFunction) {
97     import unit_threaded.runner.attrs: Name;
98     import std.traits: getUDAs;
99     import std.meta: Filter;
100 
101     // i.e. if @("this is my name") appears
102     enum strAttrs = Filter!(isStringUDA, __traits(getAttributes, testFunction));
103 
104     enum nameAttrs = getUDAs!(testFunction, Name);
105     static assert(nameAttrs.length < 2, "Only one @Name UDA allowed");
106 
107     // strAttrs might be values to pass so only if the length is 1 is it a name
108     enum hasName = nameAttrs.length || strAttrs.length == 1;
109 
110     static if(hasName) {
111         static if(nameAttrs.length == 1)
112             enum TestNameFromAttr = nameAttrs[0].value;
113         else
114             enum TestNameFromAttr = strAttrs[0];
115     } else
116         enum TestNameFromAttr = "";
117 }
118 
119 /**
120  * Finds all built-in unittest blocks in the given modules.
121  * Recurses into structs, classes, and unions of the modules.
122  *
123  * @return An array of TestData structs
124  */
125 TestData[] moduleUnitTests(modules...)() {
126     TestData[] ret;
127     static foreach(module_; modules) {
128         ret ~= moduleUnitTests_!module_;
129     }
130     return ret;
131 }
132 
133 /**
134  * Finds all built-in unittest blocks in the given module.
135  * Recurses into structs, classes, and unions of the module.
136  *
137  * @return An array of TestData structs
138  */
139 private TestData[] moduleUnitTests_(alias module_)() {
140 
141     // Return a name for a unittest block. If no @Name UDA is found a name is
142     // created automatically, else the UDA is used.
143     // the weird name for the first template parameter is so that it doesn't clash
144     // with a package name
145     string unittestName(alias _theUnitTest, int index)() @safe nothrow {
146         import std.conv: text;
147         import std.algorithm: startsWith, endsWith;
148         import std.traits: fullyQualifiedName;
149 
150         enum prefix = fullyQualifiedName!(__traits(parent, _theUnitTest)) ~ ".";
151         enum nameFromAttr = TestNameFromAttr!_theUnitTest;
152 
153         // Establish a unique name for a unittest with no name
154         static if(nameFromAttr == "") {
155             // use the unittest name if available to allow for running unittests based
156             // on location
157             if(__traits(identifier, _theUnitTest).startsWith("__unittest_L")) {
158                 const ret = prefix ~ __traits(identifier, _theUnitTest)[2 .. $];
159                 const suffix = "_C1";
160                 // simplify names for the common case where there's only one
161                 // unittest per line
162 
163                 return ret.endsWith(suffix) ? ret[0 .. $ - suffix.length] : ret;
164             }
165 
166             try
167                 return prefix ~ "unittest" ~ index.text;
168             catch(Exception)
169                 assert(false, text("Error converting ", index, " to string"));
170 
171         } else
172             return prefix ~ nameFromAttr;
173     }
174 
175     void function() getUDAFunction(alias composite, alias uda)() pure nothrow {
176         import std.traits: isSomeFunction, hasUDA;
177 
178         void function()[] ret;
179         foreach(memberStr; __traits(allMembers, composite)) {
180             static if(__traits(compiles, Identity!(__traits(getMember, composite, memberStr)))) {
181                 alias member = Identity!(__traits(getMember, composite, memberStr));
182                 static if(__traits(compiles, &member)) {
183                     static if(isSomeFunction!member && hasUDA!(member, uda)) {
184                         ret ~= &member;
185                     }
186                 }
187             }
188         }
189 
190         return ret.length ? ret[0] : null;
191     }
192 
193     TestData[] testData;
194 
195     void addMemberUnittests(alias composite)() pure nothrow {
196 
197         import unit_threaded.runner.attrs;
198         import std.traits: hasUDA, moduleName;
199         import std.meta: Filter;
200 
201         // cheap target for implicit conversion
202         string str__;
203 
204         // weird name for hygiene reasons
205         foreach(index, eLtEstO; __traits(getUnitTests, composite)) {
206             // make common case cheap: @("name") unittest {}
207             static if(__traits(getAttributes, eLtEstO).length == 1
208                 && __traits(compiles, str__ = __traits(getAttributes, eLtEstO)[0])
209             ) {
210                 enum prefix = fullyQualifiedName!(__traits(parent, eLtEstO)) ~ ".";
211                 enum name = prefix ~ __traits(getAttributes, eLtEstO)[0];
212                 // there's only one attribute and it's a string representing the name,
213                 // so attribute-based variables are all false.
214                 enum hidden = false;
215                 enum shouldFail = false;
216                 enum singleThreaded = false;
217                 enum tags = string[].init;
218                 enum exceptionTypeInfo = TypeInfo.init;
219                 enum flakyRetries = 0;
220             } else {
221                 enum name = unittestName!(eLtEstO, index);
222                 enum hidden = hasUDA!(eLtEstO, HiddenTest);
223                 enum shouldFail = hasUDA!(eLtEstO, ShouldFail) || hasUDA!(eLtEstO, ShouldFailWith);
224                 enum singleThreaded = hasUDA!(eLtEstO, Serial);
225                 enum isTags(alias T) = is(typeof(T)) && is(typeof(T) == Tags);
226                 enum tags = tagsFromAttrs!(Filter!(isTags, __traits(getAttributes, eLtEstO)));
227                 enum exceptionTypeInfo = getExceptionTypeInfo!eLtEstO;
228                 enum flakyRetries = getFlakyRetries!(eLtEstO);
229             }
230             enum builtin = true;
231             enum suffix = "";
232 
233             testData ~= TestData(
234                 moduleName!composite,
235                 name,
236                 () {
237                     auto setup = getUDAFunction!(composite, Setup);
238                     auto shutdown = getUDAFunction!(composite, Shutdown);
239 
240                     if(setup) setup();
241                     scope(exit) if(shutdown) shutdown();
242 
243                     eLtEstO();
244                 },
245                 hidden,
246                 shouldFail,
247                 singleThreaded,
248                 builtin,
249                 suffix,
250                 tags,
251                 exceptionTypeInfo,
252                 flakyRetries
253             );
254         }
255     }
256 
257     // Keeps track of mangled names of everything visited.
258     bool[string] visitedMembers;
259 
260     void addUnitTestsRecursively(alias composite)() pure nothrow {
261 
262         if (composite.mangleof in visitedMembers)
263             return;
264 
265         visitedMembers[composite.mangleof] = true;
266         addMemberUnittests!composite();
267 
268         foreach(member; __traits(allMembers, composite)) {
269 
270             // isPrivate can't be used here. I don't know why.
271             static if(__traits(compiles, __traits(getProtection, __traits(getMember, module_, member))))
272                 enum notPrivate = __traits(getProtection, __traits(getMember, module_, member)) != "private";
273             else
274                 enum notPrivate = false;
275 
276             static if (
277                 notPrivate &&
278                 // If visibility of the member is deprecated, the next line still returns true
279                 // and yet spills deprecation warning. If deprecation is turned into error,
280                 // all works as intended.
281                 __traits(compiles, __traits(getMember, composite, member)) &&
282                 __traits(compiles, __traits(allMembers, __traits(getMember, composite, member))) &&
283                 __traits(compiles, recurse!(__traits(getMember, composite, member)))
284             ) {
285                 recurse!(__traits(getMember, composite, member));
286             }
287         }
288     }
289 
290     void recurse(child)() pure nothrow {
291         static if (is(child == class) || is(child == struct) || is(child == union)) {
292             addUnitTestsRecursively!child;
293         }
294     }
295 
296     addUnitTestsRecursively!module_();
297     return testData;
298 }
299 
300 private TypeInfo getExceptionTypeInfo(alias Test)() {
301     import unit_threaded.runner.attrs: ShouldFailWith;
302     import std.traits: hasUDA, getUDAs;
303 
304     static if(hasUDA!(Test, ShouldFailWith)) {
305         alias uda = getUDAs!(Test, ShouldFailWith)[0];
306         return typeid(uda.Type);
307     } else
308         return null;
309 }
310 
311 
312 private template isStringUDA(alias T) {
313     import std.traits: isSomeString;
314     static if(__traits(compiles, isSomeString!(typeof(T))))
315         enum isStringUDA = isSomeString!(typeof(T));
316     else
317         enum isStringUDA = false;
318 }
319 
320 @safe pure unittest {
321     static assert(isStringUDA!"foo");
322     static assert(!isStringUDA!5);
323 }
324 
325 private template isPrivate(alias module_, string moduleMember) {
326     alias ut_mmbr__ = Identity!(__traits(getMember, module_, moduleMember));
327 
328     static if(__traits(compiles, __traits(getProtection, ut_mmbr__)))
329         enum isPrivate = __traits(getProtection, ut_mmbr__) == "private";
330     else
331         enum isPrivate = true;
332 }
333 
334 
335 private int getFlakyRetries(alias test)() {
336     import unit_threaded.runner.attrs: Flaky;
337     import std.traits: getUDAs;
338     import std.conv: text;
339 
340     alias flakies = getUDAs!(test, Flaky);
341 
342     static assert(flakies.length == 0 || flakies.length == 1,
343                   text("Only 1 @Flaky allowed, found ", flakies.length, " on ",
344                        __traits(identifier, test)));
345 
346     static if(flakies.length == 1) {
347         static if(is(flakies[0]))
348             return Flaky.defaultRetries;
349         else
350             return flakies[0].retries;
351     } else
352         return 0;
353 }
354 
355 string[] tagsFromAttrs(T...)() {
356     static assert(T.length <= 1, "@Tags can only be applied once");
357     static if(T.length)
358         return T[0].values;
359     else
360         return [];
361 }