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     bool shouldFail() @safe @nogc pure nothrow { return false; }
64 
65 
66 package:
67 
68     static TestCase currentTest;
69     Output _output;
70 
71     final Output getWriter() @safe {
72         import unit_threaded.runner.io: WriterThread;
73         return _output is null ? WriterThread.get : _output;
74     }
75 
76 
77 protected:
78 
79     abstract void test();
80     void setup() { } ///override to run before test()
81     void shutdown() { } ///override to run after test()
82 
83 
84 private:
85 
86     bool _failed;
87     bool _silent;
88     bool _showChrono;
89 
90     final auto doTest() {
91         import std.conv: text;
92         import std.datetime: Duration;
93         static if(__VERSION__ >= 2077)
94             import std.datetime.stopwatch: StopWatch, AutoStart;
95         else
96             import std.datetime: StopWatch, AutoStart;
97 
98         auto sw = StopWatch(AutoStart.yes);
99         print(getPath() ~ ":\n");
100         check(setup());
101         if (!_failed) check(test());
102         if (!_failed) check(shutdown());
103         if(_failed) print("\n");
104         if(_showChrono) print(text("    (", cast(Duration)sw.peek, ")\n\n"));
105         if(_failed) print("\n");
106     }
107 
108     final bool check(E)(lazy E expression) {
109         import unit_threaded.exception: UnitTestException;
110         try {
111             expression();
112         } catch(UnitTestException ex) {
113             fail(ex.toString());
114         } catch(Throwable ex) {
115             fail("\n    " ~ ex.toString() ~ "\n");
116         }
117 
118         return !_failed;
119     }
120 
121     final void fail(in string msg) {
122         _failed = true;
123         print(msg);
124     }
125 
126     final void print(in string msg) {
127         import unit_threaded.runner.io: write;
128         if(!_silent) getWriter.write(msg);
129     }
130 
131     final void flushOutput() {
132         getWriter.flush;
133     }
134 }
135 
136 unittest
137 {
138     enum Stage { setup, test, shutdown, none, }
139 
140     class TestForFailingStage : TestCase
141     {
142         Stage failedStage, currStage;
143 
144         this(Stage failedStage)
145         {
146             this.failedStage = failedStage;
147         }
148 
149         override void setup()
150         {
151             currStage = Stage.setup;
152             if (failedStage == currStage) assert(0);
153         }
154 
155         override void test()
156         {
157             currStage = Stage.test;
158             if (failedStage == currStage) assert(0);
159         }
160 
161         override void shutdown()
162         {
163             currStage = Stage.shutdown;
164             if (failedStage == currStage) assert(0);
165         }
166     }
167 
168     // the last stage of non failing test case is the shutdown stage
169     {
170         auto test = new TestForFailingStage(Stage.none);
171         test.silence;
172         test.doTest;
173 
174         assert(test.failedStage == Stage.none);
175         assert(test.currStage   == Stage.shutdown);
176     }
177 
178     // if a test case fails at setup stage the last stage is setup one
179     {
180         auto test = new TestForFailingStage(Stage.setup);
181         test.silence;
182         test.doTest;
183 
184         assert(test.failedStage == Stage.setup);
185         assert(test.currStage   == Stage.setup);
186     }
187 
188     // if a test case fails at test stage the last stage is test stage
189     {
190         auto test = new TestForFailingStage(Stage.test);
191         test.silence;
192         test.doTest;
193 
194         assert(test.failedStage == Stage.test);
195         assert(test.currStage   == Stage.test);
196     }
197 }
198 
199 /**
200    A test that runs other tests.
201  */
202 class CompositeTestCase: TestCase {
203     void add(TestCase t) { _tests ~= t;}
204 
205     void opOpAssign(string op : "~")(TestCase t) {
206         add(t);
207     }
208 
209     override string[] opCall() {
210         import std.algorithm: map, reduce;
211         return _tests.map!(a => a()).reduce!((a, b) => a ~ b);
212     }
213 
214     override void test() { assert(false, "CompositeTestCase.test should never be called"); }
215 
216     override ulong numTestsRun() const {
217         return _tests.length;
218     }
219 
220     package TestCase[] tests() @safe pure nothrow {
221         return _tests;
222     }
223 
224     override void showChrono() {
225         foreach(test; _tests) test.showChrono;
226     }
227 
228 private:
229 
230     TestCase[] _tests;
231 }
232 
233 /**
234    A test that should fail
235  */
236 class ShouldFailTestCase: TestCase {
237     this(TestCase testCase, in TypeInfo exceptionTypeInfo) {
238         this.testCase = testCase;
239         this.exceptionTypeInfo = exceptionTypeInfo;
240     }
241 
242     override bool shouldFail() @safe @nogc pure nothrow {
243         return true;
244     }
245 
246     override string getPath() const pure nothrow {
247         return this.testCase.getPath;
248     }
249 
250     override void test() {
251         import unit_threaded.exception: UnitTestException;
252         import std.exception: enforce, collectException;
253         import std.conv: text;
254 
255         const ex = collectException!Throwable(testCase.test());
256         enforce!UnitTestException(ex !is null, "Test '" ~ testCase.getPath ~ "' was expected to fail but did not");
257         enforce!UnitTestException(exceptionTypeInfo is null || typeid(ex) == exceptionTypeInfo,
258                                   text("Test '", testCase.getPath, "' was expected to throw ",
259                                        exceptionTypeInfo, " but threw ", typeid(ex)));
260     }
261 
262 private:
263 
264     TestCase testCase;
265     const(TypeInfo) exceptionTypeInfo;
266 }
267 
268 /**
269    A test that is a regular function.
270  */
271 class FunctionTestCase: TestCase {
272 
273     import unit_threaded.runner.reflection: TestData, TestFunction;
274 
275     this(in TestData data) pure nothrow {
276         _name = data.getPath;
277         _func = data.testFunction;
278     }
279 
280     override void test() {
281         _func();
282     }
283 
284     override string getPath() const pure nothrow {
285         return _name;
286     }
287 
288     private string _name;
289     private TestFunction _func;
290 }
291 
292 /**
293    A test that is a `unittest` block.
294  */
295 class BuiltinTestCase: FunctionTestCase {
296 
297     import unit_threaded.runner.reflection: TestData;
298 
299     this(in TestData data) pure nothrow {
300         super(data);
301     }
302 
303     override void test() {
304         import core.exception: AssertError;
305 
306         try
307             super.test();
308         catch(AssertError e) {
309             import unit_threaded.exception: fail;
310              fail(_stacktrace? e.toString() : e.msg, e.file, e.line);
311         }
312     }
313 }
314 
315 
316 /**
317    A test that is expected to fail some of the time.
318  */
319 class FlakyTestCase: TestCase {
320     this(TestCase testCase, int retries) {
321         this.testCase = testCase;
322         this.retries = retries;
323     }
324 
325     override string getPath() const pure nothrow {
326         return this.testCase.getPath;
327     }
328 
329     override void test() {
330 
331         foreach(i; 0 .. retries) {
332             try {
333                 testCase.test;
334                 break;
335             } catch(Throwable t) {
336                 if(i == retries - 1)
337                     throw t;
338             }
339         }
340     }
341 
342 private:
343 
344     TestCase testCase;
345     int retries;
346 }