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