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