1 /*
2                                     __
3                                    / _|
4   __ _ _   _ _ __ ___  _ __ __ _  | |_ ___  ___ ___
5  / _` | | | | '__/ _ \| '__/ _` | |  _/ _ \/ __/ __|
6 | (_| | |_| | | | (_) | | | (_| | | || (_) \__ \__ \
7  \__,_|\__,_|_|  \___/|_|  \__,_| |_| \___/|___/___/
8 
9 Copyright (C) 2012 Juan Manuel Cabo
10 Copyright (C) 2017 Mario Kröplin
11 Copyright (C) 2019 Aurora Free Open Source Software.
12 Copyright (C) 2019 Luís Ferreira <luis@aurorafoss.org>
13 
14 This file is part of the Aurora Free Open Source Software. This
15 organization promote free and open source software that you can
16 redistribute and/or modify under the terms of the GNU Lesser General
17 Public License Version 3 as published by the Free Software Foundation or
18 (at your option) any later version approved by the Aurora Free Open Source
19 Software Organization. The license is available in the package root path
20 as 'LICENSE' file. Please review the following information to ensure the
21 GNU Lesser General Public License version 3 requirements will be met:
22 https://www.gnu.org/licenses/lgpl.html .
23 
24 Alternatively, this file may be used under the terms of the GNU General
25 Public License version 3 or later as published by the Free Software
26 Foundation. Please review the following information to ensure the GNU
27 General Public License requirements will be met:
28 https://www.gnu.org/licenses/gpl-3.0.html.
29 
30 NOTE: All products, services or anything associated to trademarks and
31 service marks used or referenced on this file are the property of their
32 respective companies/owners or its subsidiaries. Other names and brands
33 may be claimed as the property of others.
34 
35 For more info about intellectual property visit: aurorafoss.org or
36 directly send an email to: contact (at) aurorafoss.org .
37 
38 This file was based on DUnit framework.
39 More about DUnit: https://github.com/linkrope/dunit
40 */
41 
42 module aurorafw.unit.assertion;
43 
44 import core.thread;
45 import core.time;
46 import std.algorithm;
47 import std.array;
48 import std.conv;
49 import std.range;
50 import std..string;
51 import std.traits;
52 import std.typecons;
53 
54 /**
55  * Thrown on an assertion failure.
56  */
57 @safe pure
58 class AssertException : Exception
59 {
60 	@safe pure nothrow this(string msg,
61 			string file = __FILE__,
62 			size_t line = __LINE__,
63 			Throwable next = null)
64 	{
65 		super(msg.empty ? "Assertion failure" : msg, file, line, next);
66 	}
67 }
68 
69 /**
70  * Thrown on an assertion failure.
71  */
72 @safe pure
73 class AssertAllException : AssertException
74 {
75 	private AssertException[] exceptions;
76 
77 	@safe pure nothrow this(AssertException[] exceptions,
78 			string file = __FILE__,
79 			size_t line = __LINE__,
80 			Throwable next = null)
81 	{
82 		string msg = heading(exceptions.length);
83 
84 		exceptions.each!(exception => msg ~= '\n' ~ exception.description);
85 		this.exceptions = exceptions;
86 		super(msg, file, line, next);
87 	}
88 
89 	private @safe pure nothrow static string heading(size_t count)
90 	{
91 		if (count == 1)
92 			return "1 assertion failure:";
93 		else
94 			return text(count, " assertion failures:");
95 	}
96 }
97 
98 @safe pure
99 unittest
100 {
101 	try {
102 		throw new AssertAllException([new AssertException("simple assert from unittesting")]);
103 	} catch(AssertAllException e)
104 	{
105 		import std.algorithm.searching : startsWith;
106 		assert(e.msg.startsWith("1 assertion failure:"));
107 	}
108 }
109 
110 /**
111  * Returns a description of the throwable.
112  */
113 @safe pure nothrow string description(Throwable throwable)
114 {
115 	with (throwable)
116 	{
117 		if (file.empty)
118 			return text(typeid(throwable).name, ": ", msg);
119 		else
120 			return text(typeid(throwable).name, "@", file, "(", line, "): ", msg);
121 	}
122 }
123 
124 ///
125 @safe pure
126 unittest
127 {
128 	assert(description(new Throwable("foobar"))
129 		== "object.Throwable: foobar");
130 
131 	assert(description(new Throwable("foobar", "foo.d", 42))
132 		== "object.Throwable@foo.d(42): foobar");
133 }
134 
135 /**
136  * Returns a description of the difference between the strings.
137  */
138 string description(string expected, string actual) @safe pure
139 {
140 	const MAX_LENGTH = 20;
141 	const result = diff(expected, actual);
142 	const oneLiner = max(result[0].length, result[1].length) <= MAX_LENGTH
143 			&& !result[0].canFind("\n", "\r")
144 			&& !result[1].canFind("\n", "\r");
145 
146 	if (oneLiner)
147 		return "expected: <" ~ result[0] ~ "> but was: <" ~ result[1] ~ ">";
148 	else
149 		return "expected:\n" ~ result[0] ~ "\nbut was:\n" ~ result[1];
150 }
151 
152 ///
153 @safe pure unittest
154 {
155 	assert(description("ab", "Ab") == "expected: <<a>b> but was: <<A>b>");
156 	assert(description("a\nb", "A\nb") == "expected:\n<a>\nb\nbut was:\n<A>\nb");
157 }
158 
159 /**
160  * Returns a pair of strings that highlight the difference between lhs and rhs.
161  */
162 @safe pure
163 Tuple!(string, string) diff(string)(string lhs, string rhs)
164 {
165 	const MAX_LENGTH = 20;
166 
167 	if (lhs == rhs)
168 		return tuple(lhs, rhs);
169 
170 	auto rest = mismatch(lhs, rhs);
171 	auto retroDiff = mismatch(retro(rest[0]), retro(rest[1]));
172 	auto diff = tuple(retro(retroDiff[0]), retro(retroDiff[1]));
173 	string prefix = lhs[0 .. $ - rest[0].length];
174 	string suffix = lhs[prefix.length + diff[0].length .. $];
175 
176 	if (prefix.length > MAX_LENGTH)
177 		prefix = "..." ~ prefix[$ - MAX_LENGTH .. $];
178 	if (suffix.length > MAX_LENGTH)
179 		suffix = suffix[0 .. MAX_LENGTH] ~ "...";
180 
181 	return tuple(
182 			prefix ~ '<' ~ diff[0] ~ '>' ~ suffix,
183 			prefix ~ '<' ~ diff[1] ~ '>' ~ suffix);
184 }
185 
186 ///
187 @safe pure unittest
188 {
189 	assert(diff("abc", "abc") == tuple("abc", "abc"));
190 	// highlight difference
191 	assert(diff("abc", "Abc") == tuple("<a>bc", "<A>bc"));
192 	assert(diff("abc", "aBc") == tuple("a<b>c", "a<B>c"));
193 	assert(diff("abc", "abC") == tuple("ab<c>", "ab<C>"));
194 	assert(diff("abc", "") == tuple("<abc>", "<>"));
195 	assert(diff("abc", "abbc") == tuple("ab<>c", "ab<b>c"));
196 	// abbreviate long prefix or suffix
197 	assert(diff("_12345678901234567890a", "_12345678901234567890A")
198 			== tuple("...12345678901234567890<a>", "...12345678901234567890<A>"));
199 	assert(diff("a12345678901234567890_", "A12345678901234567890_")
200 			== tuple("<a>12345678901234567890...", "<A>12345678901234567890..."));
201 }
202 
203 /**
204  * Asserts that a condition is true.
205  * Throws: AssertException otherwise
206  */
207 @safe pure
208 void assertTrue(T)(T condition, lazy string msg = null,
209 		string file = __FILE__,
210 		size_t line = __LINE__)
211 {
212 	if (cast(bool) condition)
213 		return;
214 
215 	fail(msg, file, line);
216 }
217 
218 ///
219 @safe pure unittest
220 {
221 	assertTrue(true);
222 	assertTrue("foo" in ["foo": "bar"]);
223 
224 	auto exception = expectThrows!AssertException(assertTrue(false));
225 
226 	assertEquals("Assertion failure", exception.msg);
227 }
228 
229 /**
230  * Asserts that a condition is false.
231  * Throws: AssertException otherwise
232  */
233 @safe pure
234 void assertFalse(T)(T condition, lazy string msg = null,
235 		string file = __FILE__,
236 		size_t line = __LINE__)
237 {
238 	if (!cast(bool) condition)
239 		return;
240 
241 	fail(msg, file, line);
242 }
243 
244 ///
245 @safe pure unittest
246 {
247 	assertFalse(false);
248 	assertFalse("foo" in ["bar": "foo"]);
249 
250 	auto exception = expectThrows!AssertException(assertFalse(true));
251 
252 	assertEquals("Assertion failure", exception.msg);
253 }
254 
255 /**
256  * Asserts that the string values are equal.
257  * Throws: AssertException otherwise
258  */
259 @safe pure
260 void assertEquals(T, U)(T expected, U actual, lazy string msg = null,
261 		string file = __FILE__,
262 		size_t line = __LINE__)
263 	if (isSomeString!T)
264 {
265 	if (expected == actual)
266 		return;
267 
268 	string header = (msg.empty) ? null : msg ~ "; ";
269 
270 	fail(header ~ description(expected.to!string, actual.to!string),
271 			file, line);
272 }
273 
274 ///
275 @safe pure unittest
276 {
277 	assertEquals("foo", "foo");
278 
279 	auto exception = expectThrows!AssertException(assertEquals("bar", "baz"));
280 
281 	assertEquals("expected: <ba<r>> but was: <ba<z>>", exception.msg);
282 }
283 
284 /**
285  * Asserts that the floating-point values are approximately equal.
286  * Throws: AssertException otherwise
287  */
288 @safe
289 void assertEquals(T, U)(T expected, U actual, lazy string msg = null,
290 		string file = __FILE__,
291 		size_t line = __LINE__)
292 	if (isFloatingPoint!T || isFloatingPoint!U)
293 {
294 	import std.math : approxEqual;
295 
296 	if (approxEqual(expected, actual))
297 		return;
298 
299 	string header = (msg.empty) ? null : msg ~ "; ";
300 
301 	fail(header ~ format("expected: <%s> but was: <%s>", expected, actual),
302 			file, line);
303 }
304 
305 ///
306 @safe /*pure*/
307 unittest // format is impure for floating point values
308 {
309 	assertEquals(1, 1.01);
310 
311 	auto exception = expectThrows!AssertException(assertEquals(1, 1.1));
312 
313 	assertEquals("expected: <1> but was: <1.1>", exception.msg);
314 }
315 
316 /**
317  * Asserts that the values are equal.
318  * Throws: AssertException otherwise
319  */
320 void assertEquals(T, U)(T expected, U actual, lazy string msg = null,
321 		string file = __FILE__,
322 		size_t line = __LINE__)
323 	if (!isSomeString!T && !isFloatingPoint!T && !isFloatingPoint!U)
324 {
325 	if (expected == actual)
326 		return;
327 
328 	string header = (msg.empty) ? null : msg ~ "; ";
329 
330 	fail(header ~ format("expected: <%s> but was: <%s>", expected, actual),
331 			file, line);
332 }
333 
334 ///
335 @safe pure
336 unittest
337 {
338 	assertEquals(42, 42);
339 
340 	auto exception = expectThrows!AssertException(assertEquals(42, 24));
341 
342 	assertEquals("expected: <42> but was: <24>", exception.msg);
343 }
344 
345 ///
346 @system
347 unittest  // Object.opEquals is impure
348 {
349 	Object foo = new Object();
350 	Object bar = null;
351 
352 	assertEquals(foo, foo);
353 	assertEquals(bar, bar);
354 
355 	auto exception = expectThrows!AssertException(assertEquals(foo, bar));
356 
357 	assertEquals("expected: <object.Object> but was: <null>", exception.msg);
358 }
359 
360 /**
361  * Asserts that the arrays are equal.
362  * Throws: AssertException otherwise
363  */
364 @safe pure
365 void assertArrayEquals(T, U)(in T[] expected, in U[] actual, lazy string msg = null,
366 		string file = __FILE__,
367 		size_t line = __LINE__)
368 {
369 	assertRangeEquals(expected, actual,
370 			msg,
371 			file, line);
372 }
373 
374 /**
375  * Asserts that the associative arrays are equal.
376  * Throws: AssertException otherwise
377  */
378 pure
379 void assertArrayEquals(T, U, V)(in T[V] expected, in U[V] actual, lazy string msg = null,
380 		string file = __FILE__,
381 		size_t line = __LINE__)
382 {
383 	string header = (msg.empty) ? null : msg ~ "; ";
384 
385 	foreach (key; expected.byKey)
386 		if (key in actual)
387 		{
388 			assertEquals(expected[key], actual[key],
389 					format(header ~ "mismatch at key %s", key.repr),
390 					file, line);
391 		}
392 
393 	auto difference = setSymmetricDifference(expected.keys.sort(), actual.keys.sort());
394 
395 	assertEmpty(difference,
396 			format("key mismatch; difference: %(%s, %)", difference),
397 			file, line);
398 }
399 
400 ///
401 @system pure
402 unittest  // keys, values, byKey, byValue not usable in @safe context
403 {
404 	int[string] expected = ["foo": 1, "bar": 2];
405 
406 	assertArrayEquals(expected, ["foo": 1, "bar": 2]);
407 
408 	AssertException exception;
409 
410 	exception = expectThrows!AssertException(assertArrayEquals(expected, ["foo": 2]));
411 	assertEquals(`mismatch at key "foo"; expected: <1> but was: <2>`, exception.msg);
412 	exception = expectThrows!AssertException(assertArrayEquals(expected, ["foo": 1]));
413 	assertEquals(`key mismatch; difference: "bar"`, exception.msg);
414 }
415 
416 /**
417  * Asserts that the ranges are equal.
418  * Throws: AssertException otherwise
419  */
420 @safe pure
421 void assertRangeEquals(R1, R2)(R1 expected, R2 actual, lazy string msg = null,
422 		string file = __FILE__,
423 		size_t line = __LINE__)
424 	if (isInputRange!R1 && isInputRange!R2 && is(typeof(expected.front == actual.front)))
425 {
426 	string header = (msg.empty) ? null : msg ~ "; ";
427 	size_t index = 0;
428 
429 	for (; !expected.empty && ! actual.empty; ++index, expected.popFront, actual.popFront)
430 	{
431 		assertEquals(expected.front, actual.front,
432 				header ~ format("mismatch at index %s", index),
433 				file, line);
434 	}
435 	assertEmpty(expected,
436 			header ~ format("length mismatch at index %s; ", index) ~
437 			format("expected: <%s> but was: empty", expected.front),
438 			file, line);
439 	assertEmpty(actual,
440 			header ~ format("length mismatch at index %s; ", index) ~
441 			format("expected: empty but was: <%s>", actual.front),
442 			file, line);
443 }
444 
445 ///
446 @safe pure
447 unittest
448 {
449 	int[] expected = [0, 1];
450 
451 	assertRangeEquals(expected, [0, 1]);
452 
453 	AssertException exception;
454 
455 	exception = expectThrows!AssertException(assertRangeEquals(expected, [0]));
456 	assertEquals("length mismatch at index 1; expected: <1> but was: empty", exception.msg);
457 	exception = expectThrows!AssertException(assertRangeEquals(expected, [0, 1, 2]));
458 	assertEquals("length mismatch at index 2; expected: empty but was: <2>", exception.msg);
459 	exception = expectThrows!AssertException(assertArrayEquals("bar", "baz"));
460 	assertEquals("mismatch at index 2; expected: <r> but was: <z>", exception.msg);
461 }
462 
463 /**
464  * Asserts that the value is empty.
465  * Throws: AssertException otherwise
466  */
467 @safe pure
468 void assertEmpty(T)(T actual, lazy string msg = null,
469 		string file = __FILE__,
470 		size_t line = __LINE__)
471 {
472 	if (actual.empty)
473 		return;
474 
475 	fail(msg, file, line);
476 }
477 
478 ///
479 @safe pure
480 unittest
481 {
482 	assertEmpty([]);
483 
484 	auto exception = expectThrows!AssertException(assertEmpty([1, 2, 3]));
485 
486 	assertEquals("Assertion failure", exception.msg);
487 }
488 
489 /**
490  * Asserts that the value is not empty.
491  * Throws: AssertException otherwise
492  */
493 @safe pure
494 void assertNotEmpty(T)(T actual, lazy string msg = null,
495 		string file = __FILE__,
496 		size_t line = __LINE__)
497 {
498 	if (!actual.empty)
499 		return;
500 
501 	fail(msg, file, line);
502 }
503 
504 ///
505 @safe pure
506 unittest
507 {
508 	assertNotEmpty([1, 2, 3]);
509 
510 	auto exception = expectThrows!AssertException(assertNotEmpty([]));
511 
512 	assertEquals("Assertion failure", exception.msg);
513 }
514 
515 /**
516  * Asserts that the value is null.
517  * Throws: AssertException otherwise
518  */
519 @safe pure
520 void assertNull(T)(T actual, lazy string msg = null,
521 		string file = __FILE__,
522 		size_t line = __LINE__)
523 {
524 	if (actual is null)
525 		return;
526 
527 	fail(msg, file, line);
528 }
529 
530 ///
531 @safe pure
532 unittest
533 {
534 	Object foo = new Object();
535 
536 	assertNull(null);
537 
538 	auto exception = expectThrows!AssertException(assertNull(foo));
539 
540 	assertEquals("Assertion failure", exception.msg);
541 }
542 
543 /**
544  * Asserts that the value is not null.
545  * Throws: AssertException otherwise
546  */
547 @safe pure
548 void assertNotNull(T)(T actual, lazy string msg = null,
549 		string file = __FILE__,
550 		size_t line = __LINE__)
551 {
552 	if (actual !is null)
553 		return;
554 
555 	fail(msg, file, line);
556 }
557 
558 ///
559 @safe pure
560 unittest
561 {
562 	Object foo = new Object();
563 
564 	assertNotNull(foo);
565 
566 	auto exception = expectThrows!AssertException(assertNotNull(null));
567 
568 	assertEquals("Assertion failure", exception.msg);
569 }
570 
571 /**
572  * Asserts that the values are the same.
573  * Throws: AssertException otherwise
574  */
575 void assertSame(T, U)(T expected, U actual, lazy string msg = null,
576 		string file = __FILE__,
577 		size_t line = __LINE__)
578 {
579 	if (expected is actual)
580 		return;
581 
582 	string header = (msg.empty) ? null : msg ~ "; ";
583 
584 	fail(header ~ format("expected same: <%s> was not: <%s>", expected, actual),
585 			file, line);
586 }
587 
588 ///
589 @system
590 unittest  // format is impure and not safe for Object
591 {
592 	Object foo = new Object();
593 	Object bar = new Object();
594 
595 	assertSame(foo, foo);
596 
597 	auto exception = expectThrows!AssertException(assertSame(foo, bar));
598 
599 	assertEquals("expected same: <object.Object> was not: <object.Object>", exception.msg);
600 }
601 
602 /**
603  * Asserts that the values are not the same.
604  * Throws: AssertException otherwise
605  */
606 @safe pure
607 void assertNotSame(T, U)(T expected, U actual, lazy string msg = null,
608 		string file = __FILE__,
609 		size_t line = __LINE__)
610 {
611 	if (expected !is actual)
612 		return;
613 
614 	string header = (msg.empty) ? null : msg ~ "; ";
615 
616 	fail(header ~ "expected not same",
617 			file, line);
618 }
619 
620 ///
621 @safe pure
622 unittest
623 {
624 	Object foo = new Object();
625 	Object bar = new Object();
626 
627 	assertNotSame(foo, bar);
628 
629 	auto exception = expectThrows!AssertException(assertNotSame(foo, foo));
630 
631 	assertEquals("expected not same", exception.msg);
632 }
633 
634 /**
635  * Asserts that all assertions pass.
636  * Throws: AssertAllException otherwise
637  */
638 @safe pure
639 void assertAll(void delegate() @safe pure [] assertions ...)
640 {
641 	AssertException[] exceptions = null;
642 
643 	foreach (assertion; assertions)
644 		try
645 			assertion();
646 		catch (AssertException exception)
647 			exceptions ~= exception;
648 	if (!exceptions.empty)
649 	{
650 		// [Issue 16345] IFTI fails with lazy variadic function in some cases
651 		const file = null;
652 		const line = 0;
653 
654 		throw new AssertAllException(exceptions, file, line);
655 	}
656 }
657 
658 ///
659 @safe pure
660 unittest
661 {
662 	assertAll(
663 		assertTrue(true),
664 		assertFalse(false),
665 	);
666 
667 	auto exception = expectThrows!AssertException(assertAll(
668 		assertTrue(false),
669 		assertFalse(true),
670 	));
671 
672 	assertTrue(exception.msg.canFind("2 assertion failures"), exception.msg);
673 }
674 
675 
676 /**
677  * Asserts that the expression throws the specified throwable.
678  * Throws: AssertException otherwise
679  * Returns: the caught throwable
680  */
681 @safe pure
682 T expectThrows(T : Throwable = Exception, E)(lazy E expression, lazy string msg = null,
683 		string file = __FILE__,
684 		size_t line = __LINE__)
685 {
686 	try
687 		expression();
688 	catch (T throwable)
689 		return throwable;
690 
691 	string header = (msg.empty) ? null : msg ~ "; ";
692 
693 	fail(header ~ format("expected <%s> was not thrown", T.stringof),
694 			file, line);
695 	assert(0);
696 }
697 
698 ///
699 @safe pure
700 unittest
701 {
702 	import std.exception : enforce;
703 
704 	auto exception = expectThrows(enforce(false));
705 
706 	assertEquals("Enforcement failed", exception.msg);
707 }
708 
709 ///
710 @safe pure
711 unittest
712 {
713 	auto exception = expectThrows!AssertException(expectThrows(42));
714 
715 	assertEquals("expected <Exception> was not thrown", exception.msg);
716 }
717 
718 /**
719  * Fails a test.
720  * Throws: AssertException
721  */
722 @safe pure
723 void fail(string msg = null,
724 		string file = __FILE__,
725 		size_t line = __LINE__)
726 {
727 	throw new AssertException(msg, file, line);
728 }
729 
730 ///
731 @safe pure
732 unittest
733 {
734 	auto exception = expectThrows!AssertException(fail());
735 
736 	assertEquals("Assertion failure", exception.msg);
737 }
738 
739 alias assertGreaterThan = assertOp!">";
740 alias assertGreaterThanOrEqual = assertOp!">=";
741 alias assertLessThan = assertOp!"<";
742 alias assertLessThanOrEqual = assertOp!"<=";
743 alias assertIn = assertOp!"in";
744 alias assertNotIn = assertOp!"!in";
745 
746 /**
747  * Asserts that the condition (lhs op rhs) is satisfied.
748  * Throws: AssertException otherwise
749  * See_Also: http://d.puremagic.com/issues/show_bug.cgi?id=4653
750  */
751 @safe pure
752 template assertOp(string op)
753 {
754 	void assertOp(T, U)(T lhs, U rhs, lazy string msg = null,
755 			string file = __FILE__,
756 			size_t line = __LINE__)
757 	{
758 		mixin("if (lhs " ~ op ~ " rhs) return;");
759 
760 		string header = (msg.empty) ? null : msg ~ "; ";
761 
762 		fail(format("%scondition (%s %s %s) not satisfied",
763 				header, lhs.repr, op, rhs.repr),
764 				file, line);
765 	}
766 }
767 
768 ///
769 @safe pure
770 unittest
771 {
772 	assertLessThan(2, 3);
773 
774 	auto exception = expectThrows!AssertException(assertGreaterThanOrEqual(2, 3));
775 
776 	assertEquals("condition (2 >= 3) not satisfied", exception.msg);
777 }
778 
779 ///
780 @safe pure
781 unittest
782 {
783 	assertIn("foo", ["foo": "bar"]);
784 
785 	auto exception = expectThrows!AssertException(assertNotIn("foo", ["foo": "bar"]));
786 
787 	assertEquals(`condition ("foo" !in ["foo":"bar"]) not satisfied`, exception.msg);
788 }
789 
790 
791 /**
792  * Checks a probe until the timeout expires. The assert error is produced
793  * if the probe fails to return 'true' before the timeout.
794  *
795  * The parameter timeout determines the maximum timeout to wait before
796  * asserting a failure (default is 500ms).
797  *
798  * The parameter delay determines how often the predicate will be
799  * checked (default is 10ms).
800  *
801  * This kind of assertion is very useful to check on code that runs in another
802  * thread. For instance, the thread that listens to a socket.
803  *
804  * Throws: AssertException when the probe fails to become true before timeout
805  */
806 @trusted
807 public static void assertEventually(bool delegate() probe,
808 		Duration timeout = 500.msecs, Duration delay = 10.msecs,
809 		lazy string msg = null,
810 		string file = __FILE__,
811 		size_t line = __LINE__)
812 {
813 	const startTime = TickDuration.currSystemTick();
814 
815 	while (!probe())
816 	{
817 		const elapsedTime = cast(Duration)(TickDuration.currSystemTick() - startTime);
818 
819 		if (elapsedTime >= timeout)
820 			fail(msg.empty ? "timed out" : msg, file, line);
821 
822 		Thread.sleep(delay);
823 	}
824 }
825 
826 
827 ///
828 @safe /* pure */
829 @("Assertation: assertEventually")
830 unittest
831 {
832 	assertEventually({ static count = 0; return ++count > 23; });
833 
834 	auto exception = expectThrows!AssertException(assertEventually({ return false; }));
835 
836 	assertEquals("timed out", exception.msg);
837 }
838 
839 
840 private string repr(T)(T value)
841 {
842 	// format string key with double quotes
843 	return format("%(%s%)", value.only);
844 }