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