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