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