1 module specd.matchers; 2 3 import specd.specd; 4 5 import std.math; 6 import std.range; 7 import std.traits; 8 import std.stdio; 9 import std.conv, std.string; 10 11 version(SpecDTests) unittest { 12 bool evaluated = false; 13 int oneTimeCalculation() { 14 evaluated = true; 15 return 1; 16 } 17 18 describe("the must function") 19 .should("wrap a lazy expression in a Match without evaluating it", (when) { 20 auto m = oneTimeCalculation().must(); 21 assert(evaluated == false); 22 assert(m.match() == 1); 23 assert(evaluated == true); 24 }) 25 ; 26 27 } 28 29 version(SpecDTests) unittest { 30 import std.array; 31 import std.format; 32 auto a = 1; 33 const(int) f() { return a; } 34 describe("const(T)") 35 .should("work with matchers", (_) { f().must.equal(a); }); 36 37 string fmt(double d) { 38 auto w = appender!string(); 39 formattedWrite(w, "%e", d); 40 return w.data; 41 } 42 43 double x = 1.0; 44 double y = 1.0; 45 double toleranceWeak = 0.00001; 46 double toleranceStrict = 0.0000001; 47 double z = 1.0 + 0.000001; 48 describe(text("x and y (", fmt(x), ",", fmt(y), ")")) 49 .should("be approxEqual since they are equal", (_) { 50 x.must.be.approxEqual(y, 0, 0); 51 x.must.approxEqual(y, 0, 0); 52 }); 53 54 describe(text("x and z (", fmt(x), ",", fmt(z), ")")) 55 .should("be approxEqual", (_) { 56 x.must.be.approxEqual(z, toleranceWeak, toleranceWeak); 57 x.must.approxEqual(z, toleranceWeak, toleranceWeak); 58 }); 59 60 describe(text("at strict threshold x and z (", fmt(x), ",", fmt(z), ")")) 61 .should("*not* be approxEqual", (_) { 62 x.must.not.be.approxEqual(z, toleranceStrict, toleranceStrict); 63 x.must.not.approxEqual(z, toleranceStrict, toleranceStrict); 64 }); 65 66 describe("[x, y, z]") 67 .should("be approxEqual [z, y, x]", (_) { 68 auto first = [x, y, z]; 69 auto second = [z, y, x]; 70 first.must.be.approxEqual(second, toleranceWeak, toleranceWeak); 71 }); 72 73 describe("at strict threshold [x, y, z]") 74 .should("*not* be approxEqual [z, y, x]", (_) { 75 [x, y, z].must.not.be 76 .approxEqual([z, y, x], toleranceStrict, toleranceStrict); 77 }); 78 79 describe("[x, x, x]") 80 .should("*not* be approxEqual [x, x]", (_) { 81 [x, x, x].must.not.be 82 .approxEqual([x, x], toleranceWeak, toleranceWeak); 83 }); 84 } 85 86 version(SpecDTests) unittest { 87 88 describe("equal matching").should([ 89 "work on string": { 90 "foo".must.equal("foo"); 91 }, 92 "work on int": { 93 2.must.equal(2); 94 }, 95 "work on double": { 96 1.3.must.equal(1.3); 97 }, 98 "work on object": { 99 auto a = new Object; 100 a.must.equal(a); 101 }, 102 "throw a MatchException if it doesn't match": { 103 try { 104 1.must.equal(2); 105 assert(false, "Expected a MatchException"); 106 } catch (MatchException e) { 107 } 108 }, 109 "invert matching with not": { 110 2.must.not.equal(1); 111 } 112 ]); 113 114 describe("sameAs matching").should([ 115 "work on slices": { 116 auto a = [1,2,3]; 117 auto b = a; 118 b.must.be.sameAs(a); 119 }, 120 "must fail on duplicate arrays": { 121 auto a = [1,2,3]; 122 auto b = a.dup; 123 b.must.not.be.sameAs(a); 124 b.must.equal(a); 125 }, 126 "work on objects": { 127 auto a = new Object(); 128 auto b = a; 129 b.must.be.sameAs(a); 130 } 131 ]); 132 133 134 135 describe("between matching").should([ 136 "match a range": { 137 1.must.be.between(1,3); 138 2.must.be.between(1,3); 139 3.must.be.between(1,3); 140 4.must.not.be.between(1,3); 141 0.must.not.be.between(1,3); 142 } 143 ]); 144 145 describe("contain matching").should([ 146 "match partial strings": { 147 "frobozz".must.contain("oboz"); 148 "frobozz".must.not.contain("bracken"); 149 } 150 ]); 151 152 describe("boolean matching").should([ 153 "match on True": { 154 true.must.be.True; 155 }, 156 "match on False": { 157 false.must.be.False; 158 }, 159 "match using be(true)": { 160 true.must.be(true); 161 } 162 ]); 163 164 describe("null matching").should([ 165 "match on Null": { 166 Object a = null; 167 Object b = new Object(); 168 a.must.be.Null; 169 b.must.not.be.Null; 170 } 171 ]); 172 173 describe("opEquals matching").should([ 174 "match == for basic types": { 175 1.must == 1; 176 1.must.not == 2; 177 }, 178 "match == for objects": { 179 Object a = new Object(); 180 Object b = new Object(); 181 a.must == a; 182 a.must.not == b; 183 } 184 ]); 185 186 187 describe("comparison matching").should([ 188 "match greater_than": { 189 1.must.be.greater_than(0); 190 1.must.not.be.greater_than(1); 191 1.must.be_!">" (0); 192 }, 193 "match greater_than_or_equal_to": { 194 1.must.be.greater_than_or_equal_to(1); 195 1.must.not.be.greater_than_or_equal_to(2); 196 }, 197 "match less_than": { 198 1.must.be.less_than(2); 199 1.must.not.be.less_than(1); 200 }, 201 "match less_than_or_equal_to": { 202 1.must.be.less_than_or_equal_to(1); 203 1.must.not.be.less_than_or_equal_to(0); 204 } 205 ]); 206 207 class TestException : Exception { 208 this(string s) { 209 super(s); 210 } 211 212 } 213 214 int throwAnException() { 215 throw new TestException("foo"); 216 } 217 218 void throwAnExceptionAndReturnVoid() { 219 throw new TestException("bar"); 220 } 221 222 describe("Exception matching").should([ 223 "match when an exception is thrown": { 224 throwAnException().must.throw_!TestException; 225 1.must.not.throw_!TestException; 226 }, 227 "work with void function calls": { 228 calling(throwAnExceptionAndReturnVoid()).must.throw_!TestException; 229 } 230 ]); 231 } 232 233 private struct Calling {}; 234 235 Match!Calling calling(lazy void m, string file = __FILE__, size_t line = __LINE__) { 236 return new Match!Calling({ m(); return Calling(); }, file, line); 237 } 238 239 auto must(T)(lazy T m, string file = __FILE__, size_t line = __LINE__) { 240 static if (is(T : Match!Calling)) { 241 return m(); // Allow the form calling(foo()).must.throw.... 242 } else { 243 return new Match!T({ return m(); }, file, line); 244 } 245 } 246 247 class Match(T) { 248 T delegate() match; 249 // TODO I use this for comparing a generic type with the type of this Match. 250 // Might be a better way to do that. 251 T dummyMatch; 252 // Signal that the match is positive, ie it has not been negated with "not" in the chain 253 // (or if it has, it has been negated again by a second "not") 254 bool isPositiveMatch = true; 255 string file; 256 size_t line; 257 258 this(T delegate() match, string file, size_t line) { 259 this.match = match; 260 this.file = file; 261 this.line = line; 262 this.dummyMatch = T.init; 263 } 264 265 // Negated match 266 267 auto not() { 268 isPositiveMatch = !isPositiveMatch; 269 return this; 270 } 271 272 // Sugar 273 auto be() { 274 return this; 275 } 276 277 // Help for matching booleans 278 static if (is(T == bool)) { 279 void be(bool expected) { 280 if (expected) 281 True(this); 282 else 283 False(this); 284 } 285 } 286 287 bool opEquals(T rhs) { 288 equal(this, rhs); 289 return true; 290 } 291 292 static if (isFloatingPoint!T) { 293 bool approxEqual(T rhs, T maxRelDiff = 1e-2, T maxAbsDiff = 1e-5) { 294 .approxEqual(this, rhs, maxRelDiff, maxAbsDiff); 295 return true; 296 } 297 } else static if(isInputRange!T && isFloatingPoint!(ElementType!T)) { 298 bool approxEqual(T rhs, ElementType!T maxRelDiff = 1e-2, ElementType!T maxAbsDiff = 1e-5) { 299 .approxEqual(this, rhs, maxRelDiff, maxAbsDiff); 300 return true; 301 } 302 } 303 304 305 alias Object.opEquals opEquals; 306 307 void throwMatchException(string reason) { 308 throw new MatchException(reason, file, line); 309 } 310 } 311 312 313 class MatchException : Exception { 314 this(string s, string file = __FILE__, size_t line = __LINE__) { 315 super(s, file, line); 316 } 317 } 318 319 320 void equal(T, T1)(Match!T matcher, T1 expected) 321 if (is(typeof(expected == matcher.dummyMatch) == bool)) 322 { 323 auto match = matcher.match(); 324 if ((expected == match) != matcher.isPositiveMatch) 325 matcher.throwMatchException("Expected " ~ 326 (matcher.isPositiveMatch ? "" : "not ") ~ 327 "<" ~ text(expected) ~ "> but got <" ~ 328 text(match) ~ ">"); 329 } 330 331 332 333 void sameAs(T, T1)(Match!T matcher, T1 expected) 334 if (is(typeof(expected == matcher.dummyMatch) == bool)) 335 { 336 auto match = matcher.match(); 337 if ((expected is match) != matcher.isPositiveMatch) 338 matcher.throwMatchException("Expected " ~ 339 (matcher.isPositiveMatch ? "" : "not ") ~ 340 "<" ~ text(expected) ~ "> but got <" ~ 341 text(match) ~ ">"); 342 } 343 344 345 346 void approxEqual(T, T1, V)(Match!T matcher, T1 expected, V maxRelDiff, V maxAbsDiff) 347 if (is(typeof(expected == matcher.dummyMatch) == bool)) 348 { 349 auto match = matcher.match(); 350 if ((std.math.approxEqual(expected, match, maxRelDiff, maxAbsDiff)) != matcher.isPositiveMatch) 351 matcher.throwMatchException("Expected Approx " ~ 352 (matcher.isPositiveMatch ? "" : "not ") ~ 353 "<" ~ text(expected) ~ "> but got <" ~ 354 text(match) ~ ">"); 355 } 356 357 private string comparisonInWords(string op)() { 358 if (op == "<") 359 return "less than"; 360 else if (op == "<=") 361 return "less than or equal to"; 362 else if (op == ">") 363 return "greater than"; 364 else if (op == ">=") 365 return "greater than or equal to"; 366 else 367 return "*unknown operation*"; 368 } 369 370 private void comparison(string op, T, T1)(Match!T matcher, T1 expected) { 371 auto match = matcher.match(); 372 auto cmp = mixin("match " ~ op ~ " expected"); 373 if (cmp != matcher.isPositiveMatch) 374 matcher.throwMatchException("Expected something " ~ 375 (matcher.isPositiveMatch ? "" : "not ") ~ 376 comparisonInWords!op ~ 377 " <" ~ text(expected) ~ "> but got <" ~ 378 text(match) ~ ">"); 379 380 } 381 382 void greater_than(T, T1)(Match!T matcher, T1 expected) 383 if (is(typeof(matcher.dummyMatch > expected) == bool)) 384 { 385 comparison!">"(matcher, expected); 386 } 387 388 void greater_than_or_equal_to(T, T1)(Match!T matcher, T1 expected) 389 if (is(typeof(matcher.dummyMatch >= expected) == bool)) 390 { 391 comparison!">="(matcher, expected); 392 } 393 394 void less_than(T, T1)(Match!T matcher, T1 expected) 395 if (is(typeof(matcher.dummyMatch < expected) == bool)) 396 { 397 comparison!"<"(matcher, expected); 398 } 399 400 void less_than_or_equal_to(T, T1)(Match!T matcher, T1 expected) 401 if (is(typeof(matcher.dummyMatch <= expected) == bool)) 402 { 403 comparison!"<="(matcher, expected); 404 } 405 406 void be_(string op, T, T1)(Match!T matcher, T1 expected) 407 if (is(typeof(matcher.dummyMatch > expected) == bool) && 408 (op == ">" || op == ">=" || op == "<" || op == "<=")) 409 { 410 comparison!op(matcher, expected); 411 } 412 413 void between(T, T1)(Match!T matcher, T1 first, T1 last) 414 if (is(typeof(matcher.dummyMatch >= first) == bool)) 415 { 416 auto match = matcher.match(); 417 bool inrange = (match >= first && match <= last); 418 if (inrange != matcher.isPositiveMatch) 419 matcher.throwMatchException("Expected something " ~ 420 (matcher.isPositiveMatch ? "" : "not ") ~ 421 "between <" ~ text(first) ~ "> and <" ~ text(last) ~ "> but got <" ~ text(match) ~ ">"); 422 } 423 void contain(T, T1)(Match!T matcher, T1 fragment) 424 if (is(typeof(indexOf(matcher.dummyMatch, fragment) != -1) == bool)) 425 { 426 auto match = matcher.match(); 427 bool contains = indexOf(match, fragment) != -1; 428 if (contains != matcher.isPositiveMatch) 429 matcher.throwMatchException("Expected <" ~ text(match) ~ "> to " ~ 430 (matcher.isPositiveMatch ? "" : "not ") ~ 431 "contain <" ~ text(fragment) ~ ">"); 432 } 433 434 void True(Match!bool matcher) { 435 auto match = matcher.match(); 436 if (match != matcher.isPositiveMatch) 437 matcher.throwMatchException("Expected <" ~ 438 (matcher.isPositiveMatch ? "true" : "false") ~ 439 "> but got <" ~ 440 text(match) ~ ">"); 441 } 442 443 void False(Match!bool matcher) { 444 auto match = matcher.match(); 445 if (match == matcher.isPositiveMatch) 446 matcher.throwMatchException("Expected <" ~ 447 (matcher.isPositiveMatch ? "false" : "true") ~ 448 "> but got <" ~ 449 text(match) ~ ">"); 450 } 451 452 void Null(T)(Match!T matcher) 453 if (is(typeof(matcher.dummyMatch is null) == bool)) 454 { 455 auto match = matcher.match(); 456 bool isNull = match is null; 457 if (isNull != matcher.isPositiveMatch) 458 matcher.throwMatchException("Expected " ~ 459 (matcher.isPositiveMatch ? "" : "not ") ~ 460 "<null> but got <" ~ 461 text(match) ~ ">"); 462 463 } 464 465 void throw_(E, T)(Match!T matcher) 466 if (is(E : Throwable)) 467 { 468 string exception = ""; 469 try { 470 matcher.match(); 471 if (matcher.isPositiveMatch) 472 exception = "Expected " ~ 473 E.stringof ~ " thrown, but nothing was thrown"; 474 475 } catch (E e) { 476 if (!matcher.isPositiveMatch) 477 exception = "Expected no " ~ 478 E.stringof ~ " thrown, but got " ~ typeof(e).stringof; 479 } 480 481 if (exception != "") 482 matcher.throwMatchException(exception); 483 }