1 /**
2  * This module implements $(D TestSuite), an aggregator for $(D TestCase)
3  * objects to run all tests.
4  */
5 
6 module unit_threaded.runner.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.runner.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.runner.io: Output;
24     import unit_threaded.runner.options: Options;
25     import unit_threaded.runner.reflection: TestData;
26     import unit_threaded.runner.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     /**
34      * Params:
35      * options = The options to run tests with.
36      * testData = The information about the tests to run.
37      */
38     this(in Options options, in TestData[] testData) {
39         import unit_threaded.runner.io: WriterThread;
40         this(options, testData, WriterThread.get);
41     }
42 
43     /**
44      * Params:
45      * options = The options to run tests with.
46      * testData = The information about the tests to run.
47      * output = Where to send text output.
48      */
49     this(in Options options, in TestData[] testData, Output output) {
50         import unit_threaded.runner.factory: createTestCases;
51 
52         _options = options;
53         _testData = testData;
54         _output = output;
55         _testCases = createTestCases(testData, options.testsToRun);
56     }
57 
58     /**
59      * Runs all test cases.
60      * Returns: true if no test failed, false otherwise.
61      */
62     bool run() {
63 
64         import unit_threaded.runner.io: writelnRed, writeln, writeRed, write, writeYellow, writelnGreen;
65         import std.algorithm: filter, count;
66         import std.conv: text;
67 
68         if (!_testCases.length) {
69             _output.writelnRed("Error! No tests to run for args: ");
70             _output.writeln(_options.testsToRun);
71             return false;
72         }
73 
74         immutable elapsed = doRun();
75 
76         if (!numTestsRun) {
77             _output.writeln("Did not run any tests!!!");
78             return false;
79         }
80 
81         _output.writeln("\nTime taken: ", elapsed);
82         _output.write(numTestsRun, " test(s) run, ");
83         const failuresStr = text(_failures.length, " failed");
84         if (_failures.length) {
85             _output.writeRed(failuresStr);
86         } else {
87             _output.write(failuresStr);
88         }
89 
90         ulong numTestsWithAttr(string attr)() {
91            return _testData.filter!(a => mixin("a. " ~ attr)).count;
92         }
93 
94         void printHidden() {
95             const num = numTestsWithAttr!"hidden";
96             if(!num) return;
97             _output.write(", ");
98             _output.writeYellow(num, " ", "hidden");
99         }
100 
101         void printShouldFail() {
102             const total = _testCases.filter!(a => a.shouldFail).count;
103             long num = total;
104 
105             foreach(f; _failures) {
106                 const data = _testData.filter!(a => a.getPath == f).front;
107                 if(data.shouldFail) --num;
108             }
109 
110             if(!total) return;
111             _output.write(", ");
112             _output.writeYellow(num, "/", total, " ", "failing as expected");
113         }
114 
115         printHidden();
116         printShouldFail();
117 
118         _output.writeln(".\n");
119 
120         if(_options.random)
121             _output.writeln("Tests were run in random order. To repeat this run, use --seed ", _options.seed, "\n");
122 
123         if (_failures.length) {
124             _output.writelnRed("Tests failed!\n");
125             return false; //oops
126         }
127 
128         _output.writelnGreen("OK!\n");
129 
130         return true;
131     }
132 
133 private:
134 
135     const(Options) _options;
136     const(TestData)[] _testData;
137     TestCase[] _testCases;
138     string[] _failures;
139     StopWatch _stopWatch;
140     Output _output;
141 
142     /**
143      * Runs the tests.
144      * Returns: how long it took to run.
145      */
146     Duration doRun() {
147 
148         import std.algorithm: reduce;
149         import std.parallelism: taskPool;
150 
151         auto tests = getTests();
152 
153         if(_options.showChrono)
154             foreach(test; tests)
155                 test.showChrono;
156 
157         if(_options.quiet)
158             foreach(test; tests)
159                 test.quiet;
160 
161         _stopWatch.start();
162 
163         if (_options.multiThreaded) {
164             _failures = reduce!((a, b) => a ~ b)(_failures, taskPool.amap!runTest(tests));
165         } else {
166             foreach (test; tests) {
167                 _failures ~= test();
168             }
169         }
170 
171         handleFailures();
172 
173         _stopWatch.stop();
174         return cast(Duration) _stopWatch.peek();
175     }
176 
177     auto getTests() {
178         import unit_threaded.runner.io: writeln;
179 
180         auto tests = _testCases.dup;
181 
182         if (_options.random) {
183             import std.random;
184 
185             auto generator = Random(_options.seed);
186             tests.randomShuffle(generator);
187             _output.writeln("Running tests in random order. ",
188                 "To repeat this run, use --seed ", _options.seed);
189         }
190 
191         return tests;
192     }
193 
194     void handleFailures() {
195         import unit_threaded.runner.io: writeln, writeRed, write;
196         import std.array: empty;
197         import std.algorithm: canFind;
198 
199         if (!_failures.empty)
200             _output.writeln("");
201         foreach (failure; _failures) {
202             _output.write("Test ", (failure.canFind(" ") ? `'` ~ failure ~ `'` : failure), " ");
203             _output.writeRed("failed");
204             _output.writeln(".");
205         }
206         if (!_failures.empty)
207             _output.writeln("");
208     }
209 
210     @property ulong numTestsRun() @trusted const {
211         import std.algorithm: map, reduce;
212         return _testCases.map!(a => a.numTestsRun).reduce!((a, b) => a + b);
213     }
214 }