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