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         import std.datetime: StopWatch, AutoStart;
42 
43         currentTest = this;
44         auto sw = StopWatch(AutoStart.yes);
45         doTest();
46         flushOutput();
47         return _failed ? [getPath()] : [];
48     }
49 
50     /**
51      Certain child classes override this
52      */
53     ulong numTestsRun() const { return 1; }
54     void showChrono() @safe pure nothrow { _showChrono = true; }
55     void setOutput(Output output) @safe pure nothrow { _output = output; }
56 
57 package:
58 
59     static TestCase currentTest;
60     Output _output;
61 
62     void silence() @safe pure nothrow { _silent = true; }
63 
64     final Output getWriter() @safe {
65         import unit_threaded.io: WriterThread;
66         return _output is null ? WriterThread.get : _output;
67     }
68 
69 protected:
70 
71     abstract void test();
72     void setup() { } ///override to run before test()
73     void shutdown() { } ///override to run after test()
74 
75 private:
76 
77     bool _failed;
78     bool _silent;
79     bool _showChrono;
80 
81     final auto doTest() {
82         import std.conv: text;
83         import std.datetime: StopWatch, AutoStart, Duration;
84 
85         auto sw = StopWatch(AutoStart.yes);
86         print(getPath() ~ ":\n");
87         check(setup());
88         check(test());
89         check(shutdown());
90         if(_failed) print("\n");
91         if(_showChrono) print(text("    (", cast(Duration)sw.peek, ")\n\n"));
92         if(_failed) print("\n");
93     }
94 
95     final bool check(E)(lazy E expression) {
96         import unit_threaded.should: UnitTestException;
97         try {
98             expression();
99         } catch(UnitTestException ex) {
100             fail(ex.toString());
101         } catch(Throwable ex) {
102             fail("\n    " ~ ex.toString() ~ "\n");
103         }
104 
105         return !_failed;
106     }
107 
108     final void fail(in string msg) {
109         _failed = true;
110         print(msg);
111     }
112 
113     final void print(in string msg) {
114         import unit_threaded.io: write;
115         if(!_silent) getWriter.write(msg);
116     }
117 
118     final void flushOutput() {
119         getWriter.flush;
120     }
121 }
122 
123 class CompositeTestCase: TestCase {
124     void add(TestCase t) { _tests ~= t;}
125 
126     void opOpAssign(string op : "~")(TestCase t) {
127         add(t);
128     }
129 
130     override string[] opCall() {
131         import std.algorithm: map, reduce;
132         return _tests.map!(a => a()).reduce!((a, b) => a ~ b);
133     }
134 
135     override void test() { assert(false, "CompositeTestCase.test should never be called"); }
136 
137     override ulong numTestsRun() const {
138         return _tests.length;
139     }
140 
141     package TestCase[] tests() @safe pure nothrow {
142         return _tests;
143     }
144 
145     override void showChrono() {
146         foreach(test; _tests) test.showChrono;
147     }
148 
149 private:
150 
151     TestCase[] _tests;
152 }
153 
154 class ShouldFailTestCase: TestCase {
155     this(TestCase testCase, in TypeInfo exceptionTypeInfo) {
156         this.testCase = testCase;
157         this.exceptionTypeInfo = exceptionTypeInfo;
158     }
159 
160     override string getPath() const pure nothrow {
161         return this.testCase.getPath;
162     }
163 
164     override void test() {
165         import unit_threaded.should: UnitTestException;
166         import std.exception: enforce, collectException;
167         import std.conv: text;
168 
169         const ex = collectException!Throwable(testCase.test());
170         enforce!UnitTestException(ex !is null, "Test '" ~ testCase.getPath ~ "' was expected to fail but did not");
171         enforce!UnitTestException(exceptionTypeInfo is null || typeid(ex) == exceptionTypeInfo,
172                                   text("Test '", testCase.getPath, "' was expected to throw ",
173                                        exceptionTypeInfo, " but threw ", typeid(ex)));
174     }
175 
176 private:
177 
178     TestCase testCase;
179     const(TypeInfo) exceptionTypeInfo;
180 }
181 
182 class FunctionTestCase: TestCase {
183 
184     import unit_threaded.reflection: TestData, TestFunction;
185 
186     this(in TestData data) pure nothrow {
187         _name = data.getPath;
188         _func = data.testFunction;
189     }
190 
191     override void test() {
192         _func();
193     }
194 
195     override string getPath() const pure nothrow {
196         return _name;
197     }
198 
199     private string _name;
200     private TestFunction _func;
201 }
202 
203 class BuiltinTestCase: FunctionTestCase {
204 
205     import unit_threaded.reflection: TestData;
206 
207     this(in TestData data) pure nothrow {
208         super(data);
209     }
210 
211     override void test() {
212         import core.exception: AssertError;
213 
214         try
215             super.test();
216         catch(AssertError e) {
217             import unit_threaded.should: fail;
218              fail(_stacktrace? e.toString() : e.msg, e.file, e.line);
219         }
220     }
221 }