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