1 /** 2 * Pijamas, a BDD assertion library for D. 3 * 4 * License: Licensed under the MIT license. See LICENSE for more information. 5 */ 6 module pijamas.assertion; 7 8 import std.algorithm : canFind, isSorted; 9 import std.range : isInputRange, isForwardRange, hasLength, ElementEncodingType, empty; 10 import std.regex : Regex, StaticRegex; 11 import std.traits : hasMember, isSomeString, isCallable, isArray, isAssociativeArray, 12 isImplicitlyConvertible, Unqual; 13 14 import pijamas.exception; 15 16 /** 17 * The function **should** it's an helper or syntax sugar to create the assertation. 18 * Because of D’s lookup shortcut syntax, one is able to use both `should(obj)` and `obj.should` to 19 * get an object wrapped around an Assertion instance 20 */ 21 public Assertion!T should(T)(auto ref T context) { 22 return Assertion!T(context); 23 } 24 25 /** 26 * The function **expect** it's an helper or syntax sugar to create the assertation. 27 * Because of D’s lookup shortcut syntax, one is able to use both `expect(obj)` and `obj.expect` to 28 * get an object wrapped around an Assertion instance 29 */ 30 public alias expect = should; 31 32 /// Class returned by should, that it's used to generate the fluent API 33 struct Assertion(T) { 34 private static bool callable = isCallable!T; 35 private bool negated = false; 36 private string operator = "be"; 37 private T context; 38 39 /// Creates a instance of Assertion, wrapping a value or object 40 this(T _context) @safe pure nothrow { 41 context = _context; 42 } 43 ///ditto 44 this(ref T _context) @safe pure nothrow { 45 context = _context; 46 } 47 48 /// Identity function. Simply does nothing beyond making assertation more human friendly 49 alias be = id; 50 ///ditto 51 alias to = id; 52 ///ditto 53 alias as = id; 54 ///ditto 55 alias of = id; 56 ///ditto 57 alias a = id; 58 ///ditto 59 alias and = id; 60 ///ditto 61 alias have = id; 62 ///ditto 63 alias which = id; 64 ///ditto 65 Assertion id() @nogc @safe pure nothrow { 66 return this; 67 } 68 69 /** 70 * This function negates the wrapper assertion. With it, one can express fluent assertions without much effort. 71 * 72 * Examples: 73 * ``` 74 * 10.should.not.equal(2); // OK 75 * 10.should.not.equal(10); // Throws an Exception "expected 10 not to be 10" 76 * ``` 77 */ 78 Assertion not() @nogc @safe pure nothrow { 79 negated = !negated; 80 return this; 81 } 82 83 version (MirNoGCException) { 84 // Sadly lazy not works with nogc 85 // Helper that evaluates the asserted expression 86 private U ok(U, V)(U expr, V value, string file = __FILE__, size_t line = __LINE__) @nogc @safe { 87 if (negated ? !expr : expr) { 88 return expr; 89 } 90 throw new AssertException(this.message(value).data(), file, line); 91 } 92 93 // Helper that evaluates the asserted expression 94 private U ok(U)(U expr, string file = __FILE__, size_t line = __LINE__) @nogc @safe { 95 if (negated ? !expr : expr) { 96 return expr; 97 } 98 throw new AssertException(this.message().data(), file, line); 99 } 100 101 import mir.format; 102 103 // Generates string message when an assertation fails 104 private stringBuf message(U)(U other) @trusted { 105 import std.traits; 106 import mir.conv : to; 107 import mir.small_string; 108 109 alias S = SmallString!512; 110 auto buf = stringBuf(); 111 buf << "expected [" << T.stringof << "]"; 112 static if ((__traits(isScalar, T) && !isPointer!T) || is(T == struct) || isArray!T ) { 113 buf << " " << to!S(this.context); 114 } else { 115 if (this.context is null) { 116 buf << " null"; 117 } //else { 118 // buf << to!S(this.context); 119 //} 120 } 121 buf << " to "; 122 if (negated) { 123 buf << "not "; 124 } 125 buf << operator << " [" << U.stringof << "]"; 126 static if ((__traits(isScalar, U) && !isPointer!U) || is(U == struct) || isArray!U ) { 127 buf << " " << to!S(other); 128 } else { 129 if (other is null) { 130 buf << " null"; 131 }// else { 132 // buf << to!S(other); 133 //} 134 } 135 return buf; 136 } 137 138 // Generates string message when an assertation fails 139 private stringBuf message() @trusted { 140 import std.traits; 141 import mir.conv : to; 142 import mir.small_string; 143 144 alias S = SmallString!512; 145 auto buf = stringBuf(); 146 buf << "expected [" << T.stringof << "]"; 147 static if ((__traits(isScalar, T) && !isPointer!T) || is(T == struct) || isArray!T ) { 148 buf << " " << to!S(this.context); 149 } else { 150 if (this.context is null) { 151 buf << " null"; 152 //} else { 153 // buf << to!S(this.context); 154 } 155 } 156 buf << " to "; 157 if (negated) { 158 buf << "not "; 159 } 160 buf << operator; 161 return buf; 162 } 163 164 } else { 165 166 // Helper that evaluates the asserted expression 167 private U ok(U, V)(lazy U expr, V value, string file = __FILE__, size_t line = __LINE__) @safe { 168 if (negated ? !expr : expr) { 169 return expr; 170 } 171 throw new AssertException(this.message(value), file, line); 172 } 173 174 // Helper that evaluates the asserted expression 175 private U ok(U)(lazy U expr, string file = __FILE__, size_t line = __LINE__) @safe { 176 if (negated ? !expr : expr) { 177 return expr; 178 } 179 throw new AssertException(this.message(), file, line); 180 } 181 182 // Generates string message when an assertation fails 183 private string message(U)(U other) @trusted { 184 import std.string : format; 185 import std.conv : to; 186 import std.traits : isPointer; 187 188 static if (!isPointer!T) { 189 string contextStr = to!string(this.context); 190 } else { 191 string contextStr = (this.context is null) ? "null" : this.context.to!string; 192 } 193 static if (!isPointer!U) { 194 string otherStr = to!string(other); 195 } else { 196 string otherStr = (other is null) ? "null" : other.to!string; 197 } 198 199 return format("expected [%s] %s to %s%s [%s] %s", T.stringof, context, (negated ? "not " : ""), operator, 200 U.stringof, otherStr); 201 } 202 203 // Generates string message when an assertation fails 204 private string message() @trusted { 205 import std.string : format; 206 import std.conv : to; 207 import std.traits : isPointer; 208 209 static if (!isPointer!T) { 210 string contextStr = to!string(this.context); 211 } else { 212 string contextStr = (this.context is null) ? "null" : this.context.to!string; 213 } 214 215 return format("expected [%s] %s to %s%s", T.stringof, contextStr, (negated ? "not " : ""), operator); 216 } 217 } 218 219 /** 220 * Asserts for equality between two objects. Returns the value wrapped around the assertion. 221 * 222 * Examples: 223 * ``` 224 * [1, 2, 3, 4].should.equal([1, 2, 3, 4]); 225 * 255.should.equal(10); // Throws an Exception "expected 255 to equal 10" 226 * ``` 227 */ 228 T equal(U)(auto ref U other, string file = __FILE__, size_t line = __LINE__) @trusted { 229 this.ok(context == other, other, file, line); 230 return context; 231 } 232 233 // Ripped from std.math 234 private template CommonDefaultFor(T, U) { 235 import std.traits : CommonType; 236 import std.algorithm.comparison : min; 237 238 alias baseT = FloatingPointBaseType!T; 239 alias baseU = FloatingPointBaseType!U; 240 241 enum CommonType!(baseT, baseU) CommonDefaultFor = 10.0L ^^ -((min(baseT.dig, baseU.dig) + 1) / 2 + 1); 242 } 243 244 // Ripped from std.math 245 private template FloatingPointBaseType(T) { 246 import std.traits : isFloatingPoint; 247 import std.range.primitives : ElementType; 248 249 static if (isFloatingPoint!T) { 250 alias FloatingPointBaseType = Unqual!T; 251 } else static if (isFloatingPoint!(ElementType!(Unqual!T))) { 252 alias FloatingPointBaseType = Unqual!(ElementType!(Unqual!T)); 253 } else { 254 alias FloatingPointBaseType = real; 255 } 256 } 257 258 /** 259 * Asserts that a float type is aproximated equal. Returns the valued wrapped around the assertion 260 * 261 * Params: 262 * other = Value to compare to compare. 263 * maxRelDiff = Maximum allowable relative difference. 264 * Setting to 0.0 disables this check. Default depends on the type of 265 * `other` and the original valie: It is approximately half the number of decimal digits of 266 * precision of the smaller type. 267 * maxAbsDiff = Maximum absolute difference. This is mainly usefull 268 * for comparing values to zero. Setting to 0.0 disables this check. 269 * Defaults to `0.0`. 270 * file = filename 271 * line = line number inside of file 272 * 273 * Examples: 274 * ``` 275 * double d = 0.1; 276 * double d2 = d + 1e-10; 277 * d.should.not.be.equal(d2); 278 * d.should.be.approxEqual(d2); 279 * ``` 280 */ 281 T approxEqual(U = double)(U other, U maxRelDiff = CommonDefaultFor!(T, U), U maxAbsDiff = 0.0, 282 string file = __FILE__, size_t line = __LINE__) @trusted 283 if (is(T : real) && __traits(isFloating, T) && is(U : real) && __traits(isFloating, U)) { 284 operator = "be approximated equal than"; 285 static if (__traits(compiles, { import std.math : isClose; })) { 286 import std.math : isClose; 287 288 this.ok(isClose(context, other, maxRelDiff, maxAbsDiff), other, file, line); 289 } else { 290 import std.math : approxEqual; 291 292 this.ok(approxEqual(context, other, maxRelDiff, maxAbsDiff), other, file, line); 293 } 294 return context; 295 } 296 297 /** 298 * Alias to approxEqual 299 * 300 * Examples: 301 * ``` 302 * double d = 0.1; 303 * double d2 = d + 1e-10; 304 * d.should.not.be.close(d2); 305 * d.should.be.close(d2); 306 * ``` 307 */ 308 alias close = approxEqual; 309 310 /** 311 * Asserts whether a value exists - currently simply compares it with null, if it is a pointer, a class or a string. 312 * Returns the value wrapped around the assertion. 313 * 314 * Examples: 315 * ``` 316 * auto exists = "I exist!"; 317 * should(exists).exist; 318 * string doesntexist; 319 * doesntexist.should.exist; // Throws an Exception "expected null to exist" 320 * Object aObject; 321 * aClass.should.not.exists; 322 * ``` 323 */ 324 T exist(string file = __FILE__, size_t line = __LINE__) @safe { 325 operator = "be not null or empty"; 326 import std.traits : isPointer, isSomeString; 327 328 static if (isPointer!T || isSomeString!T || __traits(compiles, () { 329 T t; 330 assert(t is null); 331 })) { 332 if (context is null) { 333 this.ok(false, file, line); 334 } 335 } 336 return context; 337 } 338 339 /** 340 * Asserts if a value is bigger than another value. Returns the result. 341 * 342 * Examples: 343 * ``` 344 * "z".should.be.biggerThan("a"); 345 * 10.should.be.biggerThan(1); 346 * ``` 347 */ 348 bool biggerThan(U)(U other, string file = __FILE__, size_t line = __LINE__) @trusted { 349 operator = "be bigger than"; 350 return this.ok(context > other, other, file, line); 351 } 352 353 /** 354 * Asserts if a value is bigger or equal than another value. Returns the result. 355 * 356 * Examples: 357 * ``` 358 * "z".should.be.biggerOrEqualThan("a"); 359 * 10.should.be.biggerOrEqualThan(10); 360 * 20.should.be.biggerOrEqualThan(10); 361 * ``` 362 */ 363 bool biggerOrEqualThan(U)(U other, string file = __FILE__, size_t line = __LINE__) @trusted { 364 operator = "be bigger or equal than"; 365 return this.ok(context >= other, other, file, line); 366 } 367 368 /** 369 * Asserts if a value is smaller than another value. Returns the result. 370 * 371 * Examples: 372 * ``` 373 * 10.should.be.smallerThan(100); 374 * false.should.be.smallerThan(true); 375 * ``` 376 */ 377 bool smallerThan(U)(U other, string file = __FILE__, size_t line = __LINE__) @trusted { 378 operator = "be smaller than"; 379 return this.ok(context < other, other, file, line); 380 } 381 382 /** 383 * Asserts if a value is smaller or euqal than another value. Returns the result. 384 * 385 * Examples: 386 * ``` 387 * 10.should.be.smallerOrEqualThan(100); 388 * 10.should.be.smallerOrEqualThan(10); 389 * false.should.be.smallerOrEqualThan(true); 390 * ``` 391 */ 392 bool smallerOrEqualThan(U)(U other, string file = __FILE__, size_t line = __LINE__) @trusted { 393 operator = "be smaller or equal than"; 394 return this.ok(context <= other, other, file, line); 395 } 396 397 static if (isForwardRange!T && __traits(compiles, context.isSorted)) { 398 /** 399 * Asserts whether a forward range is sorted. 400 * 401 * Examples: 402 * ``` 403 * [1, 2, 3, 4].should.be.sorted; 404 * [1, 2, 0, 4].should.not.be.sorted; 405 * ``` 406 */ 407 bool sorted(string file = __FILE__, size_t line = __LINE__) @trusted { 408 operator = "be sorted"; 409 return this.ok(context.isSorted, file, line); 410 } 411 } 412 413 static if (isAssociativeArray!T) { 414 /** 415 * Asserts for an associative array to have a key equal to other. 416 * 417 * Examples: 418 * ``` 419 * ["something": 10].should.have.key("something"); 420 * ``` 421 */ 422 void key(U)(U other, string file = __FILE__, size_t line = __LINE__) @trusted { 423 operator = "have key"; 424 this.ok(!(other !in context), other, file, line); 425 } 426 } 427 428 static if (isInputRange!T || isArray!T || isAssociativeArray!T) { 429 430 version (MirNoGCException) { 431 /** 432 * Asserts for an input range wrapped around an Assertion to contain/include a value. 433 * 434 * Examples: 435 * ``` 436 * [1, 2, 3, 4].should.include(3); 437 * "something".should.not.include('o'); 438 * "something".should.include("th"); 439 * ``` 440 */ 441 U include(U)(U other, string file = __FILE__, size_t line = __LINE__) @trusted @nogc 442 if (!isAssociativeArray!T) { 443 operator = "contain value"; 444 bool found = false; 445 foreach(value ; context) { 446 if (value == other) { 447 found = true; 448 break; 449 } 450 } 451 this.ok(found, other, file, line); 452 return other; 453 } 454 455 /// ditto 456 U include(U)(U other, string file = __FILE__, size_t line = __LINE__) @trusted 457 if (isAssociativeArray!T) { 458 auto pool = context.values; 459 operator = "contain value"; 460 this.ok(canFind(pool, other), other, file, line); 461 return other; 462 } 463 } else { 464 465 /** 466 * Asserts for an input range wrapped around an Assertion to contain/include a value. 467 * 468 * Examples: 469 * ``` 470 * [1, 2, 3, 4].should.include(3); 471 * "something".should.not.include('o'); 472 * "something".should.include("th"); 473 * ``` 474 */ 475 U include(U)(U other, string file = __FILE__, size_t line = __LINE__) @trusted { 476 static if (isAssociativeArray!T) { 477 auto pool = context.values; 478 } else { 479 auto pool = context; 480 } 481 operator = "contain value"; 482 this.ok(canFind(pool, other), other, file, line); 483 return other; 484 } 485 } 486 487 ///ditto 488 alias value = include; 489 ///ditto 490 alias contain = include; 491 } 492 493 static if (hasLength!T || hasMember!(T, "string") || isSomeString!T) { 494 /** 495 * Asserts for the .length property or function value to equal some value. 496 * 497 * Examples: 498 * ``` 499 * [1, 2, 3, 4].should.have.length(4); 500 * "abcdefg".should.have.length(0); 501 * // ^^ - Throws an Exception "expected 'abcdefg' to have length of 0" 502 * ``` 503 */ 504 U length(U)(U len, string file = __FILE__, size_t line = __LINE__) @trusted { 505 operator = "have length of"; 506 this.ok(context.length == len, len, file, line); 507 return len; 508 } 509 510 /** 511 * Asserts for the .length property or function value to be equal to 0. 512 * 513 * Examples: 514 * ``` 515 * [].should.be.empty; 516 * "".should.be.empty; 517 * ``` 518 */ 519 bool empty(string file = __FILE__, size_t line = __LINE__) @trusted { 520 operator = "is empty"; 521 return this.ok(context.length == 0, file, line); 522 } 523 } 524 525 import std.regex : Regex, isRegexFor; 526 import std.traits : isSomeString; 527 528 static if (isSomeString!T) { 529 530 /** 531 * Asserts for a string wrapped around the Assertion to match a regular expression. 532 * 533 * Examples: 534 * ``` 535 * "something weird".should.match(`[a-z]+`); 536 * "something weird".should.match(regex(`[a-z]+`)); 537 * "something 2 weird".should.not.match(ctRegex!(`^[a-z]+$`)); 538 * "1234numbers".should.match(`[0-9]+[a-z]+`); 539 * "1234numbers".should.not.match(`^[a-z]+`); 540 * ``` 541 */ 542 auto match(RegEx)(RegEx re, string file = __FILE__, size_t line = __LINE__) @safe 543 if (isSomeString!T && isRegexFor!(RegEx, T)) { 544 import std.regex : match; 545 546 auto m = match(context, re); 547 operator = "match"; 548 this.ok(!m.empty, re, file, line); 549 return m; 550 } 551 552 ///ditto 553 auto match(U)(U re, string file = __FILE__, size_t line = __LINE__) @safe 554 if (isSomeString!T && isSomeString!U) { 555 import std.regex : regex, match; 556 557 auto m = match(context, regex(re)); 558 operator = "match"; 559 this.ok(!m.empty, re, file, line); 560 return m; 561 } 562 } 563 564 static if (is(T == bool)) { 565 /** 566 * Asserts for a boolean value to be equal to true. 567 * 568 * Examples: 569 * ``` 570 * true.should.be.True; 571 * (1 == 1).should.be.True; 572 * ``` 573 */ 574 bool True(string file = __FILE__, size_t line = __LINE__) @safe { 575 return this.ok(context == true, true, file, line); 576 } 577 578 /** 579 * Asserts for a boolean value to be equal to true. 580 * 581 * Examples: 582 * ``` 583 * false.should.be.False; 584 * (1 != 1).should.be.False; 585 * ``` 586 */ 587 bool False(string file = __FILE__, size_t line = __LINE__) @safe { 588 return !this.ok(context == false, false, file, line); 589 } 590 } 591 592 static if (isCallable!T) { 593 /** 594 * Asserts whether a callable object wrapped around the assertion throws an exception of type T. 595 * 596 * Examples: 597 * ``` 598 * void throwing() 599 * { 600 * throw new Exception("I throw with 0!"); 601 * } 602 603 * should(&throwing).Throw!Exception; 604 605 * void notThrowing() 606 * { 607 * return; 608 * } 609 * 610 * should(¬Throwing).not.Throw; 611 * ``` 612 */ 613 void Throw(T : Throwable = Exception)(string file = __FILE__, size_t line = __LINE__) @trusted { 614 operator = "throw"; 615 bool thrown = false; 616 try { 617 this.context(); 618 } catch (T) { 619 thrown = true; 620 } 621 this.ok(thrown, file, line); 622 } 623 } 624 625 } 626 627 @("Should(v)") 628 @safe unittest { 629 // it("returns an Assertion", { 630 assert(is(typeof(10.should) == Assertion!int)); 631 assert(is(typeof(10f.expect) == Assertion!float)); 632 } 633 634 @("Should Assertion") 635 @safe unittest { 636 // it("can be instantiated for ranges of structs without `opCmp`", { 637 struct Test { 638 int a; 639 int b; 640 } 641 642 cast(void)[Test(2, 3)].should; 643 } 644 645 version (MirNoGCException) { 646 @("Should Assertion.message") 647 unittest { 648 // it("returns the correct message for binary operators", 649 { 650 auto a = Assertion!int(10); 651 a.operator = "equal"; 652 assert(a.message(20).data == "expected [int] 10 to equal [int] 20"); 653 654 auto a2 = Assertion!size_t(23); 655 a2.operator = "be greater that"; 656 assert(a2.be.message(20).data == "expected [ulong] 23 to be greater that [int] 20"); 657 } 658 659 // it("returns the correct message for unary operators", 660 { 661 auto a = Assertion!string("function"); 662 a.operator = "throw"; 663 assert(a.message.data == "expected [string] function to throw"); 664 665 auto a2 = Assertion!int(23); 666 a2.operator = "exist"; 667 assert(a2.message.data == "expected [int] 23 to exist"); 668 669 struct S { 670 int x; 671 672 /* 673 void toString(C = char, W)(scope ref W w) scope const 674 { 675 w.put("#C#"); 676 } 677 */ 678 } 679 680 auto a3 = Assertion!(S*)(null); 681 a3.operator = "exist"; 682 assert(a3.message.data == "expected [S*] null to exist"); 683 } 684 685 // it("returns the correct message for negated operators", 686 { 687 auto a = Assertion!int(10); 688 a.operator = "be"; 689 assert(a.not.message(false).data == "expected [int] 10 to not be [bool] false"); 690 } 691 } 692 } else { 693 @("Should Assertion.message") 694 unittest { 695 // it("returns the correct message for binary operators", 696 { 697 auto a = Assertion!int(10); 698 a.operator = "equal"; 699 assert(a.message(20) == "expected [int] 10 to equal [int] 20"); 700 701 auto a2 = Assertion!size_t(23); 702 a2.operator = "be greater that"; 703 assert(a2.be.message(20) == "expected [ulong] 23 to be greater that [int] 20"); 704 705 } 706 707 // it("returns the correct message for unary operators", 708 { 709 auto a = Assertion!string("function"); 710 a.operator = "throw"; 711 assert(a.message == "expected [string] function to throw"); 712 713 auto a2 = Assertion!int(23); 714 a2.operator = "exist"; 715 assert(a2.message == "expected [int] 23 to exist"); 716 717 class C { 718 int x; 719 } 720 721 C cl = null; 722 auto a3 = Assertion!C(cl); 723 a3.operator = "exist"; 724 assert(a3.message == "expected [C] null to exist"); 725 726 auto a4 = Assertion!(C*)(null); 727 a4.operator = "exist"; 728 assert(a4.be.message == "expected [C*] null to exist"); 729 } 730 731 // it("returns the correct message for negated operators", 732 { 733 auto a = Assertion!int(10); 734 a.operator = "be"; 735 assert(a.not.message(false) == "expected [int] 10 to not be [bool] false"); 736 } 737 } 738 }