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 }