1 module unit_threaded.testcase;
2 
3 
4 private shared(bool) _stacktrace = false;
5 
6 private void setStackTrace(bool value) @trusted nothrow @nogc {
7     synchronized {
8         _stacktrace = value;
9     }
10 }
11 
12 /// Let AssertError(s) propagate and thus dump a stacktrace.
13 public void enableStackTrace() @safe nothrow @nogc {
14     setStackTrace(true);
15 }
16 
17 /// (Default behavior) Catch AssertError(s) and thus allow all tests to be ran.
18 public void disableStackTrace() @safe nothrow @nogc {
19     setStackTrace(false);
20 }
21 
22 /**
23  * Class from which other test cases derive
24  */
25 class TestCase {
26 
27     import unit_threaded.io: Output;
28 
29     /**
30      * Returns: the name of the test
31      */
32     string getPath() const pure nothrow {
33         return this.classinfo.name;
34     }
35 
36     /**
37      * Executes the test.
38      * Returns: array of failures (child classes may have more than 1)
39      */
40     string[] opCall() {
41         static if(__VERSION__ >= 2077)
42             import std.datetime.stopwatch: StopWatch, AutoStart;
43         else
44             import std.datetime: StopWatch, AutoStart;
45 
46         currentTest = this;
47         auto sw = StopWatch(AutoStart.yes);
48         doTest();
49         flushOutput();
50         return _failed ? [getPath()] : [];
51     }
52 
53     /**
54      Certain child classes override this
55      */
56     ulong numTestsRun() const { return 1; }
57     void showChrono() @safe pure nothrow { _showChrono = true; }
58     void setOutput(Output output) @safe pure nothrow { _output = output; }
59 
60 package:
61 
62     static TestCase currentTest;
63     Output _output;
64 
65     void silence() @safe pure nothrow { _silent = true; }
66 
67     final Output getWriter() @safe {
68         import unit_threaded.io: WriterThread;
69         return _output is null ? WriterThread.get : _output;
70     }
71 
72 protected:
73 
74     abstract void test();
75     void setup() { } ///override to run before test()
76     void shutdown() { } ///override to run after test()
77 
78 private:
79 
80     bool _failed;
81     bool _silent;
82     bool _showChrono;
83 
84     final auto doTest() {
85         import std.conv: text;
86         import std.datetime: Duration;
87         static if(__VERSION__ >= 2077)
88             import std.datetime.stopwatch: StopWatch, AutoStart;
89         else
90             import std.datetime: StopWatch, AutoStart;
91 
92         auto sw = StopWatch(AutoStart.yes);
93         print(getPath() ~ ":\n");
94         check(setup());
95         check(test());
96         check(shutdown());
97         if(_failed) print("\n");
98         if(_showChrono) print(text("    (", cast(Duration)sw.peek, ")\n\n"));
99         if(_failed) print("\n");
100     }
101 
102     final bool check(E)(lazy E expression) {
103         import unit_threaded.should: UnitTestException;
104         try {
105             expression();
106         } catch(UnitTestException ex) {
107             fail(ex.toString());
108         } catch(Throwable ex) {
109             fail("\n    " ~ ex.toString() ~ "\n");
110         }
111 
112         return !_failed;
113     }
114 
115     final void fail(in string msg) {
116         _failed = true;
117         print(msg);
118     }
119 
120     final void print(in string msg) {
121         import unit_threaded.io: write;
122         if(!_silent) getWriter.write(msg);
123     }
124 
125     final void flushOutput() {
126         getWriter.flush;
127     }
128 }
129 
130 class CompositeTestCase: TestCase {
131     void add(TestCase t) { _tests ~= t;}
132 
133     void opOpAssign(string op : "~")(TestCase t) {
134         add(t);
135     }
136 
137     override string[] opCall() {
138         import std.algorithm: map, reduce;
139         return _tests.map!(a => a()).reduce!((a, b) => a ~ b);
140     }
141 
142     override void test() { assert(false, "CompositeTestCase.test should never be called"); }
143 
144     override ulong numTestsRun() const {
145         return _tests.length;
146     }
147 
148     package TestCase[] tests() @safe pure nothrow {
149         return _tests;
150     }
151 
152     override void showChrono() {
153         foreach(test; _tests) test.showChrono;
154     }
155 
156 private:
157 
158     TestCase[] _tests;
159 }
160 
161 class ShouldFailTestCase: TestCase {
162     this(TestCase testCase, in TypeInfo exceptionTypeInfo) {
163         this.testCase = testCase;
164         this.exceptionTypeInfo = exceptionTypeInfo;
165     }
166 
167     override string getPath() const pure nothrow {
168         return this.testCase.getPath;
169     }
170 
171     override void test() {
172         import unit_threaded.should: UnitTestException;
173         import std.exception: enforce, collectException;
174         import std.conv: text;
175 
176         const ex = collectException!Throwable(testCase.test());
177         enforce!UnitTestException(ex !is null, "Test '" ~ testCase.getPath ~ "' was expected to fail but did not");
178         enforce!UnitTestException(exceptionTypeInfo is null || typeid(ex) == exceptionTypeInfo,
179                                   text("Test '", testCase.getPath, "' was expected to throw ",
180                                        exceptionTypeInfo, " but threw ", typeid(ex)));
181     }
182 
183 private:
184 
185     TestCase testCase;
186     const(TypeInfo) exceptionTypeInfo;
187 }
188 
189 class FunctionTestCase: TestCase {
190 
191     import unit_threaded.reflection: TestData, TestFunction;
192 
193     this(in TestData data) pure nothrow {
194         _name = data.getPath;
195         _func = data.testFunction;
196     }
197 
198     override void test() {
199         _func();
200     }
201 
202     override string getPath() const pure nothrow {
203         return _name;
204     }
205 
206     private string _name;
207     private TestFunction _func;
208 }
209 
210 class BuiltinTestCase: FunctionTestCase {
211 
212     import unit_threaded.reflection: TestData;
213 
214     this(in TestData data) pure nothrow {
215         super(data);
216     }
217 
218     override void test() {
219         import core.exception: AssertError;
220 
221         try
222             super.test();
223         catch(AssertError e) {
224             import unit_threaded.should: fail;
225              fail(_stacktrace? e.toString() : e.msg, e.file, e.line);
226         }
227     }
228 }
229 
230 
231 class FlakyTestCase: TestCase {
232     this(TestCase testCase, int retries) {
233         this.testCase = testCase;
234         this.retries = retries;
235     }
236 
237     override string getPath() const pure nothrow {
238         return this.testCase.getPath;
239     }
240 
241     override void test() {
242 
243         foreach(i; 0 .. retries) {
244             try {
245                 testCase.test;
246                 break;
247             } catch(Throwable t) {
248                 if(i == retries - 1)
249                     throw t;
250             }
251         }
252     }
253 
254 private:
255 
256     TestCase testCase;
257     int retries;
258 }