1 /** 2 * This module implements custom assertions via $(D shouldXXX) functions 3 * that throw exceptions containing information about why the assertion 4 * failed. 5 */ 6 module unit_threaded.assertions; 7 8 9 import unit_threaded.exception: fail, UnitTestException; 10 import std.traits; // too many to list 11 import std.range; // also 12 13 14 /** 15 * Verify that the condition is `true`. 16 * Throws: UnitTestException on failure. 17 */ 18 void shouldBeTrue(E)(lazy E condition, string file = __FILE__, size_t line = __LINE__) 19 { 20 shouldEqual(cast(bool)condition, true, file, line); 21 } 22 23 /// 24 @safe pure unittest 25 { 26 shouldBeTrue(true); 27 } 28 29 30 /** 31 * Verify that the condition is `false`. 32 * Throws: UnitTestException on failure. 33 */ 34 void shouldBeFalse(E)(lazy E condition, string file = __FILE__, size_t line = __LINE__) 35 { 36 shouldEqual(cast(bool)condition, false, file, line); 37 } 38 39 /// 40 @safe pure unittest 41 { 42 shouldBeFalse(false); 43 } 44 45 46 /** 47 * Verify that two values are the same. 48 * Throws: UnitTestException on failure 49 */ 50 void shouldEqual(V, E)(auto ref V value, auto ref E expected, string file = __FILE__, size_t line = __LINE__) 51 { 52 if (!isEqual(value, expected)) 53 { 54 const msg = formatValueInItsOwnLine("Expected: ", expected) ~ 55 formatValueInItsOwnLine(" Got: ", value); 56 throw new UnitTestException(msg, file, line); 57 } 58 } 59 60 /// 61 @safe pure unittest { 62 shouldEqual(true, true); 63 shouldEqual(false, false); 64 shouldEqual(1, 1) ; 65 shouldEqual("foo", "foo") ; 66 shouldEqual([2, 3], [2, 3]) ; 67 68 shouldEqual(iota(3), [0, 1, 2]); 69 shouldEqual([[0, 1], [0, 1, 2]], [[0, 1], [0, 1, 2]]); 70 shouldEqual([[0, 1], [0, 1, 2]], [iota(2), iota(3)]); 71 shouldEqual([iota(2), iota(3)], [[0, 1], [0, 1, 2]]); 72 73 } 74 75 76 /** 77 * Verify that two values are not the same. 78 * Throws: UnitTestException on failure 79 */ 80 void shouldNotEqual(V, E) 81 (scope auto ref V value, 82 scope auto ref E expected, 83 string file = __FILE__, 84 size_t line = __LINE__) 85 { 86 if (isEqual(value, expected)) 87 { 88 const msg = ["Value:", 89 formatValueInItsOwnLine("", value).join(""), 90 "is not expected to be equal to:", 91 formatValueInItsOwnLine("", expected).join("") 92 ]; 93 throw new UnitTestException(msg, file, line); 94 } 95 } 96 97 /// 98 @safe pure unittest 99 { 100 shouldNotEqual(true, false); 101 shouldNotEqual(1, 2); 102 shouldNotEqual("f", "b"); 103 shouldNotEqual([2, 3], [2, 3, 4]); 104 } 105 106 /// 107 @safe unittest { 108 shouldNotEqual(1.0, 2.0); 109 } 110 111 112 113 /** 114 * Verify that the value is null. 115 * Throws: UnitTestException on failure 116 */ 117 void shouldBeNull(T)(const scope auto ref T value, string file = __FILE__, size_t line = __LINE__) 118 { 119 if (value !is null) 120 fail("Value is not null", file, line); 121 } 122 123 /// 124 @safe pure unittest 125 { 126 shouldBeNull(null); 127 } 128 129 130 /** 131 * Verify that the value is not null. 132 * Throws: UnitTestException on failure 133 */ 134 void shouldNotBeNull(T)(const scope auto ref T value, string file = __FILE__, size_t line = __LINE__) 135 { 136 if (value is null) 137 fail("Value is null", file, line); 138 } 139 140 /// 141 @safe pure unittest 142 { 143 class Foo 144 { 145 this(int i) { this.i = i; } 146 override string toString() const 147 { 148 import std.conv: to; 149 return i.to!string; 150 } 151 int i; 152 } 153 154 shouldNotBeNull(new Foo(4)); 155 } 156 157 enum isLikeAssociativeArray(T, K) = is(typeof({ 158 if(K.init in T) { } 159 if(K.init !in T) { } 160 })); 161 162 static assert(isLikeAssociativeArray!(string[string], string)); 163 static assert(!isLikeAssociativeArray!(string[string], int)); 164 165 166 /** 167 * Verify that the value is in the container. 168 * Throws: UnitTestException on failure 169 */ 170 void shouldBeIn(T, U)(const auto ref T value, const auto ref U container, string file = __FILE__, size_t line = __LINE__) 171 if (isLikeAssociativeArray!(U, T)) 172 { 173 import std.conv: to; 174 175 if (value !in container) 176 { 177 fail(formatValueInItsOwnLine("Value ", value) ~ formatValueInItsOwnLine("not in ", container), 178 file, line); 179 } 180 } 181 182 /// 183 @safe pure unittest { 184 5.shouldBeIn([5: "foo"]); 185 186 struct AA { 187 int onlyKey; 188 bool opBinaryRight(string op)(in int key) const { 189 return key == onlyKey; 190 } 191 } 192 193 5.shouldBeIn(AA(5)); 194 } 195 196 /** 197 * Verify that the value is in the container. 198 * Throws: UnitTestException on failure 199 */ 200 void shouldBeIn(T, U)(const auto ref T value, U container, string file = __FILE__, size_t line = __LINE__) 201 if (!isLikeAssociativeArray!(U, T) && isInputRange!U) 202 { 203 import std.algorithm: find; 204 import std.conv: to; 205 206 if (find(container, value).empty) 207 { 208 fail(formatValueInItsOwnLine("Value ", value) ~ formatValueInItsOwnLine("not in ", container), 209 file, line); 210 } 211 } 212 213 /// 214 @safe pure unittest 215 { 216 shouldBeIn(4, [1, 2, 4]); 217 shouldBeIn("foo", ["foo" : 1]); 218 } 219 220 221 /** 222 * Verify that the value is not in the container. 223 * Throws: UnitTestException on failure 224 */ 225 void shouldNotBeIn(T, U)(const auto ref T value, const auto ref U container, 226 string file = __FILE__, size_t line = __LINE__) 227 if (isLikeAssociativeArray!(U, T)) 228 { 229 import std.conv: to; 230 231 if (value in container) 232 { 233 fail(formatValueInItsOwnLine("Value ", value) ~ formatValueInItsOwnLine("is in ", container), 234 file, line); 235 } 236 } 237 238 /// 239 @safe pure unittest { 240 5.shouldNotBeIn([4: "foo"]); 241 242 struct AA { 243 int onlyKey; 244 bool opBinaryRight(string op)(in int key) const { 245 return key == onlyKey; 246 } 247 } 248 249 5.shouldNotBeIn(AA(4)); 250 } 251 252 253 /** 254 * Verify that the value is not in the container. 255 * Throws: UnitTestException on failure 256 */ 257 void shouldNotBeIn(T, U)(const scope auto ref T value, U container, 258 string file = __FILE__, size_t line = __LINE__) 259 if (!isLikeAssociativeArray!(U, T) && isInputRange!U) 260 { 261 import std.algorithm: find; 262 import std.conv: to; 263 264 if (!find(container, value).empty) 265 { 266 fail(formatValueInItsOwnLine("Value ", value) ~ formatValueInItsOwnLine("is in ", container), 267 file, line); 268 } 269 } 270 271 /// 272 @safe unittest 273 { 274 auto arrayRangeWithoutLength(T)(T[] array) 275 { 276 struct ArrayRangeWithoutLength(T) 277 { 278 private: 279 T[] array; 280 public: 281 T front() const @property 282 { 283 return array[0]; 284 } 285 286 void popFront() 287 { 288 array = array[1 .. $]; 289 } 290 291 bool empty() const @property 292 { 293 return array.empty; 294 } 295 } 296 return ArrayRangeWithoutLength!T(array); 297 } 298 299 shouldNotBeIn(3.5, [1.1, 2.2, 4.4]); 300 shouldNotBeIn(1.0, [2.0 : 1, 3.0 : 2]); 301 shouldNotBeIn(1, arrayRangeWithoutLength([2, 3, 4])); 302 } 303 304 private struct ThrownInfo 305 { 306 TypeInfo typeInfo; 307 string msg; 308 } 309 310 /** 311 * Verify that expr throws the templated Exception class. 312 * This succeeds if the expression throws a child class of 313 * the template parameter. 314 * Returns: A `ThrownInfo` containing info about the throwable 315 * Throws: UnitTestException on failure (when expr does not 316 * throw the expected exception) 317 */ 318 auto shouldThrow(T : Throwable = Exception, E) 319 (lazy E expr, string file = __FILE__, size_t line = __LINE__) 320 { 321 import std.conv: text; 322 323 // separate in order to not be inside the @trusted 324 auto callThrew() { return threw!T(expr); } 325 void wrongThrowableType(scope Throwable t) { 326 fail(text("Expression threw ", typeid(t), " instead of the expected ", T.stringof, ":\n", t.msg), file, line); 327 } 328 void didntThrow() { fail("Expression did not throw", file, line); } 329 330 // insert dummy call outside @trusted to correctly infer the attributes of shouldThrow 331 if (false) { 332 callThrew(); 333 wrongThrowableType(null); 334 didntThrow(); 335 } 336 337 return () @trusted { // @trusted because of catching Throwable 338 try { 339 const result = callThrew(); 340 if (result.threw) 341 return result.info; 342 } 343 catch(Throwable t) 344 wrongThrowableType(t); 345 didntThrow(); 346 assert(0); 347 }(); 348 } 349 350 /// 351 @safe pure unittest { 352 void funcThrows(string msg) { throw new Exception(msg); } 353 try { 354 auto exceptionInfo = funcThrows("foo bar").shouldThrow; 355 assert(exceptionInfo.msg == "foo bar"); 356 } catch(Exception e) { 357 assert(false, "should not have thrown anything and threw: " ~ e.msg); 358 } 359 } 360 361 /// 362 @safe pure unittest { 363 void func() {} 364 try { 365 func.shouldThrow; 366 assert(false, "Should never get here"); 367 } catch(Exception e) 368 assert(e.msg == "Expression did not throw"); 369 } 370 371 /// 372 @safe pure unittest { 373 void funcAsserts() { assert(false, "Oh noes"); } 374 try { 375 funcAsserts.shouldThrow; 376 assert(false, "Should never get here"); 377 } catch(Exception e) 378 assert(e.msg == 379 "Expression threw core.exception.AssertError instead of the expected Exception:\nOh noes"); 380 } 381 382 383 /** 384 * Verify that expr throws the templated Exception class. 385 * This only succeeds if the expression throws an exception of 386 * the exact type of the template parameter. 387 * Returns: A `ThrownInfo` containing info about the throwable 388 * Throws: UnitTestException on failure (when expr does not 389 * throw the expected exception) 390 */ 391 auto shouldThrowExactly(T : Throwable = Exception, E)(lazy E expr, 392 string file = __FILE__, size_t line = __LINE__) 393 { 394 import std.conv: text; 395 396 const threw = threw!T(expr); 397 if (!threw.threw) 398 fail("Expression did not throw", file, line); 399 400 //Object.opEquals is @system and impure 401 const sameType = () @trusted { return threw.info.typeInfo == typeid(T); }(); 402 if (!sameType) 403 fail(text("Expression threw wrong type ", threw.info.typeInfo, 404 "instead of expected type ", typeid(T)), file, line); 405 406 return threw.info; 407 } 408 409 /** 410 * Verify that expr does not throw the templated Exception class. 411 * Throws: UnitTestException on failure 412 */ 413 void shouldNotThrow(T: Throwable = Exception, E)(lazy E expr, 414 string file = __FILE__, size_t line = __LINE__) 415 { 416 if (threw!T(expr).threw) 417 fail("Expression threw", file, line); 418 } 419 420 421 /** 422 * Verify that an exception is thrown with the right message 423 */ 424 void shouldThrowWithMessage(T : Throwable = Exception, E)(lazy E expr, 425 string msg, 426 string file = __FILE__, 427 size_t line = __LINE__) { 428 auto threw = threw!T(expr); 429 430 if (!threw.threw) 431 fail("Expression did not throw", file, line); 432 433 threw.info.msg.shouldEqual(msg, file, line); 434 } 435 436 /// 437 @safe pure unittest { 438 void funcThrows(string msg) { throw new Exception(msg); } 439 funcThrows("foo bar").shouldThrowWithMessage!Exception("foo bar"); 440 funcThrows("foo bar").shouldThrowWithMessage("foo bar"); 441 } 442 443 private auto threw(T : Throwable, E)(lazy E expr) 444 { 445 import std.typecons : tuple; 446 import std.array: array; 447 import std.conv: text; 448 import std.traits: isSafe, isUnsafe; 449 450 auto ret = tuple!("threw", "info")(false, ThrownInfo.init); 451 452 // separate in order to not be inside the @trusted 453 auto makeRet(scope T e) { 454 // the message might have to be copied because of lifetime issues 455 // .msg is sometimes a range, so .array 456 // but .array sometimes returns dchar[] (autodecoding), so .text 457 return tuple!("threw", "info")(true, ThrownInfo(typeid(e), e.msg.array.dup.text)); 458 } 459 460 static if(isUnsafe!expr) 461 void callExpr() @system { expr(); } 462 else 463 void callExpr() @safe { expr(); } 464 465 auto impl() { 466 try { 467 callExpr; 468 return tuple!("threw", "info")(false, ThrownInfo.init); 469 } 470 catch(T t) { 471 return makeRet(t); 472 } 473 } 474 475 static if(isSafe!callExpr && isSafe!makeRet) 476 return () @trusted { return impl; }(); 477 else 478 return impl; 479 } 480 481 482 // Formats output in different lines 483 private string[] formatValueInItsOwnLine(T)(scope string prefix, auto ref T value) { 484 485 import std.conv: to; 486 import std.traits: isSomeString; 487 import std.range.primitives: isInputRange; 488 489 static if(isSomeString!T) { 490 // isSomeString is true for wstring and dstring, 491 // so call .to!string anyway 492 return [ prefix ~ `"` ~ value.to!string ~ `"`]; 493 } else static if(isInputRange!T) { 494 return formatRange(prefix, value); 495 } else { 496 return [prefix ~ convertToString(value)]; 497 } 498 } 499 500 // helper function for non-copyable types 501 string convertToString(T)(auto ref T value) { // std.conv.to sometimes is @system 502 import std.conv: text, to; 503 import std.traits: isFloatingPoint, isAssociativeArray; 504 import std.format: format; 505 506 static string text_(ref const(T) value) { 507 static if(isAssociativeArray!T) 508 return (scope ref const(T) value) @trusted { return value.text; }(value); 509 else static if(__traits(compiles, value.text)) 510 return text(value); 511 else static if(__traits(compiles, value.to!string)) 512 return value.to!string; 513 } 514 515 static if(isFloatingPoint!T) 516 return format!"%.6f"(value); 517 else static if(__traits(compiles, text_(value))) 518 return text_(value); 519 else static if(__traits(compiles, value.toString)) 520 return value.toString; 521 else 522 return T.stringof ~ "<cannot print>"; 523 } 524 525 526 private string[] formatRange(T)(scope string prefix, auto ref T value) { 527 import std.conv: text; 528 import std.range: ElementType; 529 import std.algorithm: map, reduce, max; 530 531 //some versions of `text` are @system 532 auto defaultLines = () @trusted { return [prefix ~ value.text]; }(); 533 534 static if (!isInputRange!(ElementType!T)) 535 return defaultLines; 536 else 537 { 538 import std.array: array; 539 const maxElementSize = value.empty ? 0 : value.map!(a => a.array.length).reduce!max; 540 const tooBigForOneLine = (value.array.length > 5 && maxElementSize > 5) || maxElementSize > 10; 541 if (!tooBigForOneLine) 542 return defaultLines; 543 return [prefix ~ "["] ~ 544 value.map!(a => formatValueInItsOwnLine(" ", a).join("") ~ ",").array ~ 545 " ]"; 546 } 547 } 548 549 private enum isObject(T) = is(T == class) || is(T == interface); 550 551 bool isEqual(V, E)(auto ref V value, auto ref E expected) 552 if (!isObject!V && !isInputRange!V && 553 is(typeof(value == expected) == bool)) 554 { 555 return value == expected; 556 } 557 558 559 // The reason this overload exists is because for some reason we can't 560 // compare a mutable AA with a const one so we force both of them to 561 // be const here, while not forcing users to have a const opEquals 562 // in their own types 563 bool isEqual(V, E)(in V value, in E expected) 564 if (isAssociativeArray!V && isAssociativeArray!E) 565 { 566 return value == expected; 567 } 568 569 570 /** 571 * Verify that two floating point values are approximately equal 572 * Params: 573 * value = the value to check. 574 * expected = the expected value 575 * maxRelDiff = the maximum relative difference 576 * maxAbsDiff = the maximum absolute difference 577 * Throws: UnitTestException on failure 578 */ 579 void shouldApproxEqual(V, E) 580 (in V value, 581 in E expected, 582 double maxRelDiff = 1e-2, 583 double maxAbsDiff = 1e-5, 584 string file = __FILE__, 585 size_t line = __LINE__) 586 if ((isFloatingPoint!V || isFloatingPoint!E) && is(typeof(value == expected) == bool)) 587 { 588 static if (__VERSION__ >= 2096) 589 import std.math: close = isClose; 590 else 591 import std.math: close = approxEqual; 592 593 if (!close(value, expected, maxRelDiff, maxAbsDiff)) 594 { 595 const msg = 596 formatValueInItsOwnLine("Expected approx: ", expected) ~ 597 formatValueInItsOwnLine(" Got : ", value); 598 throw new UnitTestException(msg, file, line); 599 } 600 } 601 602 /// 603 @safe unittest { 604 1.0.shouldApproxEqual(1.0001); 605 } 606 607 608 bool isEqual(V, E)(scope V value, scope E expected) 609 if(isInputRange!V && isStaticArray!E) 610 { 611 return isEqual(value, expected[]); 612 } 613 614 615 bool isEqual(V, E)(scope V value, scope E expected) 616 if (isInputRange!V && isInputRange!E && !isSomeString!V && 617 is(typeof(isEqual(value.front, expected.front)))) 618 { 619 620 while (!value.empty && !expected.empty) { 621 if(!isEqual(value.front, expected.front)) return false; 622 value.popFront; 623 expected.popFront; 624 } 625 626 return value.empty && expected.empty; 627 } 628 629 bool isEqual(V, E)(scope V value, scope E expected) 630 if (isSomeString!V && isSomeString!E && 631 is(typeof(isEqual(value.front, expected.front)))) 632 { 633 if(value.length != expected.length) return false; 634 // prevent auto-decoding 635 foreach(i; 0 .. value.length) 636 if(value[i] != expected[i]) return false; 637 638 return true; 639 } 640 641 template IsField(A...) if(A.length == 1) { 642 enum IsField = __traits(compiles, A[0].init); 643 } 644 645 646 bool isEqual(V, E)(scope V value, scope E expected) 647 if (isObject!V && isObject!E) 648 { 649 import std.meta: staticMap, Filter, staticIndexOf; 650 651 static assert(is(typeof(() { string s1 = value.toString; string s2 = expected.toString;})), 652 "Cannot compare instances of " ~ V.stringof ~ 653 " or " ~ E.stringof ~ " unless toString is overridden for both"); 654 655 if(value is null && expected !is null) return false; 656 if(value !is null && expected is null) return false; 657 if(value is null && expected is null) return true; 658 659 // If it has opEquals, use it 660 static if(staticIndexOf!("opEquals", __traits(derivedMembers, V)) != -1) { 661 return value.opEquals(expected); 662 } else { 663 664 template IsFieldOf(T, string s) { 665 static if(__traits(compiles, IsField!(typeof(__traits(getMember, T.init, s))))) 666 enum IsFieldOf = IsField!(typeof(__traits(getMember, T.init, s))); 667 else 668 enum IsFieldOf = false; 669 } 670 671 auto members(T)(T obj) { 672 import std.typecons: Tuple; 673 import std.meta: staticMap; 674 import std.traits: Unqual; 675 676 alias Member(string name) = typeof(__traits(getMember, T, name)); 677 alias IsFieldOfT(string s) = IsFieldOf!(T, s); 678 alias FieldNames = Filter!(IsFieldOfT, __traits(allMembers, T)); 679 alias FieldTypes = staticMap!(Member, FieldNames); 680 681 Tuple!(staticMap!(Unqual, FieldTypes)) ret; 682 foreach(i, name; FieldNames) 683 ret[i] = __traits(getMember, obj, name); 684 685 return ret; 686 } 687 688 static if(is(V == interface)) 689 return false; 690 else 691 return members(value) == members(expected); 692 } 693 } 694 695 696 /** 697 * Verify that rng is empty. 698 * Throws: UnitTestException on failure. 699 */ 700 void shouldBeEmpty(R)(const auto ref R rng, string file = __FILE__, size_t line = __LINE__) 701 if (isInputRange!R) 702 { 703 import std.conv: text; 704 if (!rng.empty) 705 fail(text("Range not empty: ", rng), file, line); 706 } 707 708 /** 709 * Verify that rng is empty. 710 * Throws: UnitTestException on failure. 711 */ 712 void shouldBeEmpty(R)(auto ref shared(R) rng, string file = __FILE__, size_t line = __LINE__) 713 if (isInputRange!R) 714 { 715 import std.conv: text; 716 if (!rng.empty) 717 fail(text("Range not empty: ", rng), file, line); 718 } 719 720 721 /** 722 * Verify that aa is empty. 723 * Throws: UnitTestException on failure. 724 */ 725 void shouldBeEmpty(T)(auto ref T aa, string file = __FILE__, size_t line = __LINE__) 726 if (isAssociativeArray!T) 727 { 728 //keys is @system 729 () @trusted{ if (!aa.keys.empty) fail("AA not empty", file, line); }(); 730 } 731 732 /// 733 @safe pure unittest 734 { 735 int[] ints; 736 string[] strings; 737 string[string] aa; 738 739 shouldBeEmpty(ints); 740 shouldBeEmpty(strings); 741 shouldBeEmpty(aa); 742 743 ints ~= 1; 744 strings ~= "foo"; 745 aa["foo"] = "bar"; 746 } 747 748 749 /** 750 * Verify that rng is not empty. 751 * Throws: UnitTestException on failure. 752 */ 753 void shouldNotBeEmpty(R)(R rng, string file = __FILE__, size_t line = __LINE__) 754 if (isInputRange!R) 755 { 756 if (rng.empty) 757 fail("Range empty", file, line); 758 } 759 760 /** 761 * Verify that aa is not empty. 762 * Throws: UnitTestException on failure. 763 */ 764 void shouldNotBeEmpty(T)(const scope auto ref T aa, string file = __FILE__, size_t line = __LINE__) 765 if (isAssociativeArray!T) 766 { 767 //keys is @system 768 () @trusted{ if (aa.keys.empty) 769 fail("AA empty", file, line); }(); 770 } 771 772 /// 773 @safe pure unittest 774 { 775 int[] ints; 776 string[] strings; 777 string[string] aa; 778 779 ints ~= 1; 780 strings ~= "foo"; 781 aa["foo"] = "bar"; 782 783 shouldNotBeEmpty(ints); 784 shouldNotBeEmpty(strings); 785 shouldNotBeEmpty(aa); 786 } 787 788 /** 789 * Verify that t is greater than u. 790 * Throws: UnitTestException on failure. 791 */ 792 void shouldBeGreaterThan(T, U)(const scope auto ref T t, const scope auto ref U u, 793 string file = __FILE__, size_t line = __LINE__) 794 { 795 import std.conv: text; 796 if (t <= u) 797 fail(text(t, " is not > ", u), file, line); 798 } 799 800 /// 801 @safe pure unittest 802 { 803 shouldBeGreaterThan(7, 5); 804 } 805 806 807 /** 808 * Verify that t is smaller than u. 809 * Throws: UnitTestException on failure. 810 */ 811 void shouldBeSmallerThan(T, U)(const scope auto ref T t, const scope auto ref U u, 812 string file = __FILE__, size_t line = __LINE__) 813 { 814 import std.conv: text; 815 if (t >= u) 816 fail(text(t, " is not < ", u), file, line); 817 } 818 819 /// 820 @safe pure unittest 821 { 822 shouldBeSmallerThan(5, 7); 823 } 824 825 826 827 /** 828 * Verify that t and u represent the same set (ordering is not important). 829 * Throws: UnitTestException on failure. 830 */ 831 void shouldBeSameSetAs(V, E)(auto ref V value, auto ref E expected, string file = __FILE__, size_t line = __LINE__) 832 if (isInputRange!V && isInputRange!E && is(typeof(value.front != expected.front) == bool)) 833 { 834 import std.algorithm: sort; 835 import std.array: array; 836 837 if (!isSameSet(value, expected)) 838 { 839 static if(__traits(compiles, sort(expected.array))) 840 const expPrintRange = sort(expected.array).array; 841 else 842 alias expPrintRange = expected; 843 844 static if(__traits(compiles, sort(value.array))) 845 const actPrintRange = sort(value.array).array; 846 else 847 alias actPrintRange = value; 848 849 const msg = formatValueInItsOwnLine("Expected: ", expPrintRange) ~ 850 formatValueInItsOwnLine(" Got: ", actPrintRange); 851 throw new UnitTestException(msg, file, line); 852 } 853 } 854 855 /// 856 @safe pure unittest 857 { 858 import std.range: iota; 859 860 auto inOrder = iota(4); 861 auto noOrder = [2, 3, 0, 1]; 862 auto oops = [2, 3, 4, 5]; 863 864 inOrder.shouldBeSameSetAs(noOrder); 865 inOrder.shouldBeSameSetAs(oops).shouldThrow!UnitTestException; 866 867 struct Struct 868 { 869 int i; 870 } 871 872 [Struct(1), Struct(4)].shouldBeSameSetAs([Struct(4), Struct(1)]); 873 } 874 875 private bool isSameSet(T, U)(auto ref T t, auto ref U u) { 876 import std.algorithm: canFind; 877 878 //sort makes the element types have to implement opCmp 879 //instead, try one by one 880 auto ta = t.array; 881 auto ua = u.array; 882 if (ta.length != ua.length) return false; 883 foreach(element; ta) 884 { 885 if (!ua.canFind(element)) return false; 886 } 887 888 return true; 889 } 890 891 /** 892 * Verify that value and expected do not represent the same set (ordering is not important). 893 * Throws: UnitTestException on failure. 894 */ 895 void shouldNotBeSameSetAs(V, E)(auto ref V value, auto ref E expected, string file = __FILE__, size_t line = __LINE__) 896 if (isInputRange!V && isInputRange!E && is(typeof(value.front != expected.front) == bool)) 897 { 898 if (isSameSet(value, expected)) 899 { 900 const msg = ["Value:", 901 formatValueInItsOwnLine("", value).join(""), 902 "is not expected to be equal to:", 903 formatValueInItsOwnLine("", expected).join("") 904 ]; 905 throw new UnitTestException(msg, file, line); 906 } 907 } 908 909 910 /// 911 @safe pure unittest 912 { 913 auto inOrder = iota(4); 914 auto noOrder = [2, 3, 0, 1]; 915 auto oops = [2, 3, 4, 5]; 916 917 inOrder.shouldNotBeSameSetAs(oops); 918 inOrder.shouldNotBeSameSetAs(noOrder).shouldThrow!UnitTestException; 919 } 920 921 922 923 924 /** 925 If two strings represent the same JSON regardless of formatting 926 */ 927 void shouldBeSameJsonAs(in string actual, 928 in string expected, 929 string file = __FILE__, 930 size_t line = __LINE__) 931 @trusted // not @safe pure due to parseJSON 932 { 933 import std.json: parseJSON, JSONException; 934 935 auto parse(in string str) { 936 try 937 return str.parseJSON; 938 catch(JSONException ex) 939 throw new UnitTestException("Error parsing JSON: " ~ ex.msg, file, line); 940 } 941 942 parse(actual).toPrettyString.shouldEqual(parse(expected).toPrettyString, file, line); 943 } 944 945 /// 946 @safe unittest { // not pure because parseJSON isn't pure 947 `{"foo": "bar"}`.shouldBeSameJsonAs(`{"foo": "bar"}`); 948 `{"foo": "bar"}`.shouldBeSameJsonAs(`{"foo":"bar"}`); 949 `{"foo":"bar"}`.shouldBeSameJsonAs(`{"foo": "baz"}`).shouldThrow!UnitTestException; 950 try 951 `oops`.shouldBeSameJsonAs(`oops`); 952 catch(Exception e) 953 assert(e.msg == "Error parsing JSON: Unexpected character 'o'. (Line 1:1)"); 954 } 955 956 957 958 auto should(V)(scope auto ref V value){ 959 960 import std.functional: forward; 961 962 struct ShouldNot { 963 964 bool opEquals(U)(auto ref U other, 965 string file = __FILE__, 966 size_t line = __LINE__) 967 { 968 shouldNotEqual(forward!value, other, file, line); 969 return true; 970 } 971 972 void opBinary(string op, R)(R range, 973 string file = __FILE__, 974 size_t line = __LINE__) const if(op == "in") { 975 shouldNotBeIn(forward!value, range, file, line); 976 } 977 978 void opBinary(string op, R)(R expected, 979 string file = __FILE__, 980 size_t line = __LINE__) const 981 if(op == "~" && isInputRange!R) 982 { 983 import std.conv: text; 984 985 bool failed; 986 987 try 988 shouldBeSameSetAs(forward!value, expected); 989 catch(UnitTestException) 990 failed = true; 991 992 if(!failed) 993 fail(text(value, " should not be the same set as ", expected), 994 file, line); 995 } 996 997 void opBinary(string op, E) 998 (in E expected, string file = __FILE__, size_t line = __LINE__) 999 if (isFloatingPoint!E) 1000 { 1001 import std.conv: text; 1002 1003 bool failed; 1004 1005 try 1006 shouldApproxEqual(forward!value, expected); 1007 catch(UnitTestException) 1008 failed = true; 1009 1010 if(!failed) 1011 fail(text(value, " should not be approximately equal to ", expected), 1012 file, line); 1013 } 1014 } 1015 1016 struct Should { 1017 1018 bool opEquals(U)(auto ref U other, 1019 string file = __FILE__, 1020 size_t line = __LINE__) 1021 { 1022 shouldEqual(forward!value, other, file, line); 1023 return true; 1024 } 1025 1026 void opBinary(string op, R)(R range, 1027 string file = __FILE__, 1028 size_t line = __LINE__) const 1029 if(op == "in") 1030 { 1031 shouldBeIn(forward!value, range, file, line); 1032 } 1033 1034 void opBinary(string op, R)(R range, 1035 string file = __FILE__, 1036 size_t line = __LINE__) const 1037 if(op == "~" && isInputRange!R) 1038 { 1039 shouldBeSameSetAs(forward!value, range, file, line); 1040 } 1041 1042 void opBinary(string op, E) 1043 (in E expected, string file = __FILE__, size_t line = __LINE__) 1044 if (isFloatingPoint!E) 1045 { 1046 shouldApproxEqual(forward!value, expected, 1e-2, 1e-5, file, line); 1047 } 1048 1049 auto not() { 1050 return ShouldNot(); 1051 } 1052 } 1053 1054 return Should(); 1055 } 1056 1057 1058 1059 T be(T)(T sh) { 1060 return sh; 1061 } 1062 1063 /// 1064 @safe pure unittest { 1065 1.should.be == 1; 1066 1.should.not.be == 2; 1067 1.should.be in [1, 2, 3]; 1068 4.should.not.be in [1, 2, 3]; 1069 } 1070 1071 1072 /** 1073 Asserts that `lowerBound` <= `actual` < `upperBound` 1074 */ 1075 void shouldBeBetween(A, L, U) 1076 (auto ref A actual, 1077 auto ref L lowerBound, 1078 auto ref U upperBound, 1079 string file = __FILE__, 1080 size_t line = __LINE__) 1081 { 1082 import std.conv: text; 1083 if(actual < lowerBound || actual >= upperBound) 1084 fail(text(actual, " is not between ", lowerBound, " and ", upperBound), file, line); 1085 }