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, const(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, const(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 (!_testData.length) {
69             _output.writeln("No tests to run");
70             _output.writelnGreen("OK!\n");
71             return true;
72         }
73 
74         if (!_testCases.length) {
75             _output.writelnRed("Error! No tests to run for args: ");
76             _output.writeln(_options.testsToRun);
77             return false;
78         }
79 
80         immutable elapsed = doRun();
81 
82         if (!numTestsRun) {
83             _output.writeln("Did not run any tests!!!");
84             return false;
85         }
86 
87         _output.writeln("\nTime taken: ", elapsed);
88         _output.write(numTestsRun, " test(s) run, ");
89         const failuresStr = text(_failures.length, " failed");
90         if (_failures.length) {
91             _output.writeRed(failuresStr);
92         } else {
93             _output.write(failuresStr);
94         }
95 
96         ulong numTestsWithAttr(string attr)() {
97            return _testData.filter!(a => mixin("a. " ~ attr)).count;
98         }
99 
100         void printHidden() {
101             const num = numTestsWithAttr!"hidden";
102             if(!num) return;
103             _output.write(", ");
104             _output.writeYellow(num, " ", "hidden");
105         }
106 
107         void printShouldFail() {
108             const total = _testCases.filter!(a => a.shouldFail).count;
109             long num = total;
110 
111             foreach(f; _failures) {
112                 const data = _testData.filter!(a => a.getPath == f).front;
113                 if(data.shouldFail) --num;
114             }
115 
116             if(!total) return;
117             _output.write(", ");
118             _output.writeYellow(num, "/", total, " ", "failing as expected");
119         }
120 
121         printHidden();
122         printShouldFail();
123 
124         _output.writeln(".\n");
125 
126         if(_options.random)
127             _output.writeln("Tests were run in random order. To repeat this run, use --seed ", _options.seed, "\n");
128 
129         if (_failures.length) {
130             _output.writelnRed("Tests failed!\n");
131             return false; //oops
132         }
133 
134         _output.writelnGreen("OK!\n");
135 
136         return true;
137     }
138 
139 private:
140 
141     const(Options) _options;
142     const(TestData)[] _testData;
143     TestCase[] _testCases;
144     string[] _failures;
145     StopWatch _stopWatch;
146     Output _output;
147 
148     /**
149      * Runs the tests.
150      * Returns: how long it took to run.
151      */
152     Duration doRun() {
153 
154         import std.algorithm: reduce;
155         import std.parallelism: TaskPool;
156 
157         auto tests = getTests();
158 
159         if(_options.showChrono)
160             foreach(test; tests)
161                 test.showChrono;
162 
163         if(_options.quiet)
164             foreach(test; tests)
165                 test.quiet;
166 
167         _stopWatch.start();
168 
169         if (_options.multiThreaded) {
170             // use a dedicated task pool with non-daemon worker threads
171             auto taskPool = new TaskPool;
172             _failures = reduce!((a, b) => a ~ b)(_failures, taskPool.amap!runTest(tests));
173             taskPool.finish(/*blocking=*/false);
174         } else {
175             foreach (test; tests) {
176                 _failures ~= test();
177             }
178         }
179 
180         version(Windows) {
181             // spawned child processes etc. may have tampered with the console,
182             // try to re-enable the ANSI escape codes for colors
183             import unit_threaded.runner.io: tryEnableEscapeCodes;
184             tryEnableEscapeCodes();
185         }
186 
187         handleFailures();
188 
189         _stopWatch.stop();
190         return cast(Duration) _stopWatch.peek();
191     }
192 
193     auto getTests() {
194         import unit_threaded.runner.io: writeln;
195 
196         auto tests = _testCases.dup;
197 
198         if (_options.random) {
199             import std.random;
200 
201             auto generator = Random(_options.seed);
202             tests.randomShuffle(generator);
203             _output.writeln("Running tests in random order. ",
204                 "To repeat this run, use --seed ", _options.seed);
205         }
206 
207         return tests;
208     }
209 
210     void handleFailures() {
211         import unit_threaded.runner.io: writeln, writeRed, write;
212         import std.array: empty;
213         import std.algorithm: canFind;
214 
215         if (!_failures.empty)
216             _output.writeln("");
217         foreach (failure; _failures) {
218             _output.write("Test ", (failure.canFind(" ") ? `'` ~ failure ~ `'` : failure), " ");
219             _output.writeRed("failed");
220             _output.writeln(".");
221         }
222         if (!_failures.empty)
223             _output.writeln("");
224     }
225 
226     @property ulong numTestsRun() @trusted const {
227         import std.algorithm: map, reduce;
228         return _testCases.map!(a => a.numTestsRun).reduce!((a, b) => a + b);
229     }
230 }