1 module fluentasserts.core.base; 2 3 public import fluentasserts.core.array; 4 public import fluentasserts.core..string; 5 public import fluentasserts.core.objects; 6 public import fluentasserts.core.basetype; 7 public import fluentasserts.core.callable; 8 public import fluentasserts.core.results; 9 public import fluentasserts.core.lifecycle; 10 public import fluentasserts.core.expect; 11 public import fluentasserts.core.evaluation; 12 13 import std.traits; 14 import std.stdio; 15 import std.algorithm; 16 import std.array; 17 import std.range; 18 import std.conv; 19 import std..string; 20 import std.file; 21 import std.datetime; 22 import std.range.primitives; 23 import std.typecons; 24 25 @safe: 26 27 struct Result { 28 bool willThrow; 29 IResult[] results; 30 31 MessageResult message; 32 33 string file; 34 size_t line; 35 36 private string reason; 37 38 auto because(string reason) { 39 this.reason = "Because " ~ reason ~ ", "; 40 return this; 41 } 42 43 void perform() { 44 if(!willThrow) { 45 return; 46 } 47 48 version(DisableMessageResult) { 49 IResult[] localResults = this.results; 50 } else { 51 IResult[] localResults = message ~ this.results; 52 } 53 54 version(DisableSourceResult) {} else { 55 auto sourceResult = new SourceResult(file, line); 56 message.prependValue(sourceResult.getValue); 57 message.prependText(reason); 58 59 localResults ~= sourceResult; 60 } 61 62 throw new TestException(localResults, file, line); 63 } 64 65 ~this() { 66 this.perform; 67 } 68 69 static Result success() { 70 return Result(false); 71 } 72 } 73 74 struct Message { 75 bool isValue; 76 string text; 77 } 78 79 mixin template DisabledShouldThrowableCommons() { 80 auto throwSomething(string file = __FILE__, size_t line = __LINE__) { 81 static assert("`throwSomething` does not work for arrays and ranges"); 82 } 83 84 auto throwAnyException(const string file = __FILE__, const size_t line = __LINE__) { 85 static assert("`throwAnyException` does not work for arrays and ranges"); 86 } 87 88 auto throwException(T)(const string file = __FILE__, const size_t line = __LINE__) { 89 static assert("`throwException` does not work for arrays and ranges"); 90 } 91 } 92 93 mixin template ShouldThrowableCommons() { 94 auto throwSomething(string file = __FILE__, size_t line = __LINE__) { 95 addMessage(" throw "); 96 addValue("something"); 97 beginCheck; 98 99 return throwException!Throwable(file, line); 100 } 101 102 auto throwAnyException(const string file = __FILE__, const size_t line = __LINE__) { 103 addMessage(" throw "); 104 addValue("any exception"); 105 beginCheck; 106 107 return throwException!Exception(file, line); 108 } 109 110 auto throwException(T)(const string file = __FILE__, const size_t line = __LINE__) { 111 addMessage(" throw a `"); 112 addValue(T.stringof); 113 addMessage("`"); 114 115 return ThrowableProxy!T(valueEvaluation.throwable, expectedValue, messages, file, line); 116 } 117 118 private { 119 ThrowableProxy!T throwExceptionImplementation(T)(Throwable t, string file = __FILE__, size_t line = __LINE__) { 120 addMessage(" throw a `"); 121 addValue(T.stringof); 122 addMessage("`"); 123 124 bool rightType = true; 125 if(t !is null) { 126 T castedThrowable = cast(T) t; 127 rightType = castedThrowable !is null; 128 } 129 130 return ThrowableProxy!T(t, expectedValue, rightType, messages, file, line); 131 } 132 } 133 } 134 135 mixin template ShouldCommons() 136 { 137 import std..string; 138 import fluentasserts.core.results; 139 140 private ValueEvaluation valueEvaluation; 141 private bool isNegation; 142 143 private void validateException() { 144 if(valueEvaluation.throwable !is null) { 145 throw valueEvaluation.throwable; 146 } 147 } 148 149 auto be() { 150 addMessage(" be"); 151 return this; 152 } 153 154 auto should() { 155 return this; 156 } 157 158 auto not() { 159 addMessage(" not"); 160 expectedValue = !expectedValue; 161 isNegation = !isNegation; 162 163 return this; 164 } 165 166 auto forceMessage(string message) { 167 messages = []; 168 169 addMessage(message); 170 171 return this; 172 } 173 174 auto forceMessage(Message[] messages) { 175 this.messages = messages; 176 177 return this; 178 } 179 180 private { 181 Message[] messages; 182 ulong mesageCheckIndex; 183 184 bool expectedValue = true; 185 186 void addMessage(string msg) { 187 if(mesageCheckIndex != 0) { 188 return; 189 } 190 191 messages ~= Message(false, msg); 192 } 193 194 void addValue(string msg) { 195 if(mesageCheckIndex != 0) { 196 return; 197 } 198 199 messages ~= Message(true, msg); 200 } 201 202 void addValue(EquableValue msg) { 203 if(mesageCheckIndex != 0) { 204 return; 205 } 206 207 messages ~= Message(true, msg.getSerialized); 208 } 209 210 void beginCheck() { 211 if(mesageCheckIndex != 0) { 212 return; 213 } 214 215 mesageCheckIndex = messages.length; 216 } 217 218 Result simpleResult(bool value, Message[] msg, string file, size_t line) { 219 return result(value, msg, [ ], file, line); 220 } 221 222 Result result(bool value, Message[] msg, IResult res, string file, size_t line) { 223 return result(value, msg, [ res ], file, line); 224 } 225 226 Result result(bool value, IResult res, string file, size_t line) { 227 return result(value, [], [ res ], file, line); 228 } 229 230 Result result(bool value, Message[] msg, IResult[] res, const string file, const size_t line) { 231 if(res.length == 0 && msg.length == 0) { 232 return Result(false); 233 } 234 235 auto finalMessage = new MessageResult(" should"); 236 237 messages ~= Message(false, "."); 238 239 if(msg.length > 0) { 240 messages ~= Message(false, " ") ~ msg; 241 } 242 243 foreach(message; messages) { 244 if(message.isValue) { 245 finalMessage.addValue(message.text); 246 } else { 247 finalMessage.addText(message.text); 248 } 249 } 250 251 return Result(expectedValue != value, res, finalMessage, file, line); 252 } 253 } 254 } 255 256 version(Have_unit_threaded) { 257 import unit_threaded.should; 258 alias ReferenceException = UnitTestException; 259 } else { 260 alias ReferenceException = Exception; 261 } 262 263 class TestException : ReferenceException { 264 private { 265 IResult[] results; 266 } 267 268 this(IResult[] results, string fileName, size_t line, Throwable next = null) { 269 auto msg = results.map!"a.toString".filter!"a != ``".join("\n") ~ '\n'; 270 this.results = results; 271 272 super(msg, fileName, line, next); 273 } 274 275 void print(ResultPrinter printer) { 276 foreach(result; results) { 277 result.print(printer); 278 printer.primary("\n"); 279 } 280 } 281 } 282 283 /// Test Exception should separate the results by a new line 284 unittest { 285 import std.stdio; 286 IResult[] results = [ 287 cast(IResult) new MessageResult("message"), 288 cast(IResult) new SourceResult("test/missing.txt", 10), 289 cast(IResult) new DiffResult("a", "b"), 290 cast(IResult) new ExpectedActualResult("a", "b"), 291 cast(IResult) new ExtraMissingResult("a", "b") ]; 292 293 auto exception = new TestException(results, "unknown", 0); 294 295 exception.msg.should.equal(`message 296 297 -------------------- 298 test/missing.txt:10 299 -------------------- 300 301 Diff: 302 [-a][+b] 303 304 Expected:a 305 Actual:b 306 307 Extra:a 308 Missing:b 309 `); 310 } 311 312 @("TestException should concatenate all the Result strings") 313 unittest { 314 class TestResult : IResult { 315 override string toString() { 316 return "message"; 317 } 318 319 void print(ResultPrinter) {} 320 } 321 322 auto exception = new TestException([ new TestResult, new TestResult, new TestResult], "", 0); 323 324 exception.msg.should.equal("message\nmessage\nmessage\n"); 325 } 326 327 @("TestException should call all the result print methods on print") 328 unittest { 329 int count; 330 331 class TestResult : IResult { 332 override string toString() { 333 return ""; 334 } 335 336 void print(ResultPrinter) { 337 count++; 338 } 339 } 340 341 auto exception = new TestException([ new TestResult, new TestResult, new TestResult], "", 0); 342 exception.print(new DefaultResultPrinter); 343 344 count.should.equal(3); 345 } 346 347 struct ThrowableProxy(T : Throwable) { 348 import fluentasserts.core.results; 349 350 private const { 351 bool expectedValue; 352 const string _file; 353 size_t _line; 354 } 355 356 private { 357 Message[] messages; 358 string reason; 359 bool check; 360 Throwable thrown; 361 T thrownTyped; 362 } 363 364 this(Throwable thrown, bool expectedValue, Message[] messages, const string file, size_t line) { 365 this.expectedValue = expectedValue; 366 this._file = file; 367 this._line = line; 368 this.thrown = thrown; 369 this.thrownTyped = cast(T) thrown; 370 this.messages = messages; 371 this.check = true; 372 } 373 374 ~this() { 375 checkException; 376 } 377 378 auto msg() { 379 checkException; 380 check = false; 381 382 return thrown.msg.dup.to!string.strip; 383 } 384 385 auto original() { 386 checkException; 387 check = false; 388 389 return thrownTyped; 390 } 391 392 auto file() { 393 checkException; 394 check = false; 395 396 return thrown.file; 397 } 398 399 auto info() { 400 checkException; 401 check = false; 402 403 return thrown.info; 404 } 405 406 auto line() { 407 checkException; 408 check = false; 409 410 return thrown.line; 411 } 412 413 auto next() { 414 checkException; 415 check = false; 416 417 return thrown.next; 418 } 419 420 auto withMessage() { 421 auto s = ShouldString(msg); 422 check = false; 423 424 return s.forceMessage(messages ~ Message(false, " with message")); 425 } 426 427 auto withMessage(string expectedMessage) { 428 auto s = ShouldString(msg); 429 check = false; 430 431 return s.forceMessage(messages ~ Message(false, " with message")).equal(expectedMessage); 432 } 433 434 private void checkException() { 435 if(!check) { 436 return; 437 } 438 439 bool hasException = thrown !is null; 440 bool hasTypedException = thrownTyped !is null; 441 442 if(hasException == expectedValue && hasTypedException == expectedValue) { 443 return; 444 } 445 446 auto sourceResult = new SourceResult(_file, _line); 447 auto message = new MessageResult(""); 448 449 if(reason != "") { 450 message.addText("Because " ~ reason ~ ", "); 451 } 452 453 message.addText(sourceResult.getValue ~ " should"); 454 455 foreach(msg; messages) { 456 if(msg.isValue) { 457 message.addValue(msg.text); 458 } else { 459 message.addText(msg.text); 460 } 461 } 462 463 message.addText("."); 464 465 if(thrown is null) { 466 message.addText(" Nothing was thrown."); 467 } else { 468 message.addText(" An exception of type `"); 469 message.addValue(thrown.classinfo.name); 470 message.addText("` saying `"); 471 message.addValue(thrown.msg); 472 message.addText("` was thrown."); 473 } 474 475 throw new TestException([ cast(IResult) message ], _file, _line); 476 } 477 478 auto because(string reason) { 479 this.reason = reason; 480 481 return this; 482 } 483 } 484 485 auto should(T)(lazy T testData, const string file = __FILE__, const size_t line = __LINE__) @trusted { 486 static if(is(T == void)) { 487 auto callable = ({ testData; }); 488 return expect(callable, file, line); 489 } else { 490 return expect(testData, file, line); 491 } 492 } 493 494 @("because") 495 unittest { 496 auto msg = ({ 497 true.should.equal(false).because("of test reasons"); 498 }).should.throwException!TestException.msg; 499 500 msg.split("\n")[0].should.equal("Because of test reasons, true should equal false."); 501 } 502 503 struct Assert { 504 static void opDispatch(string s, T, U)(T actual, U expected, string reason = "", const string file = __FILE__, const size_t line = __LINE__) 505 { 506 auto sh = expect(actual); 507 508 static if(s[0..3] == "not") { 509 sh.not; 510 enum assertName = s[3..4].toLower ~ s[4..$]; 511 } else { 512 enum assertName = s; 513 } 514 515 static if(assertName == "greaterThan" || 516 assertName == "lessThan" || 517 assertName == "above" || 518 assertName == "below" || 519 assertName == "between" || 520 assertName == "within" || 521 assertName == "approximately") { 522 sh.be; 523 } 524 525 mixin("auto result = sh." ~ assertName ~ "(expected);"); 526 527 if(reason != "") { 528 result.because(reason); 529 } 530 } 531 532 static void between(T, U)(T actual, U begin, U end, string reason = "", const string file = __FILE__, const size_t line = __LINE__) 533 { 534 auto s = expect(actual, file, line).to.be.between(begin, end); 535 536 if(reason != "") { 537 s.because(reason); 538 } 539 } 540 541 static void notBetween(T, U)(T actual, U begin, U end, string reason = "", const string file = __FILE__, const size_t line = __LINE__) 542 { 543 auto s = expect(actual, file, line).not.to.be.between(begin, end); 544 545 if(reason != "") { 546 s.because(reason); 547 } 548 } 549 550 static void within(T, U)(T actual, U begin, U end, string reason = "", const string file = __FILE__, const size_t line = __LINE__) 551 { 552 auto s = expect(actual, file, line).to.be.between(begin, end); 553 554 if(reason != "") { 555 s.because(reason); 556 } 557 } 558 559 static void notWithin(T, U)(T actual, U begin, U end, string reason = "", const string file = __FILE__, const size_t line = __LINE__) 560 { 561 auto s = expect(actual, file, line).not.to.be.between(begin, end); 562 563 if(reason != "") { 564 s.because(reason); 565 } 566 } 567 568 static void approximately(T, U, V)(T actual, U expected, V delta, string reason = "", const string file = __FILE__, const size_t line = __LINE__) 569 { 570 auto s = expect(actual, file, line).to.be.approximately(expected, delta); 571 572 if(reason != "") { 573 s.because(reason); 574 } 575 } 576 577 static void notApproximately(T, U, V)(T actual, U expected, V delta, string reason = "", const string file = __FILE__, const size_t line = __LINE__) 578 { 579 auto s = expect(actual, file, line).not.to.be.approximately(expected, delta); 580 581 if(reason != "") { 582 s.because(reason); 583 } 584 } 585 586 static void beNull(T)(T actual, string reason = "", const string file = __FILE__, const size_t line = __LINE__) 587 { 588 auto s = expect(actual, file, line).to.beNull; 589 590 if(reason != "") { 591 s.because(reason); 592 } 593 } 594 595 static void notNull(T)(T actual, string reason = "", const string file = __FILE__, const size_t line = __LINE__) 596 { 597 auto s = expect(actual, file, line).not.to.beNull; 598 599 if(reason != "") { 600 s.because(reason); 601 } 602 } 603 } 604 605 /// Assert should work for base types 606 unittest { 607 Assert.equal(1, 1, "they are the same value"); 608 Assert.notEqual(1, 2, "they are not the same value"); 609 610 Assert.greaterThan(1, 0); 611 Assert.notGreaterThan(0, 1); 612 613 Assert.lessThan(0, 1); 614 Assert.notLessThan(1, 0); 615 616 Assert.above(1, 0); 617 Assert.notAbove(0, 1); 618 619 Assert.below(0, 1); 620 Assert.notBelow(1, 0); 621 622 Assert.between(1, 0, 2); 623 Assert.notBetween(3, 0, 2); 624 625 Assert.within(1, 0, 2); 626 Assert.notWithin(3, 0, 2); 627 628 Assert.approximately(1.5f, 1, 0.6f); 629 Assert.notApproximately(1.5f, 1, 0.2f); 630 } 631 632 /// Assert should work for objects 633 unittest { 634 Object o = null; 635 Assert.beNull(o, "it's a null"); 636 Assert.notNull(new Object, "it's not a null"); 637 } 638 639 /// Assert should work for strings 640 unittest { 641 Assert.equal("abcd", "abcd"); 642 Assert.notEqual("abcd", "abwcd"); 643 644 Assert.contain("abcd", "bc"); 645 Assert.notContain("abcd", 'e'); 646 647 Assert.startWith("abcd", "ab"); 648 Assert.notStartWith("abcd", "bc"); 649 650 Assert.startWith("abcd", 'a'); 651 Assert.notStartWith("abcd", 'b'); 652 653 Assert.endWith("abcd", "cd"); 654 Assert.notEndWith("abcd", "bc"); 655 656 Assert.endWith("abcd", 'd'); 657 Assert.notEndWith("abcd", 'c'); 658 } 659 660 /// Assert should work for ranges 661 unittest { 662 Assert.equal([1, 2, 3], [1, 2, 3]); 663 Assert.notEqual([1, 2, 3], [1, 1, 3]); 664 665 Assert.contain([1, 2, 3], 3); 666 Assert.notContain([1, 2, 3], [5, 6]); 667 668 Assert.containOnly([1, 2, 3], [3, 2, 1]); 669 Assert.notContainOnly([1, 2, 3], [3, 1]); 670 } 671 672 void fluentHandler(string file, size_t line, string msg) nothrow { 673 import core.exception; 674 675 auto message = new MessageResult("Assert failed. " ~ msg); 676 auto source = new SourceResult(file, line); 677 678 throw new AssertError(message.toString ~ "\n\n" ~ source.toString, file, line); 679 } 680 681 void setupFluentHandler() { 682 import core.exception; 683 core.exception.assertHandler = &fluentHandler; 684 } 685 686 /// It should call the fluent handler 687 @trusted 688 unittest { 689 import core.exception; 690 691 setupFluentHandler; 692 scope(exit) core.exception.assertHandler = null; 693 694 bool thrown = false; 695 696 try { 697 assert(false, "What?"); 698 } catch(Throwable t) { 699 thrown = true; 700 t.msg.should.startWith("Assert failed. What?\n"); 701 } 702 703 thrown.should.equal(true); 704 }