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 }