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