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, in 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, in 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 (!_testCases.length) { 69 _output.writelnRed("Error! No tests to run for args: "); 70 _output.writeln(_options.testsToRun); 71 return false; 72 } 73 74 immutable elapsed = doRun(); 75 76 if (!numTestsRun) { 77 _output.writeln("Did not run any tests!!!"); 78 return false; 79 } 80 81 _output.writeln("\nTime taken: ", elapsed); 82 _output.write(numTestsRun, " test(s) run, "); 83 const failuresStr = text(_failures.length, " failed"); 84 if (_failures.length) { 85 _output.writeRed(failuresStr); 86 } else { 87 _output.write(failuresStr); 88 } 89 90 ulong numTestsWithAttr(string attr)() { 91 return _testData.filter!(a => mixin("a. " ~ attr)).count; 92 } 93 94 void printHidden() { 95 const num = numTestsWithAttr!"hidden"; 96 if(!num) return; 97 _output.write(", "); 98 _output.writeYellow(num, " ", "hidden"); 99 } 100 101 void printShouldFail() { 102 const total = _testCases.filter!(a => a.shouldFail).count; 103 long num = total; 104 105 foreach(f; _failures) { 106 const data = _testData.filter!(a => a.getPath == f).front; 107 if(data.shouldFail) --num; 108 } 109 110 if(!total) return; 111 _output.write(", "); 112 _output.writeYellow(num, "/", total, " ", "failing as expected"); 113 } 114 115 printHidden(); 116 printShouldFail(); 117 118 _output.writeln(".\n"); 119 120 if(_options.random) 121 _output.writeln("Tests were run in random order. To repeat this run, use --seed ", _options.seed, "\n"); 122 123 if (_failures.length) { 124 _output.writelnRed("Tests failed!\n"); 125 return false; //oops 126 } 127 128 _output.writelnGreen("OK!\n"); 129 130 return true; 131 } 132 133 private: 134 135 const(Options) _options; 136 const(TestData)[] _testData; 137 TestCase[] _testCases; 138 string[] _failures; 139 StopWatch _stopWatch; 140 Output _output; 141 142 /** 143 * Runs the tests. 144 * Returns: how long it took to run. 145 */ 146 Duration doRun() { 147 148 import std.algorithm: reduce; 149 import std.parallelism: taskPool; 150 151 auto tests = getTests(); 152 153 if(_options.showChrono) 154 foreach(test; tests) 155 test.showChrono; 156 157 if(_options.quiet) 158 foreach(test; tests) 159 test.quiet; 160 161 _stopWatch.start(); 162 163 if (_options.multiThreaded) { 164 _failures = reduce!((a, b) => a ~ b)(_failures, taskPool.amap!runTest(tests)); 165 } else { 166 foreach (test; tests) { 167 _failures ~= test(); 168 } 169 } 170 171 handleFailures(); 172 173 _stopWatch.stop(); 174 return cast(Duration) _stopWatch.peek(); 175 } 176 177 auto getTests() { 178 import unit_threaded.runner.io: writeln; 179 180 auto tests = _testCases.dup; 181 182 if (_options.random) { 183 import std.random; 184 185 auto generator = Random(_options.seed); 186 tests.randomShuffle(generator); 187 _output.writeln("Running tests in random order. ", 188 "To repeat this run, use --seed ", _options.seed); 189 } 190 191 return tests; 192 } 193 194 void handleFailures() { 195 import unit_threaded.runner.io: writeln, writeRed, write; 196 import std.array: empty; 197 import std.algorithm: canFind; 198 199 if (!_failures.empty) 200 _output.writeln(""); 201 foreach (failure; _failures) { 202 _output.write("Test ", (failure.canFind(" ") ? `'` ~ failure ~ `'` : failure), " "); 203 _output.writeRed("failed"); 204 _output.writeln("."); 205 } 206 if (!_failures.empty) 207 _output.writeln(""); 208 } 209 210 @property ulong numTestsRun() @trusted const { 211 import std.algorithm: map, reduce; 212 return _testCases.map!(a => a.numTestsRun).reduce!((a, b) => a + b); 213 } 214 }