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 check(test()); 102 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 /** 137 A test that runs other tests. 138 */ 139 class CompositeTestCase: TestCase { 140 void add(TestCase t) { _tests ~= t;} 141 142 void opOpAssign(string op : "~")(TestCase t) { 143 add(t); 144 } 145 146 override string[] opCall() { 147 import std.algorithm: map, reduce; 148 return _tests.map!(a => a()).reduce!((a, b) => a ~ b); 149 } 150 151 override void test() { assert(false, "CompositeTestCase.test should never be called"); } 152 153 override ulong numTestsRun() const { 154 return _tests.length; 155 } 156 157 package TestCase[] tests() @safe pure nothrow { 158 return _tests; 159 } 160 161 override void showChrono() { 162 foreach(test; _tests) test.showChrono; 163 } 164 165 private: 166 167 TestCase[] _tests; 168 } 169 170 /** 171 A test that should fail 172 */ 173 class ShouldFailTestCase: TestCase { 174 this(TestCase testCase, in TypeInfo exceptionTypeInfo) { 175 this.testCase = testCase; 176 this.exceptionTypeInfo = exceptionTypeInfo; 177 } 178 179 override bool shouldFail() @safe @nogc pure nothrow { 180 return true; 181 } 182 183 override string getPath() const pure nothrow { 184 return this.testCase.getPath; 185 } 186 187 override void test() { 188 import unit_threaded.exception: UnitTestException; 189 import std.exception: enforce, collectException; 190 import std.conv: text; 191 192 const ex = collectException!Throwable(testCase.test()); 193 enforce!UnitTestException(ex !is null, "Test '" ~ testCase.getPath ~ "' was expected to fail but did not"); 194 enforce!UnitTestException(exceptionTypeInfo is null || typeid(ex) == exceptionTypeInfo, 195 text("Test '", testCase.getPath, "' was expected to throw ", 196 exceptionTypeInfo, " but threw ", typeid(ex))); 197 } 198 199 private: 200 201 TestCase testCase; 202 const(TypeInfo) exceptionTypeInfo; 203 } 204 205 /** 206 A test that is a regular function. 207 */ 208 class FunctionTestCase: TestCase { 209 210 import unit_threaded.runner.reflection: TestData, TestFunction; 211 212 this(in TestData data) pure nothrow { 213 _name = data.getPath; 214 _func = data.testFunction; 215 } 216 217 override void test() { 218 _func(); 219 } 220 221 override string getPath() const pure nothrow { 222 return _name; 223 } 224 225 private string _name; 226 private TestFunction _func; 227 } 228 229 /** 230 A test that is a `unittest` block. 231 */ 232 class BuiltinTestCase: FunctionTestCase { 233 234 import unit_threaded.runner.reflection: TestData; 235 236 this(in TestData data) pure nothrow { 237 super(data); 238 } 239 240 override void test() { 241 import core.exception: AssertError; 242 243 try 244 super.test(); 245 catch(AssertError e) { 246 import unit_threaded.exception: fail; 247 fail(_stacktrace? e.toString() : e.msg, e.file, e.line); 248 } 249 } 250 } 251 252 253 /** 254 A test that is expected to fail some of the time. 255 */ 256 class FlakyTestCase: TestCase { 257 this(TestCase testCase, int retries) { 258 this.testCase = testCase; 259 this.retries = retries; 260 } 261 262 override string getPath() const pure nothrow { 263 return this.testCase.getPath; 264 } 265 266 override void test() { 267 268 foreach(i; 0 .. retries) { 269 try { 270 testCase.test; 271 break; 272 } catch(Throwable t) { 273 if(i == retries - 1) 274 throw t; 275 } 276 } 277 } 278 279 private: 280 281 TestCase testCase; 282 int retries; 283 }