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