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 }