1 module dshould.basic; 2 3 import std.format : format; 4 import std.range : isInputRange; 5 import std.string : empty; 6 import std.typecons : Nullable; 7 import dshould.ShouldType; 8 public import dshould.ShouldType : should; 9 10 /** 11 * The word `.not` negates the current phrase. 12 */ 13 public auto not(Should)(Should should) pure 14 if (isInstanceOf!(ShouldType, Should)) 15 { 16 should.allowOnlyWords!().before!"not"; 17 18 return should.addWord!"not"; 19 } 20 21 /** 22 * The word `.be()` is a placeholder for `.equal`. 23 * 24 * Note: Reference comparison is achieved with `.be.same.as()`. 25 */ 26 public void be(Should, T)(Should should, T expected, Fence _ = Fence(), string file = __FILE__, size_t line = __LINE__) 27 if (isInstanceOf!(ShouldType, Should) && !should.hasWord!"approximately" && !is(T == typeof(null))) 28 { 29 return equal(should, expected, Fence(), file, line); 30 } 31 32 /** 33 * The phrase `be.same.as()` compares two values by reference. 34 */ 35 public auto same(Should)(Should should) pure 36 if (isInstanceOf!(ShouldType, Should) && should.hasWord!"be") 37 { 38 should.allowOnlyWords!("not", "be").before!"same"; 39 40 return should.addWord!"same"; 41 } 42 43 /** 44 * The phrase `be(null)` is equivalent to `be.same.as(null)`. 45 */ 46 public void be(Should)( 47 Should should, typeof(null) expected, Fence _ = Fence(), string file = __FILE__, size_t line = __LINE__) 48 if (isInstanceOf!(ShouldType, Should) && !should.hasWord!"approximately") 49 { 50 should.allowOnlyWords!("not").before!"be"; 51 52 should.be.same.as(null, Fence(), file, line); 53 } 54 55 /// 56 public auto as(Should, T)( 57 Should should, T expected, 58 Fence _ = Fence(), string file = __FILE__, size_t line = __LINE__) 59 if (isInstanceOf!(ShouldType, Should) && should.hasWord!"same") 60 { 61 return dshould.basic.compareReference(should, expected, Fence(), file, line); 62 } 63 64 // 65 unittest 66 { 67 auto array1 = [5], array2 = array1.dup; 68 69 array1.should.be(array1); 70 array1.should.be(array2); 71 72 array1.should.be.same.as(array1); 73 array1.should.not.be.same.as(array2); 74 75 (new Object).should.not.be(new Object); 76 } 77 78 private void compareReference(Should, T)( 79 Should should, T expected, Fence _ = Fence(), string file = __FILE__, size_t line = __LINE__) 80 if (isInstanceOf!(ShouldType, Should)) 81 { 82 with (should) 83 { 84 enum isNullType = is(T == typeof(null)); 85 86 auto got = should.got(); 87 88 static if (hasWord!"not") 89 { 90 static if (isNullType) 91 { 92 check(got !is null, "non-null", "null", file, line); 93 } 94 else 95 { 96 check( 97 got !is expected, 98 format("different reference than %s", expected.quote), 99 "same reference", 100 file, line 101 ); 102 } 103 } 104 else 105 { 106 static if (isNullType) 107 { 108 check(got is null, "null", format("%s", got.quote), file, line); 109 } 110 else 111 { 112 check( 113 got is expected, 114 format("same reference as %s", expected.quote), 115 format("%s", got.quote), 116 file, line 117 ); 118 } 119 } 120 } 121 } 122 123 unittest 124 { 125 const(string)[][] a = [["a"]]; 126 string[][] b = [["a"]]; 127 128 a.should.equal(b); 129 b.should.equal(a); 130 } 131 132 /** 133 * `Nullable!int().should.beNull` tests that the `Nullable` is null. 134 * `should.not.beNull` tests the reverse. 135 */ 136 public void beNull(Should)(Should should, string file = __FILE__, size_t line = __LINE__) 137 if (isInstanceOf!(ShouldType, Should)) 138 { 139 import std.format : format; 140 141 should.allowOnlyWords!("not") 142 .before!"beNull"; 143 144 auto got = should.got(); 145 146 static if (__traits(compiles, got is null)) 147 { 148 compareReference(should, null, Fence(), file, line); 149 return; 150 } 151 else 152 { 153 static assert(is(typeof(got) : Nullable!T, T)); 154 static if (Should.hasWord!"not") 155 { 156 should.check(!got.isNull, "non-null Nullable", format!"%s"(got), file, line); 157 } 158 else 159 { 160 should.check(got.isNull, "Nullable.null", format!"%s"(got), file, line); 161 } 162 } 163 } 164 165 /// 166 unittest 167 { 168 Nullable!int().should.beNull; 169 Nullable!int(5).should.not.beNull; 170 } 171 172 /// 173 unittest 174 { 175 Object.init.should.beNull; 176 } 177 178 /** 179 * When called without parameters, `.be` is a filler word for `.greater`, `.less` or `.equal`. 180 */ 181 public auto be(Should)(Should should) pure 182 if (isInstanceOf!(ShouldType, Should)) 183 { 184 should.allowOnlyWords!("not").before!"be"; 185 186 return should.addWord!"be"; 187 } 188 189 /** 190 * The word `.equal` tests for equality. 191 * It takes one parameter and terminates the phrase. 192 * Its parameter is the expected value for the left-hand side of the should phrase. 193 */ 194 public void equal(Should, T)(Should should, T expected, Fence _ = Fence(), string file = __FILE__, size_t line = __LINE__) 195 if (isInstanceOf!(ShouldType, Should) && !should.hasWord!"approximately") 196 { 197 should.equal.numericCheck(expected, file, line); 198 } 199 200 /// 201 pure @safe unittest 202 { 203 5.should.equal(5); 204 5.should.not.equal(6); 205 } 206 207 /// 208 unittest 209 { 210 (new Object).should.not.equal(new Object); 211 } 212 213 /// 214 unittest 215 { 216 auto obj = new Object; 217 218 obj.should.equal(obj); 219 obj.should.be(obj); 220 } 221 222 /// 223 unittest 224 { 225 class SameyClass 226 { 227 override bool opEquals(Object o) { return true; } 228 } 229 230 (new SameyClass).should.be(new SameyClass); 231 (new SameyClass).should.equal(new SameyClass); 232 (new SameyClass).should.not.be.same.as(new SameyClass); 233 } 234 235 /// 236 unittest 237 { 238 const string[int] hashmap1 = [1: "2", 5: "4"]; 239 string[int] hashmap2 = null; 240 241 hashmap2[5] = "4"; 242 hashmap2[1] = "2"; 243 244 // TODO reliable way to produce a hash collision 245 // assert(hashmap1.keys != hashmap2.keys, "hash collision not found"); // make sure that ordering doesn't matter 246 247 hashmap1.should.equal(hashmap2); 248 hashmap2.should.equal(hashmap1); 249 } 250 251 /// 252 unittest 253 { 254 import std.range : only; 255 256 5.only.should.equal([5]); 257 [5].should.equal(5.only); 258 5.only.should.not.equal(6.only); 259 5.only.should.not.equal([6]); 260 } 261 262 /** 263 * When called without parameters, `.equal` must be terminated by `.greater` or `.less`. 264 * .should.be.equal.greater(...) is equivalent to .should.be.greater.equal(...) 265 * is equivalent to assert(got >= expected). 266 */ 267 public auto equal(Should)(Should should) 268 if (isInstanceOf!(ShouldType, Should)) 269 { 270 should.allowOnlyWords!("not", "be", "greater", "less").before!"equal"; 271 272 return should.addWord!"equal"; 273 } 274 275 /// 276 pure @safe unittest 277 { 278 5.should.not.be.greater.equal(6); 279 5.should.be.greater.equal(5); 280 5.should.be.greater.equal(4); 281 5.should.not.be.greater.equal(6); 282 } 283 284 /** 285 * The word `.greater` tests that the left-hand side is greater than the expected value. 286 * It takes one parameter and terminates the phrase. 287 */ 288 public void greater(Should, T)(Should should, T expected, Fence _ = Fence(), string file = __FILE__, size_t line = __LINE__) 289 if (isInstanceOf!(ShouldType, Should)) 290 { 291 should.greater.numericCheck(expected, file, line); 292 } 293 294 /// 295 pure @safe unittest 296 { 297 5.should.not.be.greater(6); 298 5.should.not.be.greater(5); 299 5.should.be.greater(4); 300 } 301 302 /** 303 * When called without parameters, `.greater` must be terminated by `.equal`, indicating `>=`. 304 */ 305 public auto greater(Should)(Should should) 306 if (isInstanceOf!(ShouldType, Should)) 307 { 308 should.allowOnlyWords!("not", "be", "equal").before!"greater"; 309 should.requireWord!"be".before!"greater"; 310 311 return should.addWord!"greater"; 312 } 313 314 /** 315 * The word `.less` tests that the left-hand side is less than the expected value. 316 * It takes one parameter and terminates the phrase. 317 */ 318 public void less(Should, T)(Should should, T expected, Fence _ = Fence(), string file = __FILE__, size_t line = __LINE__) 319 if (isInstanceOf!(ShouldType, Should)) 320 { 321 should.less.numericCheck(expected, file, line); 322 } 323 324 /// 325 pure @safe unittest 326 { 327 5.should.be.less(6); 328 } 329 330 /** 331 * When called without parameters, `.less` must be terminated by `.equal`, indicating `<=`. 332 */ 333 public auto less(Should)(Should should) 334 if (isInstanceOf!(ShouldType, Should)) 335 { 336 should.allowOnlyWords!("not", "be", "equal").before!"less"; 337 should.requireWord!"be".before!"less"; 338 339 return should.addWord!"less"; 340 } 341 342 // const version 343 private void numericCheck(Should, T)(Should should, const T expected, string file, size_t line) 344 if (isInstanceOf!(ShouldType, Should) 345 && __traits(compiles, 346 (const Should should, const T expected) => 347 mixin(format!(numericComparison!(Should, T).checkString)("should.got()", "expected")))) 348 { 349 with (should) 350 { 351 const got = should.got(); 352 353 alias enums = numericComparison!(Should, T); 354 355 check( 356 mixin(format!(enums.checkString)("got", "expected")), 357 format(enums.message, expected.quote), 358 got.quote, 359 file, line 360 ); 361 } 362 } 363 364 // nonconst version, for badwrong types that need nonconst opCmp 365 private void numericCheck(Should, T)(Should should, T expected, string file, size_t line) 366 if (isInstanceOf!(ShouldType, Should) 367 && !__traits(compiles, 368 (const Should should, const T expected) => 369 mixin(format!(numericComparison!(Should, T).checkString)("should.got()", "expected"))) 370 && __traits(compiles, 371 (Should should, T expected) => 372 mixin(format!(numericComparison!(Should, T).checkString)("should.got()", "expected")))) 373 { 374 with (should) 375 { 376 auto got = should.got(); 377 378 alias enums = numericComparison!(Should, T); 379 380 check( 381 mixin(format!(enums.checkString)("got", "expected")), 382 format(enums.message, expected.quote), 383 got.quote, 384 file, line 385 ); 386 } 387 } 388 389 // range version 390 private void numericCheck(Should, T)(Should should, T expected, string file, size_t line) 391 if (isInstanceOf!(ShouldType, Should) 392 && numericComparison!(Should, T).combined == "==" 393 && !__traits(compiles, should.got() == expected) 394 && !__traits(compiles, cast(const) should.got() == cast(const) expected) 395 && isInputRange!(typeof(should.got())) && isInputRange!T) 396 { 397 import std.range : array; 398 399 with (should) 400 { 401 auto got = should.got(); 402 403 alias enums = numericComparison!(Should, T); 404 405 check( 406 mixin(format!(enums.checkString)("got.array", "expected.array")), 407 format(enums.message, expected.quote), 408 got.quote, 409 file, line 410 ); 411 } 412 } 413 414 private template numericComparison(Should, T) 415 { 416 enum equalPart = Should.hasWord!"equal" ? "==" : ""; 417 enum equalPartShort = Should.hasWord!"equal" ? "=" : ""; 418 enum lessPart = Should.hasWord!"less" ? "<" : ""; 419 enum greaterPart = Should.hasWord!"greater" ? ">" : ""; 420 421 enum comparison = lessPart ~ greaterPart; 422 423 enum combined = comparison ~ (comparison.empty ? equalPart : equalPartShort); 424 425 static if (Should.hasWord!"not") 426 { 427 enum checkString = "!(%s " ~ combined ~ " %s)"; 428 enum message = "value not " ~ combined ~ " %s"; 429 } 430 else static if (combined == "==") 431 { 432 enum checkString = "%s == %s"; 433 enum message = "%s"; 434 } 435 else 436 { 437 enum checkString = "%s " ~ combined ~ " %s"; 438 enum message = "value " ~ combined ~ " %s"; 439 } 440 } 441 442 /** 443 * This could be in a separate file, say approx.d, 444 * if doing so didn't crash dmd. 445 * see https://issues.dlang.org/show_bug.cgi?id=18839 446 */ 447 448 package struct ErrorValue 449 { 450 @disable this(); 451 452 private this(double value) pure @safe 453 { 454 this.value = value; 455 } 456 457 double value; 458 } 459 460 public auto error(double value) pure @safe 461 { 462 return ErrorValue(value); 463 } 464 465 /** 466 * `.approximately` is a word indicating an approximate value comparison. 467 * When using .approximately, only the words `.be` and `.equal` may be used, though they may appear before or after. 468 * Each must be called with an additional parameter, `error = <float>`, indicating the amount of permissible error. 469 */ 470 public auto approximately(Should)( 471 Should should, double expected, ErrorValue error, 472 Fence _ = Fence(), string file = __FILE__, size_t line = __LINE__ 473 ) 474 if (isInstanceOf!(ShouldType, Should)) 475 { 476 static assert( 477 should.hasWord!"be" || should.hasWord!"equal", 478 `bad grammar: expected "be" or "equal" before "approximately"` 479 ); 480 481 should.allowOnlyWords!("be", "equal", "not").before!"approximately"; 482 483 return should 484 .addWord!"approximately" 485 .approximateCheck(expected, error, file, line); 486 } 487 488 /// 489 unittest 490 { 491 5.should.be.approximately(5.1, error = 0.11); 492 5.should.approximately.be(5.1, error = 0.11); 493 0.should.approximately.equal(1.0, error = 1.1); 494 0.should.approximately.equal(-1.0, error = 1.1); 495 0.should.not.approximately.equal(1, error = 0.1); 496 42.3.should.be.approximately(42.3, error = 1e-3); 497 } 498 499 unittest 500 { 501 class A 502 { 503 public override int opCmp(Object rhs) const @nogc pure nothrow @safe 504 { 505 return 0; 506 } 507 } 508 509 auto a = new A; 510 511 a.should.not.be.greater(a); 512 } 513 514 public void be(Should, T)(Should should, T expected, ErrorValue error, Fence _ = Fence(), string file = __FILE__, size_t line = __LINE__) 515 if (isInstanceOf!(ShouldType, Should) && should.hasWord!"approximately") 516 { 517 should.allowOnlyWords!("approximately", "not").before!"equal"; 518 519 return should.approximateCheck(expected, error, file, line); 520 } 521 522 public void equal(Should, T)(Should should, T expected, ErrorValue error, Fence _ = Fence(), string file = __FILE__, size_t line = __LINE__) 523 if (isInstanceOf!(ShouldType, Should) && should.hasWord!"approximately") 524 { 525 should.allowOnlyWords!("approximately", "not").before!"be"; 526 527 return should.approximateCheck(expected, error, file, line); 528 } 529 530 public auto approximately(Should)(Should should) 531 if (isInstanceOf!(ShouldType, Should)) 532 { 533 return should.addWord!"approximately"; 534 } 535 536 private void approximateCheck(Should, T)(Should should, T expected, ErrorValue error, string file, size_t line) 537 if (isInstanceOf!(ShouldType, Should)) 538 { 539 import std.math : abs; 540 541 with (should) 542 { 543 auto got = should.got(); 544 545 static if (hasWord!"not") 546 { 547 check( 548 abs(expected - got) >= error.value, 549 format("value outside %s ± %s", expected, error.value), 550 format("%s", got), 551 file, line 552 ); 553 } 554 else 555 { 556 check( 557 abs(expected - got) < error.value, 558 format("%s ± %s", expected, error.value), 559 format("%s", got), 560 file, line 561 ); 562 } 563 } 564 } 565 566 package string quote(T)(T t) 567 { 568 import std.typecons : Nullable; 569 570 static if (is(T : Nullable!U, U)) 571 { 572 if (t.isNull) 573 { 574 return "Nullable.null"; 575 } 576 } 577 578 static if (is(T: string)) 579 { 580 return format("'%s'", t); 581 } 582 else 583 { 584 return format("%s", t); 585 } 586 }