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