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 }