1 module unit_threaded.testcase; 2 3 import unit_threaded.should; 4 import unit_threaded.io; 5 import unit_threaded.reflection: TestData, TestFunction; 6 7 import std.exception; 8 import std.string; 9 import std.conv; 10 import std.algorithm; 11 12 private shared(bool) _stacktrace = false; 13 14 private void setStackTrace(bool value) @trusted nothrow @nogc { 15 synchronized { 16 _stacktrace = value; 17 } 18 } 19 20 /// Let AssertError(s) propagate and thus dump a stacktrace. 21 public void enableStackTrace() @safe nothrow @nogc { 22 setStackTrace(true); 23 } 24 25 /// (Default behavior) Catch AssertError(s) and thus allow all tests to be ran. 26 public void disableStackTrace() @safe nothrow @nogc { 27 setStackTrace(false); 28 } 29 30 /** 31 * Class from which other test cases derive 32 */ 33 class TestCase { 34 35 import std.datetime; 36 37 /** 38 * Returns: the name of the test 39 */ 40 string getPath() const pure nothrow { 41 return this.classinfo.name; 42 } 43 44 /** 45 * Executes the test. 46 * Returns: array of failures (child classes may have more than 1) 47 */ 48 string[] opCall() { 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() { 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: to; 89 90 auto sw = StopWatch(AutoStart.yes); 91 print(getPath() ~ ":\n"); 92 check(setup()); 93 check(test()); 94 check(shutdown()); 95 if(_failed) print("\n"); 96 if(_showChrono) print(text(" (", cast(Duration)sw.peek, ")\n\n")); 97 if(_failed) print("\n"); 98 } 99 100 final bool check(E)(lazy E expression) { 101 try { 102 expression(); 103 } catch(UnitTestException ex) { 104 fail(ex.toString()); 105 } catch(Throwable ex) { 106 fail("\n " ~ ex.toString() ~ "\n"); 107 } 108 109 return !_failed; 110 } 111 112 final void fail(in string msg) { 113 _failed = true; 114 print(msg); 115 } 116 117 final void print(in string msg) { 118 if(!_silent) getWriter.write(msg); 119 } 120 121 final void flushOutput() { 122 getWriter.flush; 123 } 124 } 125 126 class CompositeTestCase: TestCase { 127 void add(TestCase t) { _tests ~= t;} 128 129 void opOpAssign(string op : "~")(TestCase t) { 130 add(t); 131 } 132 133 override string[] opCall() { 134 return _tests.map!(a => a()).reduce!((a, b) => a ~ b); 135 } 136 137 override void test() { assert(false, "CompositeTestCase.test should never be called"); } 138 139 override ulong numTestsRun() const { 140 return _tests.length; 141 } 142 143 package TestCase[] tests() @safe pure nothrow { 144 return _tests; 145 } 146 147 override void showChrono() { 148 foreach(test; _tests) test.showChrono; 149 } 150 151 private: 152 153 TestCase[] _tests; 154 } 155 156 class ShouldFailTestCase: TestCase { 157 this(TestCase testCase, in TypeInfo exceptionTypeInfo) { 158 this.testCase = testCase; 159 this.exceptionTypeInfo = exceptionTypeInfo; 160 } 161 162 override string getPath() const pure nothrow { 163 return this.testCase.getPath; 164 } 165 166 override void test() { 167 import std.exception: enforce; 168 import std.conv: text; 169 170 const ex = collectException!Throwable(testCase.test()); 171 enforce!UnitTestException(ex !is null, "Test '" ~ testCase.getPath ~ "' was expected to fail but did not"); 172 enforce!UnitTestException(exceptionTypeInfo is null || typeid(ex) == exceptionTypeInfo, 173 text("Test '", testCase.getPath, "' was expected to throw ", 174 exceptionTypeInfo, " but threw ", typeid(ex))); 175 } 176 177 private: 178 179 TestCase testCase; 180 const(TypeInfo) exceptionTypeInfo; 181 } 182 183 class FunctionTestCase: TestCase { 184 this(in TestData data) pure nothrow { 185 _name = data.getPath; 186 _func = data.testFunction; 187 } 188 189 override void test() { 190 _func(); 191 } 192 193 override string getPath() const pure nothrow { 194 return _name; 195 } 196 197 private string _name; 198 private TestFunction _func; 199 } 200 201 class BuiltinTestCase: FunctionTestCase { 202 this(in TestData data) pure nothrow { 203 super(data); 204 } 205 206 override void test() { 207 import core.exception: AssertError; 208 209 try 210 super.test(); 211 catch(AssertError e) { 212 unit_threaded.should.fail(_stacktrace? e.toString() : e.msg, e.file, e.line); 213 } 214 } 215 }