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