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 function() TestFunction; 12 struct TestData { 13 string name; 14 bool hidden; 15 bool shouldFail; 16 TestFunction testFunction; ///only used for functions, null for classes 17 bool singleThreaded; 18 bool builtin; 19 } 20 21 22 /** 23 * Finds all test cases (functions, classes, built-in unittest blocks) 24 * Template parameters are module strings 25 */ 26 const(TestData)[] allTestCaseData(MOD_STRINGS...)() if(allSatisfy!(isSomeString, typeof(MOD_STRINGS))) { 27 28 string getModulesString() { 29 import std.array: join; 30 string[] modules; 31 foreach(module_; MOD_STRINGS) modules ~= module_; 32 return modules.join(", "); 33 } 34 35 enum modulesString = getModulesString; 36 mixin("import " ~ modulesString ~ ";"); 37 mixin("return allTestCaseData!(" ~ modulesString ~ ");"); 38 } 39 40 41 /** 42 * Finds all test cases (functions, classes, built-in unittest blocks) 43 * Template parameters are module symbols 44 */ 45 const(TestData)[] allTestCaseData(MOD_SYMBOLS...)() if(!anySatisfy!(isSomeString, typeof(MOD_SYMBOLS))) { 46 auto allTestsWithFunc(string expr, MOD_SYMBOLS...)() pure nothrow { 47 //tests is whatever type expr returns 48 ReturnType!(mixin(expr ~ q{!(MOD_SYMBOLS[0])})) tests; 49 foreach(module_; TypeTuple!MOD_SYMBOLS) { 50 tests ~= mixin(expr ~ q{!module_()}); //e.g. tests ~= moduleTestClasses!module_ 51 } 52 return tests; 53 } 54 55 return allTestsWithFunc!(q{moduleTestClasses}, MOD_SYMBOLS) ~ 56 allTestsWithFunc!(q{moduleTestFunctions}, MOD_SYMBOLS) ~ 57 allTestsWithFunc!(q{moduleUnitTests}, MOD_SYMBOLS); 58 } 59 60 61 /** 62 * Finds all built-in unittest blocks in the given module. 63 * @return An array of TestData structs 64 */ 65 auto moduleUnitTests(alias module_)() pure nothrow { 66 67 // Return a name for a unittest block. If no @Name UDA is found a name is 68 // created automatically, else the UDA is used. 69 string unittestName(alias test, int index)() @safe nothrow { 70 import std.conv; 71 mixin("import " ~ fullyQualifiedName!module_ ~ ";"); //so it's visible 72 73 enum isName(alias T) = is(typeof(T)) && is(typeof(T) == Name); 74 alias names = Filter!(isName, __traits(getAttributes, test)); 75 static assert(names.length == 0 || names.length == 1, "Found multiple Name UDAs on unittest"); 76 enum prefix = fullyQualifiedName!module_ ~ "."; 77 78 static if(names.length == 1) { 79 return prefix ~ names[0].value; 80 } else { 81 string name; 82 try { 83 return prefix ~ "unittest" ~ (index).to!string; 84 } catch(Exception) { 85 assert(false, text("Error converting ", index, " to string")); 86 } 87 } 88 } 89 90 TestData[] testData; 91 foreach(index, test; __traits(getUnitTests, module_)) { 92 enum name = unittestName!(test, index); 93 enum hidden = false; 94 enum shouldFail = false; 95 enum singleThreaded = false; 96 enum builtin = true; 97 testData ~= TestData(name, hidden, shouldFail, &test, singleThreaded, builtin); 98 } 99 return testData; 100 } 101 102 103 /** 104 * Finds all test classes (classes implementing a test() function) 105 * in the given module 106 */ 107 auto moduleTestClasses(alias module_)() pure nothrow { 108 109 template isTestClass(alias module_, string moduleMember) { 110 mixin("import " ~ fullyQualifiedName!module_ ~ ";"); //so it's visible 111 static if(!__traits(compiles, isAggregateType!(mixin(moduleMember)))) { 112 enum isTestClass = false; 113 } else static if(!isAggregateType!(mixin(moduleMember))) { 114 enum isTestClass = false; 115 } else { 116 enum hasUnitTest = HasAttribute!(module_, moduleMember, UnitTest); 117 enum hasTestMethod = __traits(hasMember, mixin(moduleMember), "test"); 118 enum isTestClass = hasTestMethod || hasUnitTest; 119 } 120 } 121 122 return moduleTestCases!(module_, isTestClass); 123 } 124 125 /** 126 * Finds all test functions in the given module. 127 * Returns an array of TestData structs 128 */ 129 auto moduleTestFunctions(alias module_)() pure nothrow { 130 131 template isTestFunction(alias module_, string moduleMember) { 132 mixin("import " ~ fullyQualifiedName!module_ ~ ";"); //so it's visible 133 static if(isSomeFunction!(mixin(moduleMember))) { 134 enum isTestFunction = hasTestPrefix!(module_, moduleMember) || 135 HasAttribute!(module_, moduleMember, UnitTest); 136 } else { 137 enum isTestFunction = false; 138 } 139 } 140 141 template hasTestPrefix(alias module_, string member) { 142 import std.uni: isUpper; 143 mixin("import " ~ fullyQualifiedName!module_ ~ ";"); //so it's visible 144 145 enum prefix = "test"; 146 enum minSize = prefix.length + 1; 147 148 static if(isSomeFunction!(mixin(member)) && 149 member.length >= minSize && member[0 .. prefix.length] == prefix && 150 isUpper(member[prefix.length])) { 151 enum hasTestPrefix = true; 152 } else { 153 enum hasTestPrefix = false; 154 } 155 } 156 157 return moduleTestCases!(module_, isTestFunction); 158 } 159 160 161 private auto moduleTestCases(alias module_, alias pred)() pure nothrow { 162 mixin("import " ~ fullyQualifiedName!module_ ~ ";"); //so it's visible 163 TestData[] testData; 164 foreach(moduleMember; __traits(allMembers, module_)) { 165 166 enum notPrivate = __traits(compiles, mixin(moduleMember)); //only way I know to check if private 167 168 static if(notPrivate && pred!(module_, moduleMember) && 169 !HasAttribute!(module_, moduleMember, DontTest)) { 170 171 TestFunction getTestFunction(alias module_, string moduleMember)() pure nothrow { 172 //returns a function pointer for test functions, null for test classes 173 static if(__traits(compiles, &__traits(getMember, module_, moduleMember))) { 174 return &__traits(getMember, module_, moduleMember); 175 } else { 176 return null; 177 } 178 } 179 180 testData ~= TestData(fullyQualifiedName!module_~ "." ~ moduleMember, 181 HasAttribute!(module_, moduleMember, HiddenTest), 182 HasAttribute!(module_, moduleMember, ShouldFail), 183 getTestFunction!(module_, moduleMember), 184 HasAttribute!(module_, moduleMember, SingleThreaded)); 185 } 186 } 187 188 return testData; 189 } 190 191 192 193 import unit_threaded.tests.module_with_tests; //defines tests and non-tests 194 import unit_threaded.asserts; 195 import std.algorithm; 196 import std.array; 197 198 //helper function for the unittest blocks below 199 private auto addModPrefix(string[] elements, string module_ = "unit_threaded.tests.module_with_tests") nothrow { 200 return elements.map!(a => module_ ~ "." ~ a).array; 201 } 202 203 unittest { 204 const expected = addModPrefix([ "FooTest", "BarTest", "Blergh"]); 205 const actual = moduleTestClasses!(unit_threaded.tests.module_with_tests).map!(a => a.name).array; 206 assertEqual(actual, expected); 207 } 208 209 unittest { 210 const expected = addModPrefix([ "testFoo", "testBar", "funcThatShouldShowUpCosOfAttr" ]); 211 const actual = moduleTestFunctions!(unit_threaded.tests.module_with_tests).map!(a => a.name).array; 212 assertEqual(actual, expected); 213 } 214 215 216 unittest { 217 const expected = addModPrefix(["unittest0", "unittest1", "myUnitTest"]); 218 const actual = moduleUnitTests!(unit_threaded.tests.module_with_tests).map!(a => a.name).array; 219 assertEqual(actual, expected); 220 }