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__, in 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__, in 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)(scope auto ref V value, scope auto ref E expected, string file = __FILE__, in 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 in 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)(in auto ref T value, string file = __FILE__, in 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)(in auto ref T value, string file = __FILE__, in 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)(in auto ref T value, in auto ref U container, string file = __FILE__, in 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)(in auto ref T value, U container, string file = __FILE__, in 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)(in auto ref T value, in auto ref U container, 226 string file = __FILE__, in 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)(in auto ref T value, U container, 258 string file = __FILE__, in 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__, in 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__, in 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__, in 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)(in string prefix, scope 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)(scope 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_(scope 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)(in string prefix, scope 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 import std.math: approxEqual; 589 if (!approxEqual(value, expected, maxRelDiff, maxAbsDiff)) 590 { 591 const msg = 592 formatValueInItsOwnLine("Expected approx: ", expected) ~ 593 formatValueInItsOwnLine(" Got : ", value); 594 throw new UnitTestException(msg, file, line); 595 } 596 } 597 598 /// 599 @safe unittest { 600 1.0.shouldApproxEqual(1.0001); 601 } 602 603 604 bool isEqual(V, E)(scope V value, scope E expected) 605 if(isInputRange!V && isStaticArray!E) 606 { 607 return isEqual(value, expected[]); 608 } 609 610 611 bool isEqual(V, E)(scope V value, scope E expected) 612 if (isInputRange!V && isInputRange!E && !isSomeString!V && 613 is(typeof(isEqual(value.front, expected.front)))) 614 { 615 616 while (!value.empty && !expected.empty) { 617 if(!isEqual(value.front, expected.front)) return false; 618 value.popFront; 619 expected.popFront; 620 } 621 622 return value.empty && expected.empty; 623 } 624 625 bool isEqual(V, E)(scope V value, scope E expected) 626 if (isSomeString!V && isSomeString!E && 627 is(typeof(isEqual(value.front, expected.front)))) 628 { 629 if(value.length != expected.length) return false; 630 // prevent auto-decoding 631 foreach(i; 0 .. value.length) 632 if(value[i] != expected[i]) return false; 633 634 return true; 635 } 636 637 template IsField(A...) if(A.length == 1) { 638 enum IsField = __traits(compiles, A[0].init); 639 } 640 641 642 bool isEqual(V, E)(scope V value, scope E expected) 643 if (isObject!V && isObject!E) 644 { 645 import std.meta: staticMap, Filter, staticIndexOf; 646 647 static assert(is(typeof(() { string s1 = value.toString; string s2 = expected.toString;})), 648 "Cannot compare instances of " ~ V.stringof ~ 649 " or " ~ E.stringof ~ " unless toString is overridden for both"); 650 651 if(value is null && expected !is null) return false; 652 if(value !is null && expected is null) return false; 653 if(value is null && expected is null) return true; 654 655 // If it has opEquals, use it 656 static if(staticIndexOf!("opEquals", __traits(derivedMembers, V)) != -1) { 657 return value.opEquals(expected); 658 } else { 659 660 template IsFieldOf(T, string s) { 661 static if(__traits(compiles, IsField!(typeof(__traits(getMember, T.init, s))))) 662 enum IsFieldOf = IsField!(typeof(__traits(getMember, T.init, s))); 663 else 664 enum IsFieldOf = false; 665 } 666 667 auto members(T)(T obj) { 668 import std.typecons: Tuple; 669 import std.meta: staticMap; 670 import std.traits: Unqual; 671 672 alias Member(string name) = typeof(__traits(getMember, T, name)); 673 alias IsFieldOfT(string s) = IsFieldOf!(T, s); 674 alias FieldNames = Filter!(IsFieldOfT, __traits(allMembers, T)); 675 alias FieldTypes = staticMap!(Member, FieldNames); 676 677 Tuple!(staticMap!(Unqual, FieldTypes)) ret; 678 foreach(i, name; FieldNames) 679 ret[i] = __traits(getMember, obj, name); 680 681 return ret; 682 } 683 684 static if(is(V == interface)) 685 return false; 686 else 687 return members(value) == members(expected); 688 } 689 } 690 691 692 /** 693 * Verify that rng is empty. 694 * Throws: UnitTestException on failure. 695 */ 696 void shouldBeEmpty(R)(in auto ref R rng, string file = __FILE__, in size_t line = __LINE__) 697 if (isInputRange!R) 698 { 699 import std.conv: text; 700 if (!rng.empty) 701 fail(text("Range not empty: ", rng), file, line); 702 } 703 704 /** 705 * Verify that rng is empty. 706 * Throws: UnitTestException on failure. 707 */ 708 void shouldBeEmpty(R)(auto ref shared(R) rng, string file = __FILE__, in size_t line = __LINE__) 709 if (isInputRange!R) 710 { 711 import std.conv: text; 712 if (!rng.empty) 713 fail(text("Range not empty: ", rng), file, line); 714 } 715 716 717 /** 718 * Verify that aa is empty. 719 * Throws: UnitTestException on failure. 720 */ 721 void shouldBeEmpty(T)(auto ref T aa, string file = __FILE__, in size_t line = __LINE__) 722 if (isAssociativeArray!T) 723 { 724 //keys is @system 725 () @trusted{ if (!aa.keys.empty) fail("AA not empty", file, line); }(); 726 } 727 728 /// 729 @safe pure unittest 730 { 731 int[] ints; 732 string[] strings; 733 string[string] aa; 734 735 shouldBeEmpty(ints); 736 shouldBeEmpty(strings); 737 shouldBeEmpty(aa); 738 739 ints ~= 1; 740 strings ~= "foo"; 741 aa["foo"] = "bar"; 742 } 743 744 745 /** 746 * Verify that rng is not empty. 747 * Throws: UnitTestException on failure. 748 */ 749 void shouldNotBeEmpty(R)(R rng, string file = __FILE__, in size_t line = __LINE__) 750 if (isInputRange!R) 751 { 752 if (rng.empty) 753 fail("Range empty", file, line); 754 } 755 756 /** 757 * Verify that aa is not empty. 758 * Throws: UnitTestException on failure. 759 */ 760 void shouldNotBeEmpty(T)(in auto ref T aa, string file = __FILE__, in size_t line = __LINE__) 761 if (isAssociativeArray!T) 762 { 763 //keys is @system 764 () @trusted{ if (aa.keys.empty) 765 fail("AA empty", file, line); }(); 766 } 767 768 /// 769 @safe pure unittest 770 { 771 int[] ints; 772 string[] strings; 773 string[string] aa; 774 775 ints ~= 1; 776 strings ~= "foo"; 777 aa["foo"] = "bar"; 778 779 shouldNotBeEmpty(ints); 780 shouldNotBeEmpty(strings); 781 shouldNotBeEmpty(aa); 782 } 783 784 /** 785 * Verify that t is greater than u. 786 * Throws: UnitTestException on failure. 787 */ 788 void shouldBeGreaterThan(T, U)(in auto ref T t, in auto ref U u, 789 string file = __FILE__, in size_t line = __LINE__) 790 { 791 import std.conv: text; 792 if (t <= u) 793 fail(text(t, " is not > ", u), file, line); 794 } 795 796 /// 797 @safe pure unittest 798 { 799 shouldBeGreaterThan(7, 5); 800 } 801 802 803 /** 804 * Verify that t is smaller than u. 805 * Throws: UnitTestException on failure. 806 */ 807 void shouldBeSmallerThan(T, U)(in auto ref T t, in auto ref U u, 808 string file = __FILE__, in size_t line = __LINE__) 809 { 810 import std.conv: text; 811 if (t >= u) 812 fail(text(t, " is not < ", u), file, line); 813 } 814 815 /// 816 @safe pure unittest 817 { 818 shouldBeSmallerThan(5, 7); 819 } 820 821 822 823 /** 824 * Verify that t and u represent the same set (ordering is not important). 825 * Throws: UnitTestException on failure. 826 */ 827 void shouldBeSameSetAs(V, E)(auto ref V value, auto ref E expected, string file = __FILE__, in size_t line = __LINE__) 828 if (isInputRange!V && isInputRange!E && is(typeof(value.front != expected.front) == bool)) 829 { 830 import std.algorithm: sort; 831 import std.array: array; 832 833 if (!isSameSet(value, expected)) 834 { 835 static if(__traits(compiles, sort(expected.array))) 836 const expPrintRange = sort(expected.array).array; 837 else 838 alias expPrintRange = expected; 839 840 static if(__traits(compiles, sort(value.array))) 841 const actPrintRange = sort(value.array).array; 842 else 843 alias actPrintRange = value; 844 845 const msg = formatValueInItsOwnLine("Expected: ", expPrintRange) ~ 846 formatValueInItsOwnLine(" Got: ", actPrintRange); 847 throw new UnitTestException(msg, file, line); 848 } 849 } 850 851 /// 852 @safe pure unittest 853 { 854 import std.range: iota; 855 856 auto inOrder = iota(4); 857 auto noOrder = [2, 3, 0, 1]; 858 auto oops = [2, 3, 4, 5]; 859 860 inOrder.shouldBeSameSetAs(noOrder); 861 inOrder.shouldBeSameSetAs(oops).shouldThrow!UnitTestException; 862 863 struct Struct 864 { 865 int i; 866 } 867 868 [Struct(1), Struct(4)].shouldBeSameSetAs([Struct(4), Struct(1)]); 869 } 870 871 private bool isSameSet(T, U)(auto ref T t, auto ref U u) { 872 import std.algorithm: canFind; 873 874 //sort makes the element types have to implement opCmp 875 //instead, try one by one 876 auto ta = t.array; 877 auto ua = u.array; 878 if (ta.length != ua.length) return false; 879 foreach(element; ta) 880 { 881 if (!ua.canFind(element)) return false; 882 } 883 884 return true; 885 } 886 887 /** 888 * Verify that value and expected do not represent the same set (ordering is not important). 889 * Throws: UnitTestException on failure. 890 */ 891 void shouldNotBeSameSetAs(V, E)(auto ref V value, auto ref E expected, string file = __FILE__, in size_t line = __LINE__) 892 if (isInputRange!V && isInputRange!E && is(typeof(value.front != expected.front) == bool)) 893 { 894 if (isSameSet(value, expected)) 895 { 896 const msg = ["Value:", 897 formatValueInItsOwnLine("", value).join(""), 898 "is not expected to be equal to:", 899 formatValueInItsOwnLine("", expected).join("") 900 ]; 901 throw new UnitTestException(msg, file, line); 902 } 903 } 904 905 906 /// 907 @safe pure unittest 908 { 909 auto inOrder = iota(4); 910 auto noOrder = [2, 3, 0, 1]; 911 auto oops = [2, 3, 4, 5]; 912 913 inOrder.shouldNotBeSameSetAs(oops); 914 inOrder.shouldNotBeSameSetAs(noOrder).shouldThrow!UnitTestException; 915 } 916 917 918 919 920 /** 921 If two strings represent the same JSON regardless of formatting 922 */ 923 void shouldBeSameJsonAs(in string actual, 924 in string expected, 925 string file = __FILE__, 926 in size_t line = __LINE__) 927 @trusted // not @safe pure due to parseJSON 928 { 929 import std.json: parseJSON, JSONException; 930 931 auto parse(in string str) { 932 try 933 return str.parseJSON; 934 catch(JSONException ex) 935 throw new UnitTestException("Error parsing JSON: " ~ ex.msg, file, line); 936 } 937 938 parse(actual).toPrettyString.shouldEqual(parse(expected).toPrettyString, file, line); 939 } 940 941 /// 942 @safe unittest { // not pure because parseJSON isn't pure 943 `{"foo": "bar"}`.shouldBeSameJsonAs(`{"foo": "bar"}`); 944 `{"foo": "bar"}`.shouldBeSameJsonAs(`{"foo":"bar"}`); 945 `{"foo":"bar"}`.shouldBeSameJsonAs(`{"foo": "baz"}`).shouldThrow!UnitTestException; 946 try 947 `oops`.shouldBeSameJsonAs(`oops`); 948 catch(Exception e) 949 assert(e.msg == "Error parsing JSON: Unexpected character 'o'. (Line 1:1)"); 950 } 951 952 953 954 auto should(V)(scope auto ref V value){ 955 956 import std.functional: forward; 957 958 struct ShouldNot { 959 960 bool opEquals(U)(auto ref U other, 961 string file = __FILE__, 962 in size_t line = __LINE__) 963 { 964 shouldNotEqual(forward!value, other, file, line); 965 return true; 966 } 967 968 void opBinary(string op, R)(R range, 969 string file = __FILE__, 970 in size_t line = __LINE__) const if(op == "in") { 971 shouldNotBeIn(forward!value, range, file, line); 972 } 973 974 void opBinary(string op, R)(R expected, 975 string file = __FILE__, 976 in size_t line = __LINE__) const 977 if(op == "~" && isInputRange!R) 978 { 979 import std.conv: text; 980 981 bool failed; 982 983 try 984 shouldBeSameSetAs(forward!value, expected); 985 catch(UnitTestException) 986 failed = true; 987 988 if(!failed) 989 fail(text(value, " should not be the same set as ", expected), 990 file, line); 991 } 992 993 void opBinary(string op, E) 994 (in E expected, string file = __FILE__, size_t line = __LINE__) 995 if (isFloatingPoint!E) 996 { 997 import std.conv: text; 998 999 bool failed; 1000 1001 try 1002 shouldApproxEqual(forward!value, expected); 1003 catch(UnitTestException) 1004 failed = true; 1005 1006 if(!failed) 1007 fail(text(value, " should not be approximately equal to ", expected), 1008 file, line); 1009 } 1010 } 1011 1012 struct Should { 1013 1014 bool opEquals(U)(auto ref U other, 1015 string file = __FILE__, 1016 in size_t line = __LINE__) 1017 { 1018 shouldEqual(forward!value, other, file, line); 1019 return true; 1020 } 1021 1022 void opBinary(string op, R)(R range, 1023 string file = __FILE__, 1024 in size_t line = __LINE__) const 1025 if(op == "in") 1026 { 1027 shouldBeIn(forward!value, range, file, line); 1028 } 1029 1030 void opBinary(string op, R)(R range, 1031 string file = __FILE__, 1032 in size_t line = __LINE__) const 1033 if(op == "~" && isInputRange!R) 1034 { 1035 shouldBeSameSetAs(forward!value, range, file, line); 1036 } 1037 1038 void opBinary(string op, E) 1039 (in E expected, string file = __FILE__, size_t line = __LINE__) 1040 if (isFloatingPoint!E) 1041 { 1042 shouldApproxEqual(forward!value, expected, 1e-2, 1e-5, file, line); 1043 } 1044 1045 auto not() { 1046 return ShouldNot(); 1047 } 1048 } 1049 1050 return Should(); 1051 } 1052 1053 1054 1055 T be(T)(T sh) { 1056 return sh; 1057 } 1058 1059 /// 1060 @safe pure unittest { 1061 1.should.be == 1; 1062 1.should.not.be == 2; 1063 1.should.be in [1, 2, 3]; 1064 4.should.not.be in [1, 2, 3]; 1065 } 1066 1067 1068 /** 1069 Asserts that `lowerBound` <= `actual` < `upperBound` 1070 */ 1071 void shouldBeBetween(A, L, U) 1072 (auto ref A actual, 1073 auto ref L lowerBound, 1074 auto ref U upperBound, 1075 string file = __FILE__, 1076 in size_t line = __LINE__) 1077 { 1078 import std.conv: text; 1079 if(actual < lowerBound || actual >= upperBound) 1080 fail(text(actual, " is not between ", lowerBound, " and ", upperBound), file, line); 1081 }