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