1 /**
2    The different TestCase classes
3  */
4 module unit_threaded.runner.testcase;
5 
6 
7 private shared(bool) _stacktrace = false;
8 
9 private void setStackTrace(bool value) @trusted nothrow @nogc {
10     synchronized {
11         _stacktrace = value;
12     }
13 }
14 
15 /// Let AssertError(s) propagate and thus dump a stacktrace.
16 public void enableStackTrace() @safe nothrow @nogc {
17     setStackTrace(true);
18 }
19 
20 /// (Default behavior) Catch AssertError(s) and thus allow all tests to be ran.
21 public void disableStackTrace() @safe nothrow @nogc {
22     setStackTrace(false);
23 }
24 
25 /**
26  * Class from which other test cases derive
27  */
28 class TestCase {
29 
30     import unit_threaded.runner.io: Output;
31 
32     /**
33      * Returns: the name of the test
34      */
35     string getPath() const pure nothrow {
36         return this.classinfo.name;
37     }
38 
39     /**
40      * Executes the test.
41      * Returns: array of failures (child classes may have more than 1)
42      */
43     string[] opCall() {
44         static if(__VERSION__ >= 2077)
45             import std.datetime.stopwatch: StopWatch, AutoStart;
46         else
47             import std.datetime: StopWatch, AutoStart;
48 
49         currentTest = this;
50         auto sw = StopWatch(AutoStart.yes);
51         doTest();
52         flushOutput();
53         return _failed ? [getPath()] : [];
54     }
55 
56     /**
57      Certain child classes override this
58      */
59     ulong numTestsRun() const { return 1; }
60     void showChrono() @safe pure nothrow { _showChrono = true; }
61     void setOutput(Output output) @safe pure nothrow { _output = output; }
62     void silence() @safe pure nothrow { _silent = true; }
63     void quiet() @safe pure nothrow { _quiet = true; }
64     bool shouldFail() @safe @nogc pure nothrow { return false; }
65 
66 
67 package:
68 
69     static TestCase currentTest;
70     Output _output;
71 
72     final Output getWriter() @safe {
73         import unit_threaded.runner.io: WriterThread;
74         return _output is null ? WriterThread.get : _output;
75     }
76 
77 
78 protected:
79 
80     abstract void test();
81     void setup() { } ///override to run before test()
82     void shutdown() { } ///override to run after test()
83 
84 
85 private:
86 
87     bool _failed;
88     bool _silent;
89     bool _quiet;
90     bool _showChrono;
91 
92     final auto doTest() {
93         import std.conv: text;
94         import std.datetime: Duration;
95         static if(__VERSION__ >= 2077)
96             import std.datetime.stopwatch: StopWatch, AutoStart;
97         else
98             import std.datetime: StopWatch, AutoStart;
99 
100         auto sw = StopWatch(AutoStart.yes);
101         // Print the name of the test, unless in quiet mode.
102         // However, we want to print everything if it fails.
103         if(!_quiet) printTestName;
104         check(setup());
105         if (!_failed) check(test());
106         if (!_failed) check(shutdown());
107         if(_failed) print("\n");
108         if(_showChrono) print(text("    (", cast(Duration)sw.peek, ")\n\n"));
109         if(_failed) print("\n");
110     }
111 
112     final void printTestName() {
113         print(getPath() ~ ":\n");
114     }
115 
116     final bool check(E)(lazy E expression) {
117         import unit_threaded.exception: UnitTestException;
118         try {
119             expression();
120         } catch(UnitTestException ex) {
121             fail(ex.toString());
122         } catch(Throwable ex) {
123             fail("\n    " ~ ex.toString() ~ "\n");
124         }
125 
126         return !_failed;
127     }
128 
129     final void fail(in string msg) {
130         // if this is the first failure and in quiet mode, print the test
131         // name since we didn't do it at first
132         if(!_failed && _quiet) printTestName;
133         _failed = true;
134         print(msg);
135     }
136 
137     final void print(in string msg) {
138         import unit_threaded.runner.io: write;
139         if(!_silent) getWriter.write(msg);
140     }
141 
142     final void alwaysPrint(in string msg) {
143         import unit_threaded.runner.io: write;
144         getWriter.write(msg);
145     }
146 
147     final void flushOutput() {
148         getWriter.flush;
149     }
150 }
151 
152 unittest
153 {
154     enum Stage { setup, test, shutdown, none, }
155 
156     class TestForFailingStage : TestCase
157     {
158         Stage failedStage, currStage;
159 
160         this(Stage failedStage)
161         {
162             this.failedStage = failedStage;
163         }
164 
165         override void setup()
166         {
167             currStage = Stage.setup;
168             if (failedStage == currStage) assert(0);
169         }
170 
171         override void test()
172         {
173             currStage = Stage.test;
174             if (failedStage == currStage) assert(0);
175         }
176 
177         override void shutdown()
178         {
179             currStage = Stage.shutdown;
180             if (failedStage == currStage) assert(0);
181         }
182     }
183 
184     // the last stage of non failing test case is the shutdown stage
185     {
186         auto test = new TestForFailingStage(Stage.none);
187         test.silence;
188         test.doTest;
189 
190         assert(test.failedStage == Stage.none);
191         assert(test.currStage   == Stage.shutdown);
192     }
193 
194     // if a test case fails at setup stage the last stage is setup one
195     {
196         auto test = new TestForFailingStage(Stage.setup);
197         test.silence;
198         test.doTest;
199 
200         assert(test.failedStage == Stage.setup);
201         assert(test.currStage   == Stage.setup);
202     }
203 
204     // if a test case fails at test stage the last stage is test stage
205     {
206         auto test = new TestForFailingStage(Stage.test);
207         test.silence;
208         test.doTest;
209 
210         assert(test.failedStage == Stage.test);
211         assert(test.currStage   == Stage.test);
212     }
213 }
214 
215 /**
216    A test that runs other tests.
217  */
218 class CompositeTestCase: TestCase {
219     void add(TestCase t) { _tests ~= t;}
220 
221     void opOpAssign(string op : "~")(TestCase t) {
222         add(t);
223     }
224 
225     override string[] opCall() {
226         import std.algorithm: map, reduce;
227         return _tests.map!(a => a()).reduce!((a, b) => a ~ b);
228     }
229 
230     override void test() { assert(false, "CompositeTestCase.test should never be called"); }
231 
232     override ulong numTestsRun() const {
233         return _tests.length;
234     }
235 
236     package TestCase[] tests() @safe pure nothrow {
237         return _tests;
238     }
239 
240     override void showChrono() {
241         foreach(test; _tests) test.showChrono;
242     }
243 
244 private:
245 
246     TestCase[] _tests;
247 }
248 
249 /**
250    A test that should fail
251  */
252 class ShouldFailTestCase: TestCase {
253     this(TestCase testCase, in TypeInfo exceptionTypeInfo) {
254         this.testCase = testCase;
255         this.exceptionTypeInfo = exceptionTypeInfo;
256     }
257 
258     override bool shouldFail() @safe @nogc pure nothrow {
259         return true;
260     }
261 
262     override string getPath() const pure nothrow {
263         return this.testCase.getPath;
264     }
265 
266     override void test() {
267         import unit_threaded.exception: UnitTestException;
268         import std.exception: enforce, collectException;
269         import std.conv: text;
270 
271         const ex = collectException!Throwable(testCase.test());
272         enforce!UnitTestException(ex !is null, "Test '" ~ testCase.getPath ~ "' was expected to fail but did not");
273         enforce!UnitTestException(exceptionTypeInfo is null || typeid(ex) == exceptionTypeInfo,
274                                   text("Test '", testCase.getPath, "' was expected to throw ",
275                                        exceptionTypeInfo, " but threw ", typeid(ex)));
276     }
277 
278 private:
279 
280     TestCase testCase;
281     const(TypeInfo) exceptionTypeInfo;
282 }
283 
284 /**
285    A test that is a regular function.
286  */
287 class FunctionTestCase: TestCase {
288 
289     import unit_threaded.runner.reflection: TestData, TestFunction;
290 
291     this(in TestData data) pure nothrow {
292         _name = data.getPath;
293         _func = data.testFunction;
294     }
295 
296     override void test() {
297         _func();
298     }
299 
300     override string getPath() const pure nothrow {
301         return _name;
302     }
303 
304     private string _name;
305     private TestFunction _func;
306 }
307 
308 /**
309    A test that is a `unittest` block.
310  */
311 class BuiltinTestCase: FunctionTestCase {
312 
313     import unit_threaded.runner.reflection: TestData;
314 
315     this(in TestData data) pure nothrow {
316         super(data);
317     }
318 
319     override void test() {
320         import core.exception: AssertError;
321 
322         try
323             super.test();
324         catch(AssertError e) {
325             import unit_threaded.exception: fail;
326              fail(_stacktrace? e.toString() : e.msg, e.file, e.line);
327         }
328     }
329 }
330 
331 
332 /**
333    A test that is expected to fail some of the time.
334  */
335 class FlakyTestCase: TestCase {
336     this(TestCase testCase, int retries) {
337         this.testCase = testCase;
338         this.retries = retries;
339     }
340 
341     override string getPath() const pure nothrow {
342         return this.testCase.getPath;
343     }
344 
345     override void test() {
346 
347         foreach(i; 0 .. retries) {
348             try {
349                 testCase.test;
350                 break;
351             } catch(Throwable t) {
352                 if(i == retries - 1)
353                     throw t;
354             }
355         }
356     }
357 
358 private:
359 
360     TestCase testCase;
361     int retries;
362 }