1 /** 2 The different TestCase classes 3 */ 4 module unit_threaded.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.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 63 package: 64 65 static TestCase currentTest; 66 Output _output; 67 68 void silence() @safe pure nothrow { _silent = true; } 69 70 final Output getWriter() @safe { 71 import unit_threaded.io: WriterThread; 72 return _output is null ? WriterThread.get : _output; 73 } 74 75 protected: 76 77 abstract void test(); 78 void setup() { } ///override to run before test() 79 void shutdown() { } ///override to run after test() 80 81 private: 82 83 bool _failed; 84 bool _silent; 85 bool _showChrono; 86 87 final auto doTest() { 88 import std.conv: text; 89 import std.datetime: Duration; 90 static if(__VERSION__ >= 2077) 91 import std.datetime.stopwatch: StopWatch, AutoStart; 92 else 93 import std.datetime: StopWatch, AutoStart; 94 95 auto sw = StopWatch(AutoStart.yes); 96 print(getPath() ~ ":\n"); 97 check(setup()); 98 check(test()); 99 check(shutdown()); 100 if(_failed) print("\n"); 101 if(_showChrono) print(text(" (", cast(Duration)sw.peek, ")\n\n")); 102 if(_failed) print("\n"); 103 } 104 105 final bool check(E)(lazy E expression) { 106 import unit_threaded.should: UnitTestException; 107 try { 108 expression(); 109 } catch(UnitTestException ex) { 110 fail(ex.toString()); 111 } catch(Throwable ex) { 112 fail("\n " ~ ex.toString() ~ "\n"); 113 } 114 115 return !_failed; 116 } 117 118 final void fail(in string msg) { 119 _failed = true; 120 print(msg); 121 } 122 123 final void print(in string msg) { 124 import unit_threaded.io: write; 125 if(!_silent) getWriter.write(msg); 126 } 127 128 final void flushOutput() { 129 getWriter.flush; 130 } 131 } 132 133 /** 134 A test that runs other tests. 135 */ 136 class CompositeTestCase: TestCase { 137 void add(TestCase t) { _tests ~= t;} 138 139 void opOpAssign(string op : "~")(TestCase t) { 140 add(t); 141 } 142 143 override string[] opCall() { 144 import std.algorithm: map, reduce; 145 return _tests.map!(a => a()).reduce!((a, b) => a ~ b); 146 } 147 148 override void test() { assert(false, "CompositeTestCase.test should never be called"); } 149 150 override ulong numTestsRun() const { 151 return _tests.length; 152 } 153 154 package TestCase[] tests() @safe pure nothrow { 155 return _tests; 156 } 157 158 override void showChrono() { 159 foreach(test; _tests) test.showChrono; 160 } 161 162 private: 163 164 TestCase[] _tests; 165 } 166 167 /** 168 A test that should fail 169 */ 170 class ShouldFailTestCase: TestCase { 171 this(TestCase testCase, in TypeInfo exceptionTypeInfo) { 172 this.testCase = testCase; 173 this.exceptionTypeInfo = exceptionTypeInfo; 174 } 175 176 override string getPath() const pure nothrow { 177 return this.testCase.getPath; 178 } 179 180 override void test() { 181 import unit_threaded.should: UnitTestException; 182 import std.exception: enforce, collectException; 183 import std.conv: text; 184 185 const ex = collectException!Throwable(testCase.test()); 186 enforce!UnitTestException(ex !is null, "Test '" ~ testCase.getPath ~ "' was expected to fail but did not"); 187 enforce!UnitTestException(exceptionTypeInfo is null || typeid(ex) == exceptionTypeInfo, 188 text("Test '", testCase.getPath, "' was expected to throw ", 189 exceptionTypeInfo, " but threw ", typeid(ex))); 190 } 191 192 private: 193 194 TestCase testCase; 195 const(TypeInfo) exceptionTypeInfo; 196 } 197 198 /** 199 A test that is a regular function. 200 */ 201 class FunctionTestCase: TestCase { 202 203 import unit_threaded.reflection: TestData, TestFunction; 204 205 this(in TestData data) pure nothrow { 206 _name = data.getPath; 207 _func = data.testFunction; 208 } 209 210 override void test() { 211 _func(); 212 } 213 214 override string getPath() const pure nothrow { 215 return _name; 216 } 217 218 private string _name; 219 private TestFunction _func; 220 } 221 222 /** 223 A test that is a `unittest` block. 224 */ 225 class BuiltinTestCase: FunctionTestCase { 226 227 import unit_threaded.reflection: TestData; 228 229 this(in TestData data) pure nothrow { 230 super(data); 231 } 232 233 override void test() { 234 import core.exception: AssertError; 235 236 try 237 super.test(); 238 catch(AssertError e) { 239 import unit_threaded.should: fail; 240 fail(_stacktrace? e.toString() : e.msg, e.file, e.line); 241 } 242 } 243 } 244 245 246 /** 247 A test that is expected to fail some of the time. 248 */ 249 class FlakyTestCase: TestCase { 250 this(TestCase testCase, int retries) { 251 this.testCase = testCase; 252 this.retries = retries; 253 } 254 255 override string getPath() const pure nothrow { 256 return this.testCase.getPath; 257 } 258 259 override void test() { 260 261 foreach(i; 0 .. retries) { 262 try { 263 testCase.test; 264 break; 265 } catch(Throwable t) { 266 if(i == retries - 1) 267 throw t; 268 } 269 } 270 } 271 272 private: 273 274 TestCase testCase; 275 int retries; 276 }