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 }