1 module specd.matchers; 2 3 import specd.specd; 4 5 import std.stdio; 6 import std.conv, std.string; 7 8 version(SpecDTests) unittest { 9 bool evaluated = false; 10 int oneTimeCalculation() { 11 evaluated = true; 12 return 1; 13 } 14 15 describe("the must function") 16 .should("wrap a lazy expression in a Match without evaluating it", (when) { 17 auto m = oneTimeCalculation().must(); 18 assert(evaluated == false); 19 assert(m.match() == 1); 20 assert(evaluated == true); 21 }) 22 ; 23 24 } 25 26 version(SpecDTests) unittest { 27 28 describe("equal matching").should([ 29 "work on string": { 30 "foo".must.equal("foo"); 31 }, 32 "work on int": { 33 2.must.equal(2); 34 }, 35 "work on double": { 36 1.3.must.equal(1.3); 37 }, 38 "work on object": { 39 auto a = new Object; 40 a.must.equal(a); 41 }, 42 "throw a MatchException if it doesn't match": { 43 try { 44 1.must.equal(2); 45 assert(false, "Expected a MatchException"); 46 } catch (MatchException e) { 47 } 48 }, 49 "invert matching with not": { 50 2.must.not.equal(1); 51 } 52 ]); 53 54 describe("between matching").should([ 55 "match a range": { 56 1.must.be.between(1,3); 57 2.must.be.between(1,3); 58 3.must.be.between(1,3); 59 4.must.not.be.between(1,3); 60 0.must.not.be.between(1,3); 61 } 62 ]); 63 64 describe("contain matching").should([ 65 "match partial strings": { 66 "frobozz".must.contain("oboz"); 67 "frobozz".must.not.contain("bracken"); 68 } 69 ]); 70 71 describe("boolean matching").should([ 72 "match on True": { 73 true.must.be.True; 74 }, 75 "match on False": { 76 false.must.be.False; 77 }, 78 "match using be(true)": { 79 true.must.be(true); 80 } 81 ]); 82 83 describe("null matching").should([ 84 "match on Null": { 85 Object a = null; 86 Object b = new Object(); 87 a.must.be.Null; 88 b.must.not.be.Null; 89 } 90 ]); 91 92 describe("opEquals matching").should([ 93 "match == for basic types": { 94 1.must == 1; 95 1.must.not == 2; 96 }, 97 "match == for objects": { 98 Object a = new Object(); 99 Object b = new Object(); 100 a.must == a; 101 a.must.not == b; 102 } 103 ]); 104 105 106 describe("comparison matching").should([ 107 "match greater_than": { 108 1.must.be.greater_than(0); 109 1.must.not.be.greater_than(1); 110 1.must.be_!">" (0); 111 }, 112 "match greater_than_or_equal_to": { 113 1.must.be.greater_than_or_equal_to(1); 114 1.must.not.be.greater_than_or_equal_to(2); 115 }, 116 "match less_than": { 117 1.must.be.less_than(2); 118 1.must.not.be.less_than(1); 119 }, 120 "match less_than_or_equal_to": { 121 1.must.be.less_than_or_equal_to(1); 122 1.must.not.be.less_than_or_equal_to(0); 123 } 124 ]); 125 126 class TestException : Exception { 127 this(string s) { 128 super(s); 129 } 130 131 } 132 133 int throwAnException() { 134 throw new TestException("foo"); 135 } 136 137 void throwAnExceptionAndReturnVoid() { 138 throw new TestException("bar"); 139 } 140 141 describe("Exception matching").should([ 142 "match when an exception is thrown": { 143 throwAnException().must.throw_!TestException; 144 1.must.not.throw_!TestException; 145 }, 146 "work with void function calls": { 147 calling(throwAnExceptionAndReturnVoid()).must.throw_!TestException; 148 } 149 ]); 150 } 151 152 private struct Calling {}; 153 154 Match!Calling calling(lazy void m, string file = __FILE__, size_t line = __LINE__) { 155 return new Match!Calling({ m(); return Calling(); }, file, line); 156 } 157 158 auto must(T)(lazy T m, string file = __FILE__, size_t line = __LINE__) { 159 static if (is(T : Match!Calling)) { 160 return m(); // Allow the form calling(foo()).must.throw.... 161 } else { 162 return new Match!T({ return m(); }, file, line); 163 } 164 } 165 166 class Match(T) { 167 T delegate() match; 168 // TODO I use this for comparing a generic type with the type of this Match. 169 // Might be a better way to do that. 170 T dummyMatch; 171 // Signal that the match is positive, ie it has not been negated with "not" in the chain 172 // (or if it has, it has been negated again by a second "not") 173 bool isPositiveMatch = true; 174 string file; 175 size_t line; 176 177 this(T delegate() match, string file, size_t line) { 178 this.match = match; 179 this.file = file; 180 this.line = line; 181 } 182 183 // Negated match 184 185 auto not() { 186 isPositiveMatch = !isPositiveMatch; 187 return this; 188 } 189 190 // Sugar 191 auto be() { 192 return this; 193 } 194 195 // Help for matching booleans 196 static if (is(T == bool)) { 197 void be(bool expected) { 198 if (expected) 199 True(this); 200 else 201 False(this); 202 } 203 } 204 205 bool opEquals(T rhs) { 206 equal(this, rhs); 207 return true; 208 } 209 210 alias Object.opEquals opEquals; 211 212 void throwMatchException(string reason) { 213 throw new MatchException(reason, file, line); 214 } 215 } 216 217 218 class MatchException : Exception { 219 this(string s, string file = __FILE__, size_t line = __LINE__) { 220 super(s, file, line); 221 } 222 } 223 224 225 void equal(T, T1)(Match!T matcher, T1 expected) 226 if (is(typeof(expected == matcher.dummyMatch) == bool)) 227 { 228 auto match = matcher.match(); 229 if ((expected == match) != matcher.isPositiveMatch) 230 matcher.throwMatchException("Expected " ~ 231 (matcher.isPositiveMatch ? "" : "not ") ~ 232 "<" ~ text(expected) ~ "> but got <" ~ 233 text(match) ~ ">"); 234 } 235 236 private string comparisonInWords(string op)() { 237 if (op == "<") 238 return "less than"; 239 else if (op == "<=") 240 return "less than or equal to"; 241 else if (op == ">") 242 return "greater than"; 243 else if (op == ">=") 244 return "greater than or equal to"; 245 else 246 return "*unknown operation*"; 247 } 248 249 private void comparison(string op, T, T1)(Match!T matcher, T1 expected) { 250 auto match = matcher.match(); 251 auto cmp = mixin("match " ~ op ~ " expected"); 252 if (cmp != matcher.isPositiveMatch) 253 matcher.throwMatchException("Expected something " ~ 254 (matcher.isPositiveMatch ? "" : "not ") ~ 255 comparisonInWords!op ~ 256 " <" ~ text(expected) ~ "> but got <" ~ 257 text(match) ~ ">"); 258 259 } 260 261 void greater_than(T, T1)(Match!T matcher, T1 expected) 262 if (is(typeof(matcher.dummyMatch > expected) == bool)) 263 { 264 comparison!">"(matcher, expected); 265 } 266 267 void greater_than_or_equal_to(T, T1)(Match!T matcher, T1 expected) 268 if (is(typeof(matcher.dummyMatch >= expected) == bool)) 269 { 270 comparison!">="(matcher, expected); 271 } 272 273 void less_than(T, T1)(Match!T matcher, T1 expected) 274 if (is(typeof(matcher.dummyMatch < expected) == bool)) 275 { 276 comparison!"<"(matcher, expected); 277 } 278 279 void less_than_or_equal_to(T, T1)(Match!T matcher, T1 expected) 280 if (is(typeof(matcher.dummyMatch <= expected) == bool)) 281 { 282 comparison!"<="(matcher, expected); 283 } 284 285 void be_(string op, T, T1)(Match!T matcher, T1 expected) 286 if (is(typeof(matcher.dummyMatch > expected) == bool) && 287 (op == ">" || op == ">=" || op == "<" || op == "<=")) 288 { 289 comparison!op(matcher, expected); 290 } 291 292 void between(T, T1)(Match!T matcher, T1 first, T1 last) 293 if (is(typeof(matcher.dummyMatch >= first) == bool)) 294 { 295 auto match = matcher.match(); 296 bool inrange = (match >= first && match <= last); 297 if (inrange != matcher.isPositiveMatch) 298 matcher.throwMatchException("Expected something " ~ 299 (matcher.isPositiveMatch ? "" : "not ") ~ 300 "between <" ~ text(first) ~ "> and <" ~ text(last) ~ "> but got <" ~ text(match) ~ ">"); 301 } 302 void contain(T, T1)(Match!T matcher, T1 fragment) 303 if (is(typeof(indexOf(matcher.dummyMatch, fragment) != -1) == bool)) 304 { 305 auto match = matcher.match(); 306 bool contains = indexOf(match, fragment) != -1; 307 if (contains != matcher.isPositiveMatch) 308 matcher.throwMatchException("Expected <" ~ text(match) ~ "> to " ~ 309 (matcher.isPositiveMatch ? "" : "not ") ~ 310 "contain <" ~ text(fragment) ~ ">"); 311 } 312 313 void True(Match!bool matcher) { 314 auto match = matcher.match(); 315 if (match != matcher.isPositiveMatch) 316 matcher.throwMatchException("Expected <" ~ 317 (matcher.isPositiveMatch ? "true" : "false") ~ 318 "> but got <" ~ 319 text(match) ~ ">"); 320 } 321 322 void False(Match!bool matcher) { 323 auto match = matcher.match(); 324 if (match == matcher.isPositiveMatch) 325 matcher.throwMatchException("Expected <" ~ 326 (matcher.isPositiveMatch ? "false" : "true") ~ 327 "> but got <" ~ 328 text(match) ~ ">"); 329 } 330 331 void Null(T)(Match!T matcher) 332 if (is(typeof(matcher.dummyMatch is null) == bool)) 333 { 334 auto match = matcher.match(); 335 bool isNull = match is null; 336 if (isNull != matcher.isPositiveMatch) 337 matcher.throwMatchException("Expected " ~ 338 (matcher.isPositiveMatch ? "" : "not ") ~ 339 "<null> but got <" ~ 340 text(match) ~ ">"); 341 342 } 343 344 void throw_(E, T)(Match!T matcher) 345 if (is(E : Throwable)) 346 { 347 string exception = ""; 348 try { 349 matcher.match(); 350 if (matcher.isPositiveMatch) 351 exception = "Expected " ~ 352 E.stringof ~ " thrown, but nothing was thrown"; 353 354 } catch (E e) { 355 if (!matcher.isPositiveMatch) 356 exception = "Expected no " ~ 357 E.stringof ~ " thrown, but got " ~ typeof(e).stringof; 358 } 359 360 if (exception != "") 361 matcher.throwMatchException(exception); 362 }