1 module unit_threaded.testcase;
2 
3 import unit_threaded.should;
4 import unit_threaded.io;
5 import unit_threaded.reflection: TestData, TestFunction;
6 
7 import std.exception;
8 import std.string;
9 import std.conv;
10 import std.algorithm;
11 
12 private shared(bool) _stacktrace = false;
13 
14 private void setStackTrace(bool value) @trusted nothrow @nogc {
15     synchronized {
16         _stacktrace = value;
17     }
18 }
19 
20 /// Let AssertError(s) propagate and thus dump a stacktrace.
21 public void enableStackTrace() @safe nothrow @nogc {
22     setStackTrace(true);
23 }
24 
25 /// (Default behavior) Catch AssertError(s) and thus allow all tests to be ran.
26 public void disableStackTrace() @safe nothrow @nogc {
27     setStackTrace(false);
28 }
29 
30 /**
31  * Class from which other test cases derive
32  */
33 class TestCase {
34 
35     import std.datetime;
36 
37     /**
38      * Returns: the name of the test
39      */
40     string getPath() const pure nothrow {
41         return this.classinfo.name;
42     }
43 
44     /**
45      * Executes the test.
46      * Returns: array of failures (child classes may have more than 1)
47      */
48     string[] opCall() {
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() {
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: to;
89 
90         auto sw = StopWatch(AutoStart.yes);
91         print(getPath() ~ ":\n");
92         check(setup());
93         check(test());
94         check(shutdown());
95         if(_failed) print("\n");
96         if(_showChrono) print(text("    (", cast(Duration)sw.peek, ")\n\n"));
97         if(_failed) print("\n");
98     }
99 
100     final bool check(E)(lazy E expression) {
101         try {
102             expression();
103         } catch(UnitTestException ex) {
104             fail(ex.toString());
105         } catch(Throwable ex) {
106             fail("\n    " ~ ex.toString() ~ "\n");
107         }
108 
109         return !_failed;
110     }
111 
112     final void fail(in string msg) {
113         _failed = true;
114         print(msg);
115     }
116 
117     final void print(in string msg) {
118         if(!_silent) getWriter.write(msg);
119     }
120 
121     final void flushOutput() {
122         getWriter.flush;
123     }
124 }
125 
126 class CompositeTestCase: TestCase {
127     void add(TestCase t) { _tests ~= t;}
128 
129     void opOpAssign(string op : "~")(TestCase t) {
130         add(t);
131     }
132 
133     override string[] opCall() {
134         return _tests.map!(a => a()).reduce!((a, b) => a ~ b);
135     }
136 
137     override void test() { assert(false, "CompositeTestCase.test should never be called"); }
138 
139     override ulong numTestsRun() const {
140         return _tests.length;
141     }
142 
143     package TestCase[] tests() @safe pure nothrow {
144         return _tests;
145     }
146 
147     override void showChrono() {
148         foreach(test; _tests) test.showChrono;
149     }
150 
151 private:
152 
153     TestCase[] _tests;
154 }
155 
156 class ShouldFailTestCase: TestCase {
157     this(TestCase testCase, in TypeInfo exceptionTypeInfo) {
158         this.testCase = testCase;
159         this.exceptionTypeInfo = exceptionTypeInfo;
160     }
161 
162     override string getPath() const pure nothrow {
163         return this.testCase.getPath;
164     }
165 
166     override void test() {
167         import std.exception: enforce;
168         import std.conv: text;
169 
170         const ex = collectException!Throwable(testCase.test());
171         enforce!UnitTestException(ex !is null, "Test '" ~ testCase.getPath ~ "' was expected to fail but did not");
172         enforce!UnitTestException(exceptionTypeInfo is null || typeid(ex) == exceptionTypeInfo,
173                                   text("Test '", testCase.getPath, "' was expected to throw ",
174                                        exceptionTypeInfo, " but threw ", typeid(ex)));
175     }
176 
177 private:
178 
179     TestCase testCase;
180     const(TypeInfo) exceptionTypeInfo;
181 }
182 
183 class FunctionTestCase: TestCase {
184     this(in TestData data) pure nothrow {
185         _name = data.getPath;
186         _func = data.testFunction;
187     }
188 
189     override void test() {
190         _func();
191     }
192 
193     override string getPath() const pure nothrow {
194         return _name;
195     }
196 
197     private string _name;
198     private TestFunction _func;
199 }
200 
201 class BuiltinTestCase: FunctionTestCase {
202     this(in TestData data) pure nothrow {
203         super(data);
204     }
205 
206     override void test() {
207         import core.exception: AssertError;
208 
209         try
210             super.test();
211         catch(AssertError e) {
212              unit_threaded.should.fail(_stacktrace? e.toString() : e.msg, e.file, e.line);
213         }
214     }
215 }