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 }