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 /** 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 _options = options; 40 _testData = testData; 41 _testCases = createTestCases(testData, options.testsToRun); 42 WriterThread.start; 43 } 44 45 ~this() { 46 WriterThread.get.join; 47 } 48 49 /** 50 * Runs all test cases. 51 * Returns: true if no test failed, false otherwise. 52 */ 53 bool run() { 54 if (!_testCases.length) { 55 utWritelnRed("Error! No tests to run for args: "); 56 utWriteln(_options.testsToRun); 57 return false; 58 } 59 60 immutable elapsed = doRun(); 61 62 if (!numTestsRun) { 63 utWriteln("Did not run any tests!!!"); 64 return false; 65 } 66 67 utWriteln("\nTime taken: ", elapsed); 68 utWrite(numTestsRun, " test(s) run, "); 69 const failuresStr = text(_failures.length, " failed"); 70 if (_failures.length) { 71 utWriteRed(failuresStr); 72 } else { 73 utWrite(failuresStr); 74 } 75 76 ulong numTestsWithAttr(string attr)() { 77 return _testData.filter!(a => mixin("a. " ~ attr)).count; 78 } 79 80 void printHidden() { 81 const num = numTestsWithAttr!"hidden"; 82 if(!num) return; 83 utWrite(", "); 84 utWriteYellow(num, " ", "hidden"); 85 } 86 87 void printShouldFail() { 88 const total = numTestsWithAttr!"shouldFail"; 89 ulong num = total; 90 91 foreach(f; _failures) { 92 const data = _testData.filter!(a => a.getPath == f).front; 93 if(data.shouldFail) --num; 94 } 95 96 if(!total) return; 97 utWrite(", "); 98 utWriteYellow(num, "/", total, " ", "failing as expected"); 99 } 100 101 printHidden(); 102 printShouldFail(); 103 104 utWriteln(".\n"); 105 106 if (_failures.length) { 107 utWritelnRed("Tests failed!\n"); 108 return false; //oops 109 } 110 111 utWritelnGreen("OK!\n"); 112 113 return true; 114 } 115 116 private: 117 118 const(Options) _options; 119 const(TestData)[] _testData; 120 TestCase[] _testCases; 121 string[] _failures; 122 StopWatch _stopWatch; 123 124 /** 125 * Runs the tests with the given options. 126 * Returns: how long it took to run. 127 */ 128 Duration doRun() { 129 auto tests = getTests(); 130 131 if(_options.showChrono) 132 foreach(test; tests) 133 test.showChrono; 134 135 _stopWatch.start(); 136 137 if (_options.multiThreaded) { 138 _failures = reduce!((a, b) => a ~ b)(_failures, taskPool.amap!runTest(tests)); 139 } else { 140 foreach (test; tests) { 141 _failures ~= test(); 142 } 143 } 144 145 handleFailures(); 146 147 _stopWatch.stop(); 148 return cast(Duration) _stopWatch.peek(); 149 } 150 151 auto getTests() { 152 auto tests = _testCases.dup; 153 if (_options.random) { 154 import std.random; 155 156 auto generator = Random(_options.seed); 157 tests.randomShuffle(generator); 158 utWriteln("Running tests in random order. ", 159 "To repeat this run, use --seed ", _options.seed); 160 } 161 return tests; 162 } 163 164 void handleFailures() const { 165 if (!_failures.empty) 166 utWriteln(""); 167 foreach (failure; _failures) { 168 utWrite("Test ", (failure.canFind(" ") ? `"` ~ failure ~ `"` : failure), " "); 169 utWriteRed("failed"); 170 utWriteln("."); 171 } 172 if (!_failures.empty) 173 utWriteln(""); 174 } 175 176 @property ulong numTestsRun() @trusted const { 177 return _testCases.map!(a => a.numTestsRun).reduce!((a, b) => a + b); 178 } 179 } 180 181 /** 182 * Replace the D runtime's normal unittest block tester. If this is not done, 183 * the tests will run twice. 184 */ 185 void replaceModuleUnitTester() { 186 import core.runtime; 187 188 Runtime.moduleUnitTester = &moduleUnitTester; 189 } 190 191 shared static this() { 192 replaceModuleUnitTester(); 193 } 194 195 /** 196 * Replacement for the usual unittest runner. Since unit_threaded 197 * runs the tests itself, the moduleUnitTester doesn't really have to do anything. 198 */ 199 private bool moduleUnitTester() { 200 //this is so unit-threaded's own tests run 201 foreach(module_; ModuleInfo) { 202 if(module_ && module_.unitTest && 203 module_.name.startsWith("unit_threaded") && // we want to run the "normal" unit tests 204 !module_.name.startsWith("unit_threaded.tests")) { //but not the ones from the test modules 205 module_.unitTest()(); 206 } 207 } 208 209 return true; 210 }