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