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 }