1 /** 2 * This module implements $(D TestSuite), an aggregator for $(D TestCase) 3 * objects to run all tests. 4 */ 5 6 module unit_threaded.runner.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.runner.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.runner.io: Output; 24 import unit_threaded.runner.options: Options; 25 import unit_threaded.runner.reflection: TestData; 26 import unit_threaded.runner.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 /** 34 * Params: 35 * options = The options to run tests with. 36 * testData = The information about the tests to run. 37 */ 38 this(in Options options, const(TestData)[] testData) { 39 import unit_threaded.runner.io: WriterThread; 40 this(options, testData, WriterThread.get); 41 } 42 43 /** 44 * Params: 45 * options = The options to run tests with. 46 * testData = The information about the tests to run. 47 * output = Where to send text output. 48 */ 49 this(in Options options, const(TestData)[] testData, Output output) { 50 import unit_threaded.runner.factory: createTestCases; 51 52 _options = options; 53 _testData = testData; 54 _output = output; 55 _testCases = createTestCases(testData, options.testsToRun); 56 } 57 58 /** 59 * Runs all test cases. 60 * Returns: true if no test failed, false otherwise. 61 */ 62 bool run() { 63 64 import unit_threaded.runner.io: writelnRed, writeln, writeRed, write, writeYellow, writelnGreen; 65 import std.algorithm: filter, count; 66 import std.conv: text; 67 68 if (!_testData.length) { 69 _output.writeln("No tests to run"); 70 _output.writelnGreen("OK!\n"); 71 return true; 72 } 73 74 if (!_testCases.length) { 75 _output.writelnRed("Error! No tests to run for args: "); 76 _output.writeln(_options.testsToRun); 77 return false; 78 } 79 80 immutable elapsed = doRun(); 81 82 if (!numTestsRun) { 83 _output.writeln("Did not run any tests!!!"); 84 return false; 85 } 86 87 _output.writeln("\nTime taken: ", elapsed); 88 _output.write(numTestsRun, " test(s) run, "); 89 const failuresStr = text(_failures.length, " failed"); 90 if (_failures.length) { 91 _output.writeRed(failuresStr); 92 } else { 93 _output.write(failuresStr); 94 } 95 96 ulong numTestsWithAttr(string attr)() { 97 return _testData.filter!(a => mixin("a. " ~ attr)).count; 98 } 99 100 void printHidden() { 101 const num = numTestsWithAttr!"hidden"; 102 if(!num) return; 103 _output.write(", "); 104 _output.writeYellow(num, " ", "hidden"); 105 } 106 107 void printShouldFail() { 108 const total = _testCases.filter!(a => a.shouldFail).count; 109 long num = total; 110 111 foreach(f; _failures) { 112 const data = _testData.filter!(a => a.getPath == f).front; 113 if(data.shouldFail) --num; 114 } 115 116 if(!total) return; 117 _output.write(", "); 118 _output.writeYellow(num, "/", total, " ", "failing as expected"); 119 } 120 121 printHidden(); 122 printShouldFail(); 123 124 _output.writeln(".\n"); 125 126 if(_options.random) 127 _output.writeln("Tests were run in random order. To repeat this run, use --seed ", _options.seed, "\n"); 128 129 if (_failures.length) { 130 _output.writelnRed("Tests failed!\n"); 131 return false; //oops 132 } 133 134 _output.writelnGreen("OK!\n"); 135 136 return true; 137 } 138 139 private: 140 141 const(Options) _options; 142 const(TestData)[] _testData; 143 TestCase[] _testCases; 144 string[] _failures; 145 StopWatch _stopWatch; 146 Output _output; 147 148 /** 149 * Runs the tests. 150 * Returns: how long it took to run. 151 */ 152 Duration doRun() { 153 154 import std.algorithm: reduce; 155 import std.parallelism: TaskPool; 156 157 auto tests = getTests(); 158 159 if(_options.showChrono) 160 foreach(test; tests) 161 test.showChrono; 162 163 if(_options.quiet) 164 foreach(test; tests) 165 test.quiet; 166 167 _stopWatch.start(); 168 169 if (_options.multiThreaded) { 170 // use a dedicated task pool with non-daemon worker threads 171 auto taskPool = new TaskPool; 172 _failures = reduce!((a, b) => a ~ b)(_failures, taskPool.amap!runTest(tests)); 173 taskPool.finish(/*blocking=*/false); 174 } else { 175 foreach (test; tests) { 176 _failures ~= test(); 177 } 178 } 179 180 version(Windows) { 181 // spawned child processes etc. may have tampered with the console, 182 // try to re-enable the ANSI escape codes for colors 183 import unit_threaded.runner.io: tryEnableEscapeCodes; 184 tryEnableEscapeCodes(); 185 } 186 187 handleFailures(); 188 189 _stopWatch.stop(); 190 return cast(Duration) _stopWatch.peek(); 191 } 192 193 auto getTests() { 194 import unit_threaded.runner.io: writeln; 195 196 auto tests = _testCases.dup; 197 198 if (_options.random) { 199 import std.random; 200 201 auto generator = Random(_options.seed); 202 tests.randomShuffle(generator); 203 _output.writeln("Running tests in random order. ", 204 "To repeat this run, use --seed ", _options.seed); 205 } 206 207 return tests; 208 } 209 210 void handleFailures() { 211 import unit_threaded.runner.io: writeln, writeRed, write; 212 import std.array: empty; 213 import std.algorithm: canFind; 214 215 if (!_failures.empty) 216 _output.writeln(""); 217 foreach (failure; _failures) { 218 _output.write("Test ", (failure.canFind(" ") ? `'` ~ failure ~ `'` : failure), " "); 219 _output.writeRed("failed"); 220 _output.writeln("."); 221 } 222 if (!_failures.empty) 223 _output.writeln(""); 224 } 225 226 @property ulong numTestsRun() @trusted const { 227 import std.algorithm: map, reduce; 228 return _testCases.map!(a => a.numTestsRun).reduce!((a, b) => a + b); 229 } 230 }