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