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