1 /** 2 * This module implements $(D TestSuite), an aggregator for $(D TestCase) 3 * objects to run all tests. 4 */ 5 6 module unit_threaded.testsuite; 7 8 import unit_threaded.testcase: TestCase; 9 10 /* 11 * taskPool.amap only works with public functions, not closures. 12 */ 13 auto runTest(TestCase test) 14 { 15 return test(); 16 } 17 18 /** 19 * Responsible for running tests and printing output. 20 */ 21 struct TestSuite 22 { 23 import unit_threaded.io: Output; 24 import unit_threaded.options: Options; 25 import unit_threaded.reflection: TestData; 26 import std.datetime: StopWatch, Duration; 27 28 package Output output; 29 30 this(in Options options, in TestData[] testData) { 31 import unit_threaded.io: WriterThread; 32 this(options, testData, WriterThread.get); 33 } 34 35 /** 36 * Params: 37 * options = The options to run tests with. 38 * testData = The information about the tests to run. 39 */ 40 this(in Options options, in TestData[] testData, Output output) { 41 import unit_threaded.factory: createTestCases; 42 43 _options = options; 44 _testData = testData; 45 _output = output; 46 _testCases = createTestCases(testData, options.testsToRun); 47 } 48 49 ~this() { 50 import unit_threaded.io: WriterThread; 51 WriterThread.stop; 52 } 53 54 /** 55 * Runs all test cases. 56 * Returns: true if no test failed, false otherwise. 57 */ 58 bool run() { 59 60 import unit_threaded.io: writelnRed, writeln, writeRed, write, writeYellow, writelnGreen; 61 import std.algorithm: filter, count; 62 import std.conv: text; 63 64 if (!_testCases.length) { 65 _output.writelnRed("Error! No tests to run for args: "); 66 _output.writeln(_options.testsToRun); 67 return false; 68 } 69 70 immutable elapsed = doRun(); 71 72 if (!numTestsRun) { 73 _output.writeln("Did not run any tests!!!"); 74 return false; 75 } 76 77 _output.writeln("\nTime taken: ", elapsed); 78 _output.write(numTestsRun, " test(s) run, "); 79 const failuresStr = text(_failures.length, " failed"); 80 if (_failures.length) { 81 _output.writeRed(failuresStr); 82 } else { 83 _output.write(failuresStr); 84 } 85 86 ulong numTestsWithAttr(string attr)() { 87 return _testData.filter!(a => mixin("a. " ~ attr)).count; 88 } 89 90 void printHidden() { 91 const num = numTestsWithAttr!"hidden"; 92 if(!num) return; 93 _output.write(", "); 94 _output.writeYellow(num, " ", "hidden"); 95 } 96 97 void printShouldFail() { 98 const total = numTestsWithAttr!"shouldFail"; 99 ulong num = total; 100 101 foreach(f; _failures) { 102 const data = _testData.filter!(a => a.getPath == f).front; 103 if(data.shouldFail) --num; 104 } 105 106 if(!total) return; 107 _output.write(", "); 108 _output.writeYellow(num, "/", total, " ", "failing as expected"); 109 } 110 111 printHidden(); 112 printShouldFail(); 113 114 _output.writeln(".\n"); 115 116 if (_failures.length) { 117 _output.writelnRed("Tests failed!\n"); 118 return false; //oops 119 } 120 121 _output.writelnGreen("OK!\n"); 122 123 return true; 124 } 125 126 private: 127 128 const(Options) _options; 129 const(TestData)[] _testData; 130 TestCase[] _testCases; 131 string[] _failures; 132 StopWatch _stopWatch; 133 Output _output; 134 135 /** 136 * Runs the tests with the given options. 137 * Returns: how long it took to run. 138 */ 139 Duration doRun() { 140 141 import std.algorithm: reduce; 142 import std.parallelism: taskPool; 143 144 auto tests = getTests(); 145 146 if(_options.showChrono) 147 foreach(test; tests) 148 test.showChrono; 149 150 _stopWatch.start(); 151 152 if (_options.multiThreaded) { 153 _failures = reduce!((a, b) => a ~ b)(_failures, taskPool.amap!runTest(tests)); 154 } else { 155 foreach (test; tests) { 156 _failures ~= test(); 157 } 158 } 159 160 handleFailures(); 161 162 _stopWatch.stop(); 163 return cast(Duration) _stopWatch.peek(); 164 } 165 166 auto getTests() { 167 import unit_threaded.io: writeln; 168 169 auto tests = _testCases.dup; 170 if (_options.random) { 171 import std.random; 172 173 auto generator = Random(_options.seed); 174 tests.randomShuffle(generator); 175 _output.writeln("Running tests in random order. ", 176 "To repeat this run, use --seed ", _options.seed); 177 } 178 return tests; 179 } 180 181 void handleFailures() { 182 import unit_threaded.io: writeln, writeRed, write; 183 import std.array: empty; 184 import std.algorithm: canFind; 185 186 if (!_failures.empty) 187 _output.writeln(""); 188 foreach (failure; _failures) { 189 _output.write("Test ", (failure.canFind(" ") ? `"` ~ failure ~ `"` : failure), " "); 190 _output.writeRed("failed"); 191 _output.writeln("."); 192 } 193 if (!_failures.empty) 194 _output.writeln(""); 195 } 196 197 @property ulong numTestsRun() @trusted const { 198 import std.algorithm: map, reduce; 199 return _testCases.map!(a => a.numTestsRun).reduce!((a, b) => a + b); 200 } 201 } 202 203 /** 204 * Replace the D runtime's normal unittest block tester. If this is not done, 205 * the tests will run twice. 206 */ 207 void replaceModuleUnitTester() { 208 import core.runtime: Runtime; 209 Runtime.moduleUnitTester = &moduleUnitTester; 210 } 211 212 shared static this() { 213 replaceModuleUnitTester; 214 } 215 216 /** 217 * Replacement for the usual unittest runner. Since unit_threaded 218 * runs the tests itself, the moduleUnitTester doesn't really have to do anything. 219 */ 220 private bool moduleUnitTester() { 221 //this is so unit-threaded's own tests run 222 import std.algorithm: startsWith; 223 foreach(module_; ModuleInfo) { 224 if(module_ && module_.unitTest && 225 module_.name.startsWith("unit_threaded") && // we want to run the "normal" unit tests 226 //!module_.name.startsWith("unit_threaded.property") && // left here for fast iteration when developing 227 !module_.name.startsWith("unit_threaded.tests")) { //but not the ones from the test modules 228 version(testing_unit_threaded) { 229 import std.stdio: writeln; 230 writeln("Running unit-threaded UT for module " ~ module_.name); 231 } 232 module_.unitTest()(); 233 234 } 235 } 236 237 return true; 238 }