1 module unit_threaded.reflection; 2 3 import unit_threaded.attrs; 4 import unit_threaded.uda; 5 import std.traits; 6 import std.typetuple; 7 8 /** 9 * Common data for test functions and test classes 10 */ 11 alias void delegate() TestFunction; 12 struct TestData { 13 string name; 14 TestFunction testFunction; ///only used for functions, null for classes 15 bool hidden; 16 bool shouldFail; 17 bool singleThreaded; 18 bool builtin; 19 string suffix; // append to end of getPath 20 } 21 22 23 /** 24 * Finds all test cases (functions, classes, built-in unittest blocks) 25 * Template parameters are module strings 26 */ 27 const(TestData)[] allTestData(MOD_STRINGS...)() if(allSatisfy!(isSomeString, typeof(MOD_STRINGS))) { 28 29 string getModulesString() { 30 import std.array: join; 31 string[] modules; 32 foreach(module_; MOD_STRINGS) modules ~= module_; 33 return modules.join(", "); 34 } 35 36 enum modulesString = getModulesString; 37 mixin("import " ~ modulesString ~ ";"); 38 mixin("return allTestData!(" ~ modulesString ~ ");"); 39 } 40 41 42 /** 43 * Finds all test cases (functions, classes, built-in unittest blocks) 44 * Template parameters are module symbols 45 */ 46 const(TestData)[] allTestData(MOD_SYMBOLS...)() if(!anySatisfy!(isSomeString, typeof(MOD_SYMBOLS))) { 47 auto allTestsWithFunc(string expr, MOD_SYMBOLS...)() pure { 48 //tests is whatever type expr returns 49 ReturnType!(mixin(expr ~ q{!(MOD_SYMBOLS[0])})) tests; 50 foreach(module_; TypeTuple!MOD_SYMBOLS) { 51 tests ~= mixin(expr ~ q{!module_()}); //e.g. tests ~= moduleTestClasses!module_ 52 } 53 return tests; 54 } 55 56 return allTestsWithFunc!(q{moduleTestClasses}, MOD_SYMBOLS) ~ 57 allTestsWithFunc!(q{moduleTestFunctions}, MOD_SYMBOLS) ~ 58 allTestsWithFunc!(q{moduleUnitTests}, MOD_SYMBOLS); 59 } 60 61 62 /** 63 * Finds all built-in unittest blocks in the given module. 64 * @return An array of TestData structs 65 */ 66 TestData[] moduleUnitTests(alias module_)() pure nothrow { 67 68 // Return a name for a unittest block. If no @Name UDA is found a name is 69 // created automatically, else the UDA is used. 70 string unittestName(alias test, int index)() @safe nothrow { 71 import std.conv; 72 mixin("import " ~ fullyQualifiedName!module_ ~ ";"); //so it's visible 73 74 enum nameAttrs = getUDAs!(test, Name); 75 static assert(nameAttrs.length == 0 || nameAttrs.length == 1, "Found multiple Name UDAs on unittest"); 76 77 enum strAttrs = Filter!(isStringUDA, __traits(getAttributes, test)); 78 enum hasName = nameAttrs.length || strAttrs.length == 1; 79 enum prefix = fullyQualifiedName!module_ ~ "."; 80 81 static if(hasName) { 82 static if(nameAttrs.length == 1) 83 return prefix ~ nameAttrs[0].value; 84 else 85 return prefix ~ strAttrs[0]; 86 } else { 87 string name; 88 try { 89 return prefix ~ "unittest" ~ (index).to!string; 90 } catch(Exception) { 91 assert(false, text("Error converting ", index, " to string")); 92 } 93 } 94 } 95 96 TestData[] testData; 97 foreach(index, test; __traits(getUnitTests, module_)) { 98 enum name = unittestName!(test, index); 99 enum hidden = hasUDA!(test, HiddenTest); 100 enum shouldFail = hasUDA!(test, ShouldFail); 101 enum singleThreaded = hasUDA!(test, Serial); 102 enum builtin = true; 103 testData ~= TestData(name, (){ test(); }, hidden, shouldFail, singleThreaded, builtin); 104 } 105 return testData; 106 } 107 108 private template isStringUDA(alias T) { 109 static if(__traits(compiles, isSomeString!(typeof(T)))) 110 enum isStringUDA = isSomeString!(typeof(T)); 111 else 112 enum isStringUDA = false; 113 } 114 115 unittest { 116 static assert(isStringUDA!"foo"); 117 static assert(!isStringUDA!5); 118 } 119 120 121 /** 122 * Finds all test classes (classes implementing a test() function) 123 * in the given module 124 */ 125 TestData[] moduleTestClasses(alias module_)() pure nothrow { 126 127 template isTestClass(alias module_, string moduleMember) { 128 mixin("import " ~ fullyQualifiedName!module_ ~ ";"); //so it's visible 129 static if(!__traits(compiles, isAggregateType!(mixin(moduleMember)))) { 130 enum isTestClass = false; 131 } else static if(!isAggregateType!(mixin(moduleMember))) { 132 enum isTestClass = false; 133 } else static if(!__traits(compiles, mixin("new " ~ moduleMember))) { 134 enum isTestClass = false; //can't new it, can't use it 135 } else { 136 enum hasUnitTest = HasAttribute!(module_, moduleMember, UnitTest); 137 enum hasTestMethod = __traits(hasMember, mixin(moduleMember), "test"); 138 enum isTestClass = hasTestMethod || hasUnitTest; 139 } 140 } 141 142 return moduleTestData!(module_, isTestClass); 143 } 144 145 /** 146 * Finds all test functions in the given module. 147 * Returns an array of TestData structs 148 */ 149 TestData[] moduleTestFunctions(alias module_)() pure { 150 151 template isTestFunction(alias module_, string moduleMember) { 152 mixin("import " ~ fullyQualifiedName!module_ ~ ";"); //so it's visible 153 static if(isSomeFunction!(mixin(moduleMember))) { 154 enum isTestFunction = hasTestPrefix!(module_, moduleMember) || 155 HasAttribute!(module_, moduleMember, UnitTest); 156 } else { 157 enum isTestFunction = false; 158 } 159 } 160 161 template hasTestPrefix(alias module_, string member) { 162 import std.uni: isUpper; 163 mixin("import " ~ fullyQualifiedName!module_ ~ ";"); //so it's visible 164 165 enum prefix = "test"; 166 enum minSize = prefix.length + 1; 167 168 static if(isSomeFunction!(mixin(member)) && 169 member.length >= minSize && member[0 .. prefix.length] == prefix && 170 isUpper(member[prefix.length])) { 171 enum hasTestPrefix = true; 172 } else { 173 enum hasTestPrefix = false; 174 } 175 } 176 177 return moduleTestData!(module_, isTestFunction); 178 } 179 180 private struct TestFunctionSuffix { 181 TestFunction testFunction; 182 string suffix; 183 } 184 185 186 private TestData[] moduleTestData(alias module_, alias pred)() pure { 187 mixin("import " ~ fullyQualifiedName!module_ ~ ";"); //so it's visible 188 TestData[] testData; 189 foreach(moduleMember; __traits(allMembers, module_)) { 190 191 enum notPrivate = __traits(compiles, mixin(moduleMember)); //only way I know to check if private 192 193 static if(notPrivate && pred!(module_, moduleMember) && 194 !HasAttribute!(module_, moduleMember, DontTest)) { 195 196 TestFunctionSuffix[] getTestFunctions(alias module_, string moduleMember)() { 197 //returns delegates for test functions, null for test classes 198 static if(__traits(compiles, &__traits(getMember, module_, moduleMember))) { 199 enum func = &__traits(getMember, module_, moduleMember); 200 enum arity = arity!func; 201 202 static assert(arity == 0 || arity == 1, "Test functions may take at most one parameter"); 203 204 static if(arity == 0) 205 return [ TestFunctionSuffix((){ func(); }) ]; //simple case, just call it 206 else { 207 //check to see if the function has UDAs to call it with 208 alias params = Parameters!func; 209 static assert(params.length == 1, "Test functions may take at most one parameter"); 210 211 alias values = GetAttributes!(module_, moduleMember, params[0]); 212 import std.conv; 213 static assert(values.length > 0, 214 text("Test functions with a parameter of type <", params[0].stringof, 215 "> must have value UDAs of the same type")); 216 217 TestFunctionSuffix[] functions; 218 foreach(v; values) functions ~= TestFunctionSuffix((){ func(v); }, v.to!string); 219 return functions; 220 } 221 } else { 222 //test class 223 return [TestFunctionSuffix(null)]; 224 } 225 } 226 227 auto functions = getTestFunctions!(module_, moduleMember); 228 foreach(f; functions) { 229 //if there is more than one function, they're all single threaded - multiple values per test call. 230 immutable singleThreaded = functions.length > 1 || HasAttribute!(module_, moduleMember, Serial); 231 immutable builtin = false; 232 testData ~= TestData(fullyQualifiedName!module_~ "." ~ moduleMember, 233 f.testFunction, 234 HasAttribute!(module_, moduleMember, HiddenTest), 235 HasAttribute!(module_, moduleMember, ShouldFail), 236 singleThreaded, 237 builtin, 238 f.suffix); 239 } 240 } 241 } 242 243 return testData; 244 } 245 246 247 248 import unit_threaded.tests.module_with_tests; //defines tests and non-tests 249 import unit_threaded.asserts; 250 import std.algorithm; 251 import std.array; 252 253 //helper function for the unittest blocks below 254 private auto addModPrefix(string[] elements, string module_ = "unit_threaded.tests.module_with_tests") nothrow { 255 return elements.map!(a => module_ ~ "." ~ a).array; 256 } 257 258 unittest { 259 const expected = addModPrefix([ "FooTest", "BarTest", "Blergh"]); 260 const actual = moduleTestClasses!(unit_threaded.tests.module_with_tests).map!(a => a.name).array; 261 assertEqual(actual, expected); 262 } 263 264 unittest { 265 const expected = addModPrefix([ "testFoo", "testBar", "funcThatShouldShowUpCosOfAttr" ]); 266 const actual = moduleTestFunctions!(unit_threaded.tests.module_with_tests).map!(a => a.name).array; 267 assertEqual(actual, expected); 268 } 269 270 271 unittest { 272 const expected = addModPrefix(["unittest0", "unittest1", "myUnitTest"]); 273 const actual = moduleUnitTests!(unit_threaded.tests.module_with_tests).map!(a => a.name).array; 274 assertEqual(actual, expected); 275 }