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