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 }