1 module beep;
2 
3 import std.format : format;
4 
5 enum equal;
6 enum less;
7 enum greater;
8 enum contain;
9 enum match;
10 enum throw_;
11 
12 final class ExpectException : Exception {
13 	pure nothrow @safe this(string msg,
14 								  string file = __FILE__,
15 								  size_t line = __LINE__,
16 								  Throwable nextInChain = null) {
17 		super("Expectation failed: " ~ msg, file, line, nextInChain);
18 	}
19 }
20 
21 struct Fence {}
22 
23 T1 expect(OP, T1, T2)(lazy T1 lhs, lazy T2 rhs, Fence _ = Fence(), string file = __FILE__, size_t line = __LINE__)
24 if(is(OP == equal) && __traits(compiles, lhs == rhs)) {
25 	if(!(lhs == rhs))
26 		throw new ExpectException(
27 			"`%s` is expected, got `%s`".format(rhs, lhs),
28 			file,
29 			line,
30 		);
31 
32 	return lhs;
33 }
34 
35 @("expect!equal")
36 @safe pure unittest {
37 	1.expect!equal(1);
38 	"Hi!".expect!equal("Hi!");
39 
40 	struct S {
41 		int* ptr;
42 	}
43 
44 	S(null).expect!equal(S(null));
45 }
46 
47 @("expect!equal checks can be chained")
48 @safe pure unittest {
49 	1.expect!equal(1)
50 		.expect!equal(1)
51 		.expect!equal(1);
52 }
53 
54 @("expect!equal fails if value is not equal to expected value")
55 unittest {
56 	({
57 		1.expect!equal(2);
58 	}).expect!(throw_, ExpectException)
59 		.message.expect!contain("`2` is expected, got `1`");
60 
61 	({
62 		"Hello, Alice!".expect!equal("Hello, Bob!");
63 	}).expect!(throw_, ExpectException)
64 		.message.expect!contain("`Hello, Bob!` is expected, got `Hello, Alice!`");
65 
66 	({
67 		struct S {
68 			string s;
69 			int n;
70 		}
71 
72 		S("Hi!", 42).expect!equal(S("Bye!", 43));
73 	}).expect!(throw_, ExpectException)
74 		.message.expect!contain("`S(\"Bye!\", 43)` is expected, got `S(\"Hi!\", 42)`");
75 }
76 
77 T1 expect(bool bool_, T1)(lazy T1 lhs, Fence _ = Fence(), string file = __FILE__, size_t line = __LINE__) {
78 	return expect!(equal, T1)(lhs, bool_, _, file, line);
79 }
80 
81 @("expect!(true|false) - convenience wrapper around expect!equal")
82 @safe pure unittest {
83 	1.expect!true;
84 	0.expect!false;
85 }
86 
87 @("expect!(true|false) checks can be chained")
88 @safe pure unittest {
89 	1.expect!true
90 		.expect!true
91 		.expect!true;
92 
93 	0.expect!false
94 		.expect!false
95 		.expect!false;
96 }
97 
98 @("expect!(true|false) fails if value is not true|false")
99 unittest {
100 	({
101 		0.expect!true;
102 	}).expect!(throw_, ExpectException)
103 		.message.expect!contain("`true` is expected, got `0`");
104 
105 	({
106 		1.expect!false;
107 	}).expect!(throw_, ExpectException)
108 		.message.expect!contain("`false` is expected, got `1`");
109 }
110 
111 T1 expect(OP, T1, T2)(lazy T1 lhs, lazy T2 rhs, Fence _ = Fence(), string file = __FILE__, size_t line = __LINE__)
112 if(is(OP == less) && __traits(compiles, lhs < rhs)) {
113 	if(!(lhs < rhs))
114 		throw new ExpectException(
115 			"value less than `%s` is expected, got `%s`".format(rhs, lhs),
116 			file,
117 			line,
118 		);
119 
120 	return lhs;
121 }
122 
123 @("expect!less")
124 @safe unittest {
125 	1.expect!less(2);
126 	1.0.expect!less(1.00001);
127 }
128 
129 @("expect!less checks can be chained")
130 @safe pure unittest {
131 	1.expect!less(2)
132 		.expect!less(3)
133 		.expect!less(4)
134 		.expect!less(5)
135 		.expect!less(6);
136 }
137 
138 @("expect!less fails if one value is not less than expected")
139 unittest {
140 	({
141 		1.expect!less(2)
142 			.expect!less(3)
143 			.expect!less(4)
144 			.expect!less(0);
145 	}).expect!(throw_, ExpectException)
146 		.message.expect!contain("value less than `0` is expected, got `1`");
147 }
148 
149 T1 expect(OP, T1, T2)(lazy T1 lhs, lazy T2 rhs, Fence _ = Fence(), string file = __FILE__, size_t line = __LINE__)
150 if(is(OP == greater) && __traits(compiles, lhs > rhs)) {
151 	if(!(lhs > rhs))
152 		throw new ExpectException(
153 			"value greater than `%s` is expected, got `%s`".format(rhs, lhs),
154 			file,
155 			line,
156 		);
157 
158 	return lhs;
159 }
160 
161 @("expect!greater")
162 @safe unittest {
163 	1.expect!greater(0);
164 	1.001.expect!greater(1);
165 }
166 
167 @("expect!greater checks can be chained")
168 @safe pure unittest {
169 	1.expect!greater(0)
170 		.expect!greater(-1)
171 		.expect!greater(-2)
172 		.expect!greater(-3)
173 		.expect!greater(-4)
174 		.expect!greater(-5);
175 }
176 
177 @("expect!greater fails if one value is not greater than expected")
178 unittest {
179 	({
180 		1.expect!greater(0)
181 			.expect!greater(-1)
182 			.expect!greater(2);
183 	}).expect!(throw_, ExpectException)
184 		.message.expect!contain("value greater than `2` is expected, got `1`");
185 }
186 
187 T1 expect(typeof(null) null_, T1)(lazy T1 lhs, Fence _ = Fence(), string file = __FILE__, size_t line = __LINE__) {
188 	if(lhs !is null)
189 		throw new ExpectException(
190 			"null is expected, got `%s`".format(lhs),
191 			file,
192 			line,
193 		);
194 
195 	return lhs;
196 }
197 
198 @("expect!null")
199 @safe pure unittest {
200 	void delegate() func;
201 	func.expect!null;
202 
203 	null.expect!null;
204 }
205 
206 @("expect!null fails if value is not null")
207 unittest {
208 	({
209 		void delegate() func = () {};
210 
211 		func.expect!null;
212 	}).expect!(throw_, ExpectException)
213 		.message.expect!contain("null is expected, got `void delegate()`");
214 
215 	({
216 		auto v = true;
217 		(&v).expect!null;
218 	}).expect!(throw_, ExpectException)
219 		.message.expect!match("null is expected, got `.*`");
220 }
221 
222 T1 expect(OP, T1, T2)(lazy T1 lhs, lazy T2 rhs, Fence _ = Fence(), string file = __FILE__, size_t line = __LINE__)
223 if(is(OP == contain) && __traits(compiles, {import std.algorithm.searching : canFind; lhs.canFind(rhs);})) {
224 	import std.algorithm.searching : canFind;
225 	if(!lhs.canFind(rhs))
226 		throw new ExpectException(
227 			"value is expected to contain `%s`, got `%s`".format(rhs, lhs),
228 			file,
229 			line,
230 		);
231 
232 	return lhs;
233 }
234 
235 @("expect!contain")
236 @safe pure unittest {
237 	"Hello, World!".expect!contain("World");
238 	[1,2,3].expect!contain(1);
239 	[[1,2],[2,3],[4,5]].expect!contain([1,2]);
240 }
241 
242 @("expect!contain checks can be chained")
243 @safe pure unittest {
244 	"Hello, World!".expect!contain("World")
245 		.expect!contain("Hello")
246 		.expect!contain('!')
247 		.expect!contain(',')
248 		.expect!contain(' ');
249 }
250 
251 @("expect!contain fails if value does not contain expected data")
252 unittest {
253 	({
254 		"Hello, World!".expect!contain("Hi!");
255 	}).expect!(throw_, ExpectException)
256 		.message.expect!contain("value is expected to contain `Hi!`, got `Hello, World!`");
257 
258 	({
259 		[1,2,3].expect!contain(4);
260 	}).expect!(throw_, ExpectException)
261 		.message.expect!contain("value is expected to contain `4`, got `[1, 2, 3]`");
262 }
263 
264 T1 expect(OP, T1)(lazy T1 lhs, string re, Fence _ = Fence(), string file = __FILE__, size_t line = __LINE__)
265 if(is(OP == match) && __traits(compiles, {import std.regex : matchFirst; matchFirst(lhs, re);})) {
266 	import std.regex : matchFirst;
267 	if(lhs.matchFirst(re).empty)
268 		throw new ExpectException(
269 			"value matching `%s` is expected, got `%s`".format(re, lhs),
270 			file,
271 			line,
272 		);
273 
274 	return lhs;
275 }
276 
277 @("expect!match")
278 @safe unittest {
279 	"12345".expect!match(r"\d\d\d\d\d");
280 	"abc".expect!match(r"\w{3}");
281 }
282 
283 @("expect!match checks can be chained")
284 @safe unittest {
285 	"123abc".expect!match(r"\d\d\d\w\w\w")
286 		.expect!match(r"\d{3}.*")
287 		.expect!match(r"[1-3]+[a-c]+");
288 }
289 
290 @("expect!match fails if value does not match provided regex")
291 unittest {
292 	({
293 		"123".expect!match(r"abc");
294 	}).expect!(throw_, ExpectException)
295 		.message.expect!contain("value matching `abc` is expected, got `123`");
296 }
297 
298 auto expect(OP, E : Exception = Exception, T1)(lazy T1 lhs, Fence _ = Fence(), string file = __FILE__, size_t line = __LINE__)
299 if(is(OP == throw_) && __traits(compiles, {lhs(/+_+/)(/*_*/);})) {
300 	struct Result {
301 		T1 data;
302 		string message;
303 	}
304 
305 	Result r = Result(lhs);
306 
307 	try {
308 		lhs()();
309 	} catch(Exception e) {
310 		if(!(cast(E) e))
311 			throw new ExpectException(
312 				"`%s` is expected to be thrown, an exception of type `%s` has been thrown instead".format(
313 					typeid(E).name,
314 					typeid(e).name),
315 				file,
316 				line,
317 				e,
318 			);
319 
320 		r.message = e.message.idup;
321 		return r;
322 	}
323 
324 	throw new ExpectException(
325 		"`%s` is expected to be thrown but nothing has been thrown".format(typeid(E).name),
326 		file,
327 		line,
328 	);
329 }
330 
331 @("expect!throw_")
332 unittest {
333 	(() {
334 		throw new Exception("Hello!");
335 	}).expect!throw_;
336 }
337 
338 @("expect!(throw_, CustomException)")
339 unittest {
340 	final class CustomException : Exception {
341 		pure nothrow @nogc @safe this(string msg) {
342 			super(msg);
343 		}
344 	}
345 
346 	({
347 		throw new CustomException("message");
348 	}).expect!(throw_, CustomException);
349 
350 	({
351 		throw new CustomException("message");
352 	}).expect!(throw_, Exception);
353 
354 	({
355 		({
356 			throw new Exception("message");
357 		}).expect!(throw_, CustomException);
358 	}).expect!(throw_, ExpectException);
359 }
360 
361 @("expect!throw_ returns a tuple of the input data and message of the exception that has been thrown")
362 unittest {
363 	auto func = ({
364 		throw new Exception("Hello!");
365 	});
366 	
367 	auto result = func.expect!throw_;
368 
369 	(func is result.data).expect!equal(true);
370 	result.message.expect!equal("Hello!");
371 }
372 
373 @("expect!throw_ does *not* catch Errors")
374 unittest {
375 	import core.exception : AssertError;
376 
377 	bool success;
378 	try {
379 		({
380 			assert(false);
381 		}).expect!throw_;
382 	} catch(AssertError e) { // This is something nobody should ever do
383 		success = true;
384 	}
385 
386 	success.expect!equal(true);
387 }
388 
389 @("expect!throw_ fails when nothing has been thrown")
390 unittest {
391 	({
392 		({
393 			1.expect!true;
394 		}).expect!throw_;
395 	}).expect!(throw_, ExpectException)
396 		.message.expect!contain("`object.Exception` is expected to be thrown but nothing has been thrown");
397 }