1 module beep; 2 3 import std.format : format; 4 5 enum equal; 6 enum less; 7 enum greater; 8 enum contain; 9 enum match; 10 enum throw_; 11 12 final class ExpectException : Exception { 13 pure nothrow @safe this(string msg, 14 string file = __FILE__, 15 size_t line = __LINE__, 16 Throwable nextInChain = null) { 17 super("Expectation failed: " ~ msg, file, line, nextInChain); 18 } 19 } 20 21 struct Fence {} 22 23 T1 expect(OP, T1, T2)(lazy T1 lhs, lazy T2 rhs, Fence _ = Fence(), string file = __FILE__, size_t line = __LINE__) 24 if(is(OP == equal) && __traits(compiles, lhs == rhs)) { 25 if(!(lhs == rhs)) 26 throw new ExpectException( 27 "`%s` is expected, got `%s`".format(rhs, lhs), 28 file, 29 line, 30 ); 31 32 return lhs; 33 } 34 35 @("expect!equal") 36 @safe pure unittest { 37 1.expect!equal(1); 38 "Hi!".expect!equal("Hi!"); 39 40 struct S { 41 int* ptr; 42 } 43 44 S(null).expect!equal(S(null)); 45 } 46 47 @("expect!equal checks can be chained") 48 @safe pure unittest { 49 1.expect!equal(1) 50 .expect!equal(1) 51 .expect!equal(1); 52 } 53 54 @("expect!equal fails if value is not equal to expected value") 55 unittest { 56 ({ 57 1.expect!equal(2); 58 }).expect!(throw_, ExpectException) 59 .message.expect!contain("`2` is expected, got `1`"); 60 61 ({ 62 "Hello, Alice!".expect!equal("Hello, Bob!"); 63 }).expect!(throw_, ExpectException) 64 .message.expect!contain("`Hello, Bob!` is expected, got `Hello, Alice!`"); 65 66 ({ 67 struct S { 68 string s; 69 int n; 70 } 71 72 S("Hi!", 42).expect!equal(S("Bye!", 43)); 73 }).expect!(throw_, ExpectException) 74 .message.expect!contain("`S(\"Bye!\", 43)` is expected, got `S(\"Hi!\", 42)`"); 75 } 76 77 T1 expect(bool bool_, T1)(lazy T1 lhs, Fence _ = Fence(), string file = __FILE__, size_t line = __LINE__) { 78 return expect!(equal, T1)(lhs, bool_, _, file, line); 79 } 80 81 @("expect!(true|false) - convenience wrapper around expect!equal") 82 @safe pure unittest { 83 1.expect!true; 84 0.expect!false; 85 } 86 87 @("expect!(true|false) checks can be chained") 88 @safe pure unittest { 89 1.expect!true 90 .expect!true 91 .expect!true; 92 93 0.expect!false 94 .expect!false 95 .expect!false; 96 } 97 98 @("expect!(true|false) fails if value is not true|false") 99 unittest { 100 ({ 101 0.expect!true; 102 }).expect!(throw_, ExpectException) 103 .message.expect!contain("`true` is expected, got `0`"); 104 105 ({ 106 1.expect!false; 107 }).expect!(throw_, ExpectException) 108 .message.expect!contain("`false` is expected, got `1`"); 109 } 110 111 T1 expect(OP, T1, T2)(lazy T1 lhs, lazy T2 rhs, Fence _ = Fence(), string file = __FILE__, size_t line = __LINE__) 112 if(is(OP == less) && __traits(compiles, lhs < rhs)) { 113 if(!(lhs < rhs)) 114 throw new ExpectException( 115 "value less than `%s` is expected, got `%s`".format(rhs, lhs), 116 file, 117 line, 118 ); 119 120 return lhs; 121 } 122 123 @("expect!less") 124 @safe unittest { 125 1.expect!less(2); 126 1.0.expect!less(1.00001); 127 } 128 129 @("expect!less checks can be chained") 130 @safe pure unittest { 131 1.expect!less(2) 132 .expect!less(3) 133 .expect!less(4) 134 .expect!less(5) 135 .expect!less(6); 136 } 137 138 @("expect!less fails if one value is not less than expected") 139 unittest { 140 ({ 141 1.expect!less(2) 142 .expect!less(3) 143 .expect!less(4) 144 .expect!less(0); 145 }).expect!(throw_, ExpectException) 146 .message.expect!contain("value less than `0` is expected, got `1`"); 147 } 148 149 T1 expect(OP, T1, T2)(lazy T1 lhs, lazy T2 rhs, Fence _ = Fence(), string file = __FILE__, size_t line = __LINE__) 150 if(is(OP == greater) && __traits(compiles, lhs > rhs)) { 151 if(!(lhs > rhs)) 152 throw new ExpectException( 153 "value greater than `%s` is expected, got `%s`".format(rhs, lhs), 154 file, 155 line, 156 ); 157 158 return lhs; 159 } 160 161 @("expect!greater") 162 @safe unittest { 163 1.expect!greater(0); 164 1.001.expect!greater(1); 165 } 166 167 @("expect!greater checks can be chained") 168 @safe pure unittest { 169 1.expect!greater(0) 170 .expect!greater(-1) 171 .expect!greater(-2) 172 .expect!greater(-3) 173 .expect!greater(-4) 174 .expect!greater(-5); 175 } 176 177 @("expect!greater fails if one value is not greater than expected") 178 unittest { 179 ({ 180 1.expect!greater(0) 181 .expect!greater(-1) 182 .expect!greater(2); 183 }).expect!(throw_, ExpectException) 184 .message.expect!contain("value greater than `2` is expected, got `1`"); 185 } 186 187 T1 expect(typeof(null) null_, T1)(lazy T1 lhs, Fence _ = Fence(), string file = __FILE__, size_t line = __LINE__) { 188 if(lhs !is null) 189 throw new ExpectException( 190 "null is expected, got `%s`".format(lhs), 191 file, 192 line, 193 ); 194 195 return lhs; 196 } 197 198 @("expect!null") 199 @safe pure unittest { 200 void delegate() func; 201 func.expect!null; 202 203 null.expect!null; 204 } 205 206 @("expect!null fails if value is not null") 207 unittest { 208 ({ 209 void delegate() func = () {}; 210 211 func.expect!null; 212 }).expect!(throw_, ExpectException) 213 .message.expect!contain("null is expected, got `void delegate()`"); 214 215 ({ 216 auto v = true; 217 (&v).expect!null; 218 }).expect!(throw_, ExpectException) 219 .message.expect!match("null is expected, got `.*`"); 220 } 221 222 T1 expect(OP, T1, T2)(lazy T1 lhs, lazy T2 rhs, Fence _ = Fence(), string file = __FILE__, size_t line = __LINE__) 223 if(is(OP == contain) && __traits(compiles, {import std.algorithm.searching : canFind; lhs.canFind(rhs);})) { 224 import std.algorithm.searching : canFind; 225 if(!lhs.canFind(rhs)) 226 throw new ExpectException( 227 "value is expected to contain `%s`, got `%s`".format(rhs, lhs), 228 file, 229 line, 230 ); 231 232 return lhs; 233 } 234 235 @("expect!contain") 236 @safe pure unittest { 237 "Hello, World!".expect!contain("World"); 238 [1,2,3].expect!contain(1); 239 [[1,2],[2,3],[4,5]].expect!contain([1,2]); 240 } 241 242 @("expect!contain checks can be chained") 243 @safe pure unittest { 244 "Hello, World!".expect!contain("World") 245 .expect!contain("Hello") 246 .expect!contain('!') 247 .expect!contain(',') 248 .expect!contain(' '); 249 } 250 251 @("expect!contain fails if value does not contain expected data") 252 unittest { 253 ({ 254 "Hello, World!".expect!contain("Hi!"); 255 }).expect!(throw_, ExpectException) 256 .message.expect!contain("value is expected to contain `Hi!`, got `Hello, World!`"); 257 258 ({ 259 [1,2,3].expect!contain(4); 260 }).expect!(throw_, ExpectException) 261 .message.expect!contain("value is expected to contain `4`, got `[1, 2, 3]`"); 262 } 263 264 T1 expect(OP, T1)(lazy T1 lhs, string re, Fence _ = Fence(), string file = __FILE__, size_t line = __LINE__) 265 if(is(OP == match) && __traits(compiles, {import std.regex : matchFirst; matchFirst(lhs, re);})) { 266 import std.regex : matchFirst; 267 if(lhs.matchFirst(re).empty) 268 throw new ExpectException( 269 "value matching `%s` is expected, got `%s`".format(re, lhs), 270 file, 271 line, 272 ); 273 274 return lhs; 275 } 276 277 @("expect!match") 278 @safe unittest { 279 "12345".expect!match(r"\d\d\d\d\d"); 280 "abc".expect!match(r"\w{3}"); 281 } 282 283 @("expect!match checks can be chained") 284 @safe unittest { 285 "123abc".expect!match(r"\d\d\d\w\w\w") 286 .expect!match(r"\d{3}.*") 287 .expect!match(r"[1-3]+[a-c]+"); 288 } 289 290 @("expect!match fails if value does not match provided regex") 291 unittest { 292 ({ 293 "123".expect!match(r"abc"); 294 }).expect!(throw_, ExpectException) 295 .message.expect!contain("value matching `abc` is expected, got `123`"); 296 } 297 298 auto expect(OP, E : Exception = Exception, T1)(lazy T1 lhs, Fence _ = Fence(), string file = __FILE__, size_t line = __LINE__) 299 if(is(OP == throw_) && __traits(compiles, {lhs(/+_+/)(/*_*/);})) { 300 struct Result { 301 T1 data; 302 string message; 303 } 304 305 Result r = Result(lhs); 306 307 try { 308 lhs()(); 309 } catch(Exception e) { 310 if(!(cast(E) e)) 311 throw new ExpectException( 312 "`%s` is expected to be thrown, an exception of type `%s` has been thrown instead".format( 313 typeid(E).name, 314 typeid(e).name), 315 file, 316 line, 317 e, 318 ); 319 320 r.message = e.message.idup; 321 return r; 322 } 323 324 throw new ExpectException( 325 "`%s` is expected to be thrown but nothing has been thrown".format(typeid(E).name), 326 file, 327 line, 328 ); 329 } 330 331 @("expect!throw_") 332 unittest { 333 (() { 334 throw new Exception("Hello!"); 335 }).expect!throw_; 336 } 337 338 @("expect!(throw_, CustomException)") 339 unittest { 340 final class CustomException : Exception { 341 pure nothrow @nogc @safe this(string msg) { 342 super(msg); 343 } 344 } 345 346 ({ 347 throw new CustomException("message"); 348 }).expect!(throw_, CustomException); 349 350 ({ 351 throw new CustomException("message"); 352 }).expect!(throw_, Exception); 353 354 ({ 355 ({ 356 throw new Exception("message"); 357 }).expect!(throw_, CustomException); 358 }).expect!(throw_, ExpectException); 359 } 360 361 @("expect!throw_ returns a tuple of the input data and message of the exception that has been thrown") 362 unittest { 363 auto func = ({ 364 throw new Exception("Hello!"); 365 }); 366 367 auto result = func.expect!throw_; 368 369 (func is result.data).expect!equal(true); 370 result.message.expect!equal("Hello!"); 371 } 372 373 @("expect!throw_ does *not* catch Errors") 374 unittest { 375 import core.exception : AssertError; 376 377 bool success; 378 try { 379 ({ 380 assert(false); 381 }).expect!throw_; 382 } catch(AssertError e) { // This is something nobody should ever do 383 success = true; 384 } 385 386 success.expect!equal(true); 387 } 388 389 @("expect!throw_ fails when nothing has been thrown") 390 unittest { 391 ({ 392 ({ 393 1.expect!true; 394 }).expect!throw_; 395 }).expect!(throw_, ExpectException) 396 .message.expect!contain("`object.Exception` is expected to be thrown but nothing has been thrown"); 397 }