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 _stopWatch.start(); 131 132 if (_options.multiThreaded) { 133 _failures = reduce!((a, b) => a ~ b)(_failures, taskPool.amap!runTest(tests)); 134 } else { 135 foreach (test; tests) 136 _failures ~= test(); 137 } 138 139 handleFailures(); 140 141 _stopWatch.stop(); 142 return cast(Duration) _stopWatch.peek(); 143 } 144 145 auto getTests() { 146 auto tests = _testCases.dup; 147 if (_options.random) { 148 import std.random; 149 150 auto generator = Random(_options.seed); 151 tests.randomShuffle(generator); 152 utWriteln("Running tests in random order. ", 153 "To repeat this run, use --seed ", _options.seed); 154 } 155 return tests; 156 } 157 158 void handleFailures() const { 159 if (!_failures.empty) 160 utWriteln(""); 161 foreach (failure; _failures) { 162 utWrite("Test ", (failure.canFind(" ") ? `"` ~ failure ~ `"` : failure), " "); 163 utWriteRed("failed"); 164 utWriteln("."); 165 } 166 if (!_failures.empty) 167 utWriteln(""); 168 } 169 170 @property ulong numTestsRun() @trusted const { 171 return _testCases.map!(a => a.numTestsRun).reduce!((a, b) => a + b); 172 } 173 } 174 175 /** 176 * Replace the D runtime's normal unittest block tester. If this is not done, 177 * the tests will run twice. 178 */ 179 void replaceModuleUnitTester() { 180 import core.runtime; 181 182 Runtime.moduleUnitTester = &moduleUnitTester; 183 } 184 185 shared static this() { 186 replaceModuleUnitTester(); 187 } 188 189 /** 190 * Replacement for the usual unittest runner. Since unit_threaded 191 * runs the tests itself, the moduleUnitTester doesn't really have to do anything. 192 */ 193 private bool moduleUnitTester() { 194 //this is so unit-threaded's own tests run 195 foreach(module_; ModuleInfo) { 196 if(module_ && module_.unitTest && 197 module_.name.startsWith("unit_threaded") && // we want to run the "normal" unit tests 198 !module_.name.startsWith("unit_threaded.tests")) { //but not the ones from the test modules 199 module_.unitTest()(); 200 } 201 } 202 203 return true; 204 }