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