1 /** 2 The different TestCase classes 3 */ 4 module unit_threaded.runner.testcase; 5 6 7 private shared(bool) _stacktrace = false; 8 9 private void setStackTrace(bool value) @trusted nothrow @nogc { 10 synchronized { 11 _stacktrace = value; 12 } 13 } 14 15 /// Let AssertError(s) propagate and thus dump a stacktrace. 16 public void enableStackTrace() @safe nothrow @nogc { 17 setStackTrace(true); 18 } 19 20 /// (Default behavior) Catch AssertError(s) and thus allow all tests to be ran. 21 public void disableStackTrace() @safe nothrow @nogc { 22 setStackTrace(false); 23 } 24 25 /** 26 * Class from which other test cases derive 27 */ 28 class TestCase { 29 30 import unit_threaded.runner.io: Output; 31 32 /** 33 * Returns: the name of the test 34 */ 35 string getPath() const pure nothrow { 36 return this.classinfo.name; 37 } 38 39 /** 40 * Executes the test. 41 * Returns: array of failures (child classes may have more than 1) 42 */ 43 string[] opCall() { 44 static if(__VERSION__ >= 2077) 45 import std.datetime.stopwatch: StopWatch, AutoStart; 46 else 47 import std.datetime: StopWatch, AutoStart; 48 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 void silence() @safe pure nothrow { _silent = true; } 63 bool shouldFail() @safe @nogc pure nothrow { return false; } 64 65 66 package: 67 68 static TestCase currentTest; 69 Output _output; 70 71 final Output getWriter() @safe { 72 import unit_threaded.runner.io: WriterThread; 73 return _output is null ? WriterThread.get : _output; 74 } 75 76 77 protected: 78 79 abstract void test(); 80 void setup() { } ///override to run before test() 81 void shutdown() { } ///override to run after test() 82 83 84 private: 85 86 bool _failed; 87 bool _silent; 88 bool _showChrono; 89 90 final auto doTest() { 91 import std.conv: text; 92 import std.datetime: Duration; 93 static if(__VERSION__ >= 2077) 94 import std.datetime.stopwatch: StopWatch, AutoStart; 95 else 96 import std.datetime: StopWatch, AutoStart; 97 98 auto sw = StopWatch(AutoStart.yes); 99 print(getPath() ~ ":\n"); 100 check(setup()); 101 if (!_failed) check(test()); 102 if (!_failed) check(shutdown()); 103 if(_failed) print("\n"); 104 if(_showChrono) print(text(" (", cast(Duration)sw.peek, ")\n\n")); 105 if(_failed) print("\n"); 106 } 107 108 final bool check(E)(lazy E expression) { 109 import unit_threaded.exception: UnitTestException; 110 try { 111 expression(); 112 } catch(UnitTestException ex) { 113 fail(ex.toString()); 114 } catch(Throwable ex) { 115 fail("\n " ~ ex.toString() ~ "\n"); 116 } 117 118 return !_failed; 119 } 120 121 final void fail(in string msg) { 122 _failed = true; 123 print(msg); 124 } 125 126 final void print(in string msg) { 127 import unit_threaded.runner.io: write; 128 if(!_silent) getWriter.write(msg); 129 } 130 131 final void flushOutput() { 132 getWriter.flush; 133 } 134 } 135 136 unittest 137 { 138 enum Stage { setup, test, shutdown, none, } 139 140 class TestForFailingStage : TestCase 141 { 142 Stage failedStage, currStage; 143 144 this(Stage failedStage) 145 { 146 this.failedStage = failedStage; 147 } 148 149 override void setup() 150 { 151 currStage = Stage.setup; 152 if (failedStage == currStage) assert(0); 153 } 154 155 override void test() 156 { 157 currStage = Stage.test; 158 if (failedStage == currStage) assert(0); 159 } 160 161 override void shutdown() 162 { 163 currStage = Stage.shutdown; 164 if (failedStage == currStage) assert(0); 165 } 166 } 167 168 // the last stage of non failing test case is the shutdown stage 169 { 170 auto test = new TestForFailingStage(Stage.none); 171 test.silence; 172 test.doTest; 173 174 assert(test.failedStage == Stage.none); 175 assert(test.currStage == Stage.shutdown); 176 } 177 178 // if a test case fails at setup stage the last stage is setup one 179 { 180 auto test = new TestForFailingStage(Stage.setup); 181 test.silence; 182 test.doTest; 183 184 assert(test.failedStage == Stage.setup); 185 assert(test.currStage == Stage.setup); 186 } 187 188 // if a test case fails at test stage the last stage is test stage 189 { 190 auto test = new TestForFailingStage(Stage.test); 191 test.silence; 192 test.doTest; 193 194 assert(test.failedStage == Stage.test); 195 assert(test.currStage == Stage.test); 196 } 197 } 198 199 /** 200 A test that runs other tests. 201 */ 202 class CompositeTestCase: TestCase { 203 void add(TestCase t) { _tests ~= t;} 204 205 void opOpAssign(string op : "~")(TestCase t) { 206 add(t); 207 } 208 209 override string[] opCall() { 210 import std.algorithm: map, reduce; 211 return _tests.map!(a => a()).reduce!((a, b) => a ~ b); 212 } 213 214 override void test() { assert(false, "CompositeTestCase.test should never be called"); } 215 216 override ulong numTestsRun() const { 217 return _tests.length; 218 } 219 220 package TestCase[] tests() @safe pure nothrow { 221 return _tests; 222 } 223 224 override void showChrono() { 225 foreach(test; _tests) test.showChrono; 226 } 227 228 private: 229 230 TestCase[] _tests; 231 } 232 233 /** 234 A test that should fail 235 */ 236 class ShouldFailTestCase: TestCase { 237 this(TestCase testCase, in TypeInfo exceptionTypeInfo) { 238 this.testCase = testCase; 239 this.exceptionTypeInfo = exceptionTypeInfo; 240 } 241 242 override bool shouldFail() @safe @nogc pure nothrow { 243 return true; 244 } 245 246 override string getPath() const pure nothrow { 247 return this.testCase.getPath; 248 } 249 250 override void test() { 251 import unit_threaded.exception: UnitTestException; 252 import std.exception: enforce, collectException; 253 import std.conv: text; 254 255 const ex = collectException!Throwable(testCase.test()); 256 enforce!UnitTestException(ex !is null, "Test '" ~ testCase.getPath ~ "' was expected to fail but did not"); 257 enforce!UnitTestException(exceptionTypeInfo is null || typeid(ex) == exceptionTypeInfo, 258 text("Test '", testCase.getPath, "' was expected to throw ", 259 exceptionTypeInfo, " but threw ", typeid(ex))); 260 } 261 262 private: 263 264 TestCase testCase; 265 const(TypeInfo) exceptionTypeInfo; 266 } 267 268 /** 269 A test that is a regular function. 270 */ 271 class FunctionTestCase: TestCase { 272 273 import unit_threaded.runner.reflection: TestData, TestFunction; 274 275 this(in TestData data) pure nothrow { 276 _name = data.getPath; 277 _func = data.testFunction; 278 } 279 280 override void test() { 281 _func(); 282 } 283 284 override string getPath() const pure nothrow { 285 return _name; 286 } 287 288 private string _name; 289 private TestFunction _func; 290 } 291 292 /** 293 A test that is a `unittest` block. 294 */ 295 class BuiltinTestCase: FunctionTestCase { 296 297 import unit_threaded.runner.reflection: TestData; 298 299 this(in TestData data) pure nothrow { 300 super(data); 301 } 302 303 override void test() { 304 import core.exception: AssertError; 305 306 try 307 super.test(); 308 catch(AssertError e) { 309 import unit_threaded.exception: fail; 310 fail(_stacktrace? e.toString() : e.msg, e.file, e.line); 311 } 312 } 313 } 314 315 316 /** 317 A test that is expected to fail some of the time. 318 */ 319 class FlakyTestCase: TestCase { 320 this(TestCase testCase, int retries) { 321 this.testCase = testCase; 322 this.retries = retries; 323 } 324 325 override string getPath() const pure nothrow { 326 return this.testCase.getPath; 327 } 328 329 override void test() { 330 331 foreach(i; 0 .. retries) { 332 try { 333 testCase.test; 334 break; 335 } catch(Throwable t) { 336 if(i == retries - 1) 337 throw t; 338 } 339 } 340 } 341 342 private: 343 344 TestCase testCase; 345 int retries; 346 }