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 template isStringUDA(alias T) { 78 static if(__traits(compiles, is(typeof(T)) && isSomeString!T)) 79 enum isStringUDA = is(typeof(T)) && isSomeString!T; 80 else 81 enum isStringUDA = false; 82 } 83 84 enum strAttrs = Filter!(isStringUDA, __traits(getAttributes, test)); 85 86 enum hasName = nameAttrs.length || strAttrs.length == 1; 87 enum prefix = fullyQualifiedName!module_ ~ "."; 88 89 static if(hasName) { 90 static if(nameAttrs.length == 1) 91 return prefix ~ nameAttrs[0].value; 92 else 93 return prefix ~ strAttrs[0]; 94 } else { 95 string name; 96 try { 97 return prefix ~ "unittest" ~ (index).to!string; 98 } catch(Exception) { 99 assert(false, text("Error converting ", index, " to string")); 100 } 101 } 102 } 103 104 TestData[] testData; 105 foreach(index, test; __traits(getUnitTests, module_)) { 106 enum name = unittestName!(test, index); 107 enum hidden = hasUDA!(test, HiddenTest); 108 enum shouldFail = hasUDA!(test, ShouldFail); 109 enum singleThreaded = hasUDA!(test, Serial); 110 enum builtin = true; 111 testData ~= TestData(name, (){ test(); }, hidden, shouldFail, singleThreaded, builtin); 112 } 113 return testData; 114 } 115 116 117 /** 118 * Finds all test classes (classes implementing a test() function) 119 * in the given module 120 */ 121 TestData[] moduleTestClasses(alias module_)() pure nothrow { 122 123 template isTestClass(alias module_, string moduleMember) { 124 mixin("import " ~ fullyQualifiedName!module_ ~ ";"); //so it's visible 125 static if(!__traits(compiles, isAggregateType!(mixin(moduleMember)))) { 126 enum isTestClass = false; 127 } else static if(!isAggregateType!(mixin(moduleMember))) { 128 enum isTestClass = false; 129 } else static if(!__traits(compiles, mixin("new " ~ moduleMember))) { 130 enum isTestClass = false; //can't new it, can't use it 131 } else { 132 enum hasUnitTest = HasAttribute!(module_, moduleMember, UnitTest); 133 enum hasTestMethod = __traits(hasMember, mixin(moduleMember), "test"); 134 enum isTestClass = hasTestMethod || hasUnitTest; 135 } 136 } 137 138 return moduleTestData!(module_, isTestClass); 139 } 140 141 /** 142 * Finds all test functions in the given module. 143 * Returns an array of TestData structs 144 */ 145 TestData[] moduleTestFunctions(alias module_)() pure { 146 147 template isTestFunction(alias module_, string moduleMember) { 148 mixin("import " ~ fullyQualifiedName!module_ ~ ";"); //so it's visible 149 static if(isSomeFunction!(mixin(moduleMember))) { 150 enum isTestFunction = hasTestPrefix!(module_, moduleMember) || 151 HasAttribute!(module_, moduleMember, UnitTest); 152 } else { 153 enum isTestFunction = false; 154 } 155 } 156 157 template hasTestPrefix(alias module_, string member) { 158 import std.uni: isUpper; 159 mixin("import " ~ fullyQualifiedName!module_ ~ ";"); //so it's visible 160 161 enum prefix = "test"; 162 enum minSize = prefix.length + 1; 163 164 static if(isSomeFunction!(mixin(member)) && 165 member.length >= minSize && member[0 .. prefix.length] == prefix && 166 isUpper(member[prefix.length])) { 167 enum hasTestPrefix = true; 168 } else { 169 enum hasTestPrefix = false; 170 } 171 } 172 173 return moduleTestData!(module_, isTestFunction); 174 } 175 176 private struct TestFunctionSuffix { 177 TestFunction testFunction; 178 string suffix; 179 } 180 181 182 private TestData[] moduleTestData(alias module_, alias pred)() pure { 183 mixin("import " ~ fullyQualifiedName!module_ ~ ";"); //so it's visible 184 TestData[] testData; 185 foreach(moduleMember; __traits(allMembers, module_)) { 186 187 enum notPrivate = __traits(compiles, mixin(moduleMember)); //only way I know to check if private 188 189 static if(notPrivate && pred!(module_, moduleMember) && 190 !HasAttribute!(module_, moduleMember, DontTest)) { 191 192 TestFunctionSuffix[] getTestFunctions(alias module_, string moduleMember)() { 193 //returns delegates for test functions, null for test classes 194 static if(__traits(compiles, &__traits(getMember, module_, moduleMember))) { 195 enum func = &__traits(getMember, module_, moduleMember); 196 enum arity = arity!func; 197 198 static assert(arity == 0 || arity == 1, "Test functions may take at most one parameter"); 199 200 static if(arity == 0) 201 return [ TestFunctionSuffix((){ func(); }) ]; //simple case, just call it 202 else { 203 //check to see if the function has UDAs to call it with 204 alias params = Parameters!func; 205 static assert(params.length == 1, "Test functions may take at most one parameter"); 206 207 alias values = GetAttributes!(module_, moduleMember, params[0]); 208 import std.conv; 209 static assert(values.length > 0, 210 text("Test functions with a parameter of type <", params[0].stringof, 211 "> must have value UDAs of the same type")); 212 213 TestFunctionSuffix[] functions; 214 foreach(v; values) functions ~= TestFunctionSuffix((){ func(v); }, v.to!string); 215 return functions; 216 } 217 } else { 218 //test class 219 return [TestFunctionSuffix(null)]; 220 } 221 } 222 223 auto functions = getTestFunctions!(module_, moduleMember); 224 foreach(f; functions) { 225 //if there is more than one function, they're all single threaded - multiple values per test call. 226 immutable singleThreaded = functions.length > 1 || HasAttribute!(module_, moduleMember, Serial); 227 immutable builtin = false; 228 testData ~= TestData(fullyQualifiedName!module_~ "." ~ moduleMember, 229 f.testFunction, 230 HasAttribute!(module_, moduleMember, HiddenTest), 231 HasAttribute!(module_, moduleMember, ShouldFail), 232 singleThreaded, 233 builtin, 234 f.suffix); 235 } 236 } 237 } 238 239 return testData; 240 } 241 242 243 244 import unit_threaded.tests.module_with_tests; //defines tests and non-tests 245 import unit_threaded.asserts; 246 import std.algorithm; 247 import std.array; 248 249 //helper function for the unittest blocks below 250 private auto addModPrefix(string[] elements, string module_ = "unit_threaded.tests.module_with_tests") nothrow { 251 return elements.map!(a => module_ ~ "." ~ a).array; 252 } 253 254 unittest { 255 const expected = addModPrefix([ "FooTest", "BarTest", "Blergh"]); 256 const actual = moduleTestClasses!(unit_threaded.tests.module_with_tests).map!(a => a.name).array; 257 assertEqual(actual, expected); 258 } 259 260 unittest { 261 const expected = addModPrefix([ "testFoo", "testBar", "funcThatShouldShowUpCosOfAttr" ]); 262 const actual = moduleTestFunctions!(unit_threaded.tests.module_with_tests).map!(a => a.name).array; 263 assertEqual(actual, expected); 264 } 265 266 267 unittest { 268 const expected = addModPrefix(["unittest0", "unittest1", "myUnitTest"]); 269 const actual = moduleUnitTests!(unit_threaded.tests.module_with_tests).map!(a => a.name).array; 270 assertEqual(actual, expected); 271 }