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