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 prefix = fullyQualifiedName!(__traits(parent, eLtEstO)) ~ ".";
210                 enum name = prefix ~ __traits(getAttributes, eLtEstO)[0];
211                 enum hidden = false;
212                 enum shouldFail = false;
213                 enum singleThreaded = false;
214                 enum tags = string[].init;
215                 enum exceptionTypeInfo = TypeInfo.init;
216                 enum flakyRetries = 0;
217             } else {
218                 enum name = unittestName!(eLtEstO, index);
219                 enum hidden = hasUDA!(eLtEstO, HiddenTest);
220                 enum shouldFail = hasUDA!(eLtEstO, ShouldFail) || hasUDA!(eLtEstO, ShouldFailWith);
221                 enum singleThreaded = hasUDA!(eLtEstO, Serial);
222                 enum isTags(alias T) = is(typeof(T)) && is(typeof(T) == Tags);
223                 enum tags = tagsFromAttrs!(Filter!(isTags, __traits(getAttributes, eLtEstO)));
224                 enum exceptionTypeInfo = getExceptionTypeInfo!eLtEstO;
225                 enum flakyRetries = getFlakyRetries!(eLtEstO);
226             }
227             enum builtin = true;
228             enum suffix = "";
229 
230             testData ~= TestData(name,
231                                  () {
232                                      auto setup = getUDAFunction!(composite, Setup);
233                                      auto shutdown = getUDAFunction!(composite, Shutdown);
234 
235                                      if(setup) setup();
236                                      scope(exit) if(shutdown) shutdown();
237 
238                                      eLtEstO();
239                                  },
240                                  hidden,
241                                  shouldFail,
242                                  singleThreaded,
243                                  builtin,
244                                  suffix,
245                                  tags,
246                                  exceptionTypeInfo,
247                                  flakyRetries);
248         }
249     }
250 
251     // Keeps track of mangled names of everything visited.
252     bool[string] visitedMembers;
253 
254     void addUnitTestsRecursively(alias composite)() pure nothrow {
255 
256         if (composite.mangleof in visitedMembers)
257             return;
258 
259         visitedMembers[composite.mangleof] = true;
260         addMemberUnittests!composite();
261 
262         foreach(member; __traits(allMembers, composite)) {
263 
264             // isPrivate can't be used here. I don't know why.
265             static if(__traits(compiles, __traits(getProtection, __traits(getMember, module_, member))))
266                 enum notPrivate = __traits(getProtection, __traits(getMember, module_, member)) != "private";
267             else
268                 enum notPrivate = false;
269 
270             static if (
271                 notPrivate &&
272                 // If visibility of the member is deprecated, the next line still returns true
273                 // and yet spills deprecation warning. If deprecation is turned into error,
274                 // all works as intended.
275                 __traits(compiles, __traits(getMember, composite, member)) &&
276                 __traits(compiles, __traits(allMembers, __traits(getMember, composite, member))) &&
277                 __traits(compiles, recurse!(__traits(getMember, composite, member)))
278             ) {
279                 recurse!(__traits(getMember, composite, member));
280             }
281         }
282     }
283 
284     void recurse(child)() pure nothrow {
285         static if (is(child == class) || is(child == struct) || is(child == union)) {
286             addUnitTestsRecursively!child;
287         }
288     }
289 
290     addUnitTestsRecursively!module_();
291     return testData;
292 }
293 
294 private TypeInfo getExceptionTypeInfo(alias Test)() {
295     import unit_threaded.runner.attrs: ShouldFailWith;
296     import std.traits: hasUDA, getUDAs;
297 
298     static if(hasUDA!(Test, ShouldFailWith)) {
299         alias uda = getUDAs!(Test, ShouldFailWith)[0];
300         return typeid(uda.Type);
301     } else
302         return null;
303 }
304 
305 
306 private template isStringUDA(alias T) {
307     import std.traits: isSomeString;
308     static if(__traits(compiles, isSomeString!(typeof(T))))
309         enum isStringUDA = isSomeString!(typeof(T));
310     else
311         enum isStringUDA = false;
312 }
313 
314 @safe pure unittest {
315     static assert(isStringUDA!"foo");
316     static assert(!isStringUDA!5);
317 }
318 
319 private template isPrivate(alias module_, string moduleMember) {
320     alias ut_mmbr__ = Identity!(__traits(getMember, module_, moduleMember));
321 
322     static if(__traits(compiles, __traits(getProtection, ut_mmbr__)))
323         enum isPrivate = __traits(getProtection, ut_mmbr__) == "private";
324     else
325         enum isPrivate = true;
326 }
327 
328 
329 private int getFlakyRetries(alias test)() {
330     import unit_threaded.runner.attrs: Flaky;
331     import std.traits: getUDAs;
332     import std.conv: text;
333 
334     alias flakies = getUDAs!(test, Flaky);
335 
336     static assert(flakies.length == 0 || flakies.length == 1,
337                   text("Only 1 @Flaky allowed, found ", flakies.length, " on ",
338                        __traits(identifier, test)));
339 
340     static if(flakies.length == 1) {
341         static if(is(flakies[0]))
342             return Flaky.defaultRetries;
343         else
344             return flakies[0].retries;
345     } else
346         return 0;
347 }
348 
349 string[] tagsFromAttrs(T...)() {
350     static assert(T.length <= 1, "@Tags can only be applied once");
351     static if(T.length)
352         return T[0].values;
353     else
354         return [];
355 }