1 /**
2  * This module implements custom assertions via $(D shouldXXX) functions
3  * that throw exceptions containing information about why the assertion
4  * failed.
5  */
6 module unit_threaded.assertions;
7 
8 import unit_threaded.exception : fail, UnitTestException;
9 import std.traits; // too many to list
10 import std.range; // also
11 
12 /**
13  * Verify that the condition is `true`.
14  * Throws: UnitTestException on failure.
15  */
16 void shouldBeTrue(E)(lazy E condition, in string file = __FILE__, in size_t line = __LINE__) {
17     shouldEqual(cast(bool) condition, true, file, line);
18 }
19 
20 ///
21 @safe pure unittest {
22     shouldBeTrue(true);
23 }
24 
25 /**
26  * Verify that the condition is `false`.
27  * Throws: UnitTestException on failure.
28  */
29 void shouldBeFalse(E)(lazy E condition, in string file = __FILE__, in size_t line = __LINE__) {
30     shouldEqual(cast(bool) condition, false, file, line);
31 }
32 
33 ///
34 @safe pure unittest {
35     shouldBeFalse(false);
36 }
37 
38 /**
39  * Verify that two values are the same.
40  * Floating point values are compared using $(D std.math.approxEqual).
41  * Throws: UnitTestException on failure
42  */
43 void shouldEqual(V, E)(scope auto ref V value, scope auto ref E expected,
44         in string file = __FILE__, in size_t line = __LINE__) @trusted {
45     if (!isEqual(value, expected)) {
46         const msg = formatValueInItsOwnLine("Expected: ", expected) ~ formatValueInItsOwnLine("     Got: ",
47                 value);
48         throw new UnitTestException(msg, file, line);
49     }
50 }
51 
52 ///
53 @safe pure unittest {
54     shouldEqual(true, true);
55     shouldEqual(false, false);
56     shouldEqual(1, 1);
57     shouldEqual("foo", "foo");
58     shouldEqual([2, 3], [2, 3]);
59 
60     shouldEqual(iota(3), [0, 1, 2]);
61     shouldEqual([[0, 1], [0, 1, 2]], [[0, 1], [0, 1, 2]]);
62     shouldEqual([[0, 1], [0, 1, 2]], [iota(2), iota(3)]);
63     shouldEqual([iota(2), iota(3)], [[0, 1], [0, 1, 2]]);
64 
65 }
66 
67 /**
68  * Verify that two values are not the same.
69  * Throws: UnitTestException on failure
70  */
71 void shouldNotEqual(V, E)(V value, E expected, in string file = __FILE__, in size_t line = __LINE__) {
72     if (isEqual(value, expected)) {
73         const msg = [
74             "Value:", formatValueInItsOwnLine("", value).join(""),
75             "is not expected to be equal to:",
76             formatValueInItsOwnLine("", expected).join("")
77         ];
78         throw new UnitTestException(msg, file, line);
79     }
80 }
81 
82 ///
83 @safe pure unittest {
84     shouldNotEqual(true, false);
85     shouldNotEqual(1, 2);
86     shouldNotEqual("f", "b");
87     shouldNotEqual([2, 3], [2, 3, 4]);
88 }
89 
90 ///
91 @safe unittest {
92     shouldNotEqual(1.0, 2.0);
93 }
94 
95 /**
96  * Verify that the value is null.
97  * Throws: UnitTestException on failure
98  */
99 void shouldBeNull(T)(in auto ref T value, in string file = __FILE__, in size_t line = __LINE__) {
100     if (value !is null)
101         fail("Value is not null", file, line);
102 }
103 
104 ///
105 @safe pure unittest {
106     shouldBeNull(null);
107 }
108 
109 /**
110  * Verify that the value is not null.
111  * Throws: UnitTestException on failure
112  */
113 void shouldNotBeNull(T)(in auto ref T value, in string file = __FILE__, in size_t line = __LINE__) {
114     if (value is null)
115         fail("Value is null", file, line);
116 }
117 
118 ///
119 @safe pure unittest {
120     class Foo {
121         this(int i) {
122             this.i = i;
123         }
124 
125         override string toString() const {
126             import std.conv : to;
127 
128             return i.to!string;
129         }
130 
131         int i;
132     }
133 
134     shouldNotBeNull(new Foo(4));
135 }
136 
137 enum isLikeAssociativeArray(T, K) = is(typeof({
138             if (K.init in T) {
139             }
140             if (K.init !in T) {
141             }
142         }));
143 
144 static assert(isLikeAssociativeArray!(string[string], string));
145 static assert(!isLikeAssociativeArray!(string[string], int));
146 
147 /**
148  * Verify that the value is in the container.
149  * Throws: UnitTestException on failure
150 */
151 void shouldBeIn(T, U)(in auto ref T value, in auto ref U container,
152         in string file = __FILE__, in size_t line = __LINE__)
153         if (isLikeAssociativeArray!(U, T)) {
154     import std.conv : to;
155 
156     if (value !in container) {
157         fail(formatValueInItsOwnLine("Value ",
158                 value) ~ formatValueInItsOwnLine("not in ", container), file, line);
159     }
160 }
161 
162 ///
163 @safe pure unittest {
164     5.shouldBeIn([5: "foo"]);
165 
166     struct AA {
167         int onlyKey;
168         bool opBinaryRight(string op)(in int key) const {
169             return key == onlyKey;
170         }
171     }
172 
173     5.shouldBeIn(AA(5));
174 }
175 
176 /**
177  * Verify that the value is in the container.
178  * Throws: UnitTestException on failure
179  */
180 void shouldBeIn(T, U)(in auto ref T value, U container, in string file = __FILE__,
181         in size_t line = __LINE__) @trusted
182         if (!isLikeAssociativeArray!(U, T) && isInputRange!U) {
183     import std.algorithm : find;
184     import std.conv : to;
185 
186     if (find(container, value).empty) {
187         fail(formatValueInItsOwnLine("Value ",
188                 value) ~ formatValueInItsOwnLine("not in ", container), file, line);
189     }
190 }
191 
192 ///
193 @safe pure unittest {
194     shouldBeIn(4, [1, 2, 4]);
195     shouldBeIn("foo", ["foo": 1]);
196 }
197 
198 /**
199  * Verify that the value is not in the container.
200  * Throws: UnitTestException on failure
201  */
202 void shouldNotBeIn(T, U)(in auto ref T value, in auto ref U container,
203         in string file = __FILE__, in size_t line = __LINE__)
204         if (isLikeAssociativeArray!(U, T)) {
205     import std.conv : to;
206 
207     if (value in container) {
208         fail(formatValueInItsOwnLine("Value ",
209                 value) ~ formatValueInItsOwnLine("is in ", container), file, line);
210     }
211 }
212 
213 ///
214 @safe pure unittest {
215     5.shouldNotBeIn([4: "foo"]);
216 
217     struct AA {
218         int onlyKey;
219         bool opBinaryRight(string op)(in int key) const {
220             return key == onlyKey;
221         }
222     }
223 
224     5.shouldNotBeIn(AA(4));
225 }
226 
227 /**
228  * Verify that the value is not in the container.
229  * Throws: UnitTestException on failure
230  */
231 void shouldNotBeIn(T, U)(in auto ref T value, U container, in string file = __FILE__,
232         in size_t line = __LINE__)
233         if (!isLikeAssociativeArray!(U, T) && isInputRange!U) {
234     import std.algorithm : find;
235     import std.conv : to;
236 
237     if (!find(container, value).empty) {
238         fail(formatValueInItsOwnLine("Value ",
239                 value) ~ formatValueInItsOwnLine("is in ", container), file, line);
240     }
241 }
242 
243 ///
244 @safe unittest {
245     auto arrayRangeWithoutLength(T)(T[] array) {
246         struct ArrayRangeWithoutLength(T) {
247         private:
248             T[] array;
249         public:
250             T front() const @property {
251                 return array[0];
252             }
253 
254             void popFront() {
255                 array = array[1 .. $];
256             }
257 
258             bool empty() const @property {
259                 return array.empty;
260             }
261         }
262 
263         return ArrayRangeWithoutLength!T(array);
264     }
265 
266     shouldNotBeIn(3.5, [1.1, 2.2, 4.4]);
267     shouldNotBeIn(1.0, [2.0: 1, 3.0: 2]);
268     shouldNotBeIn(1, arrayRangeWithoutLength([2, 3, 4]));
269 }
270 
271 /**
272  * Verify that expr throws the templated Exception class.
273  * This succeeds if the expression throws a child class of
274  * the template parameter.
275  * Returns: The caught throwable.
276  * Throws: UnitTestException on failure (when expr does not
277  * throw the expected exception)
278  */
279 auto shouldThrow(T : Throwable = Exception, E)(lazy E expr,
280         in string file = __FILE__, in size_t line = __LINE__) {
281     import std.conv : text;
282 
283     return () @trusted { // @trusted because of catching Throwable
284         try {
285             const result = threw!T(expr);
286             if (result)
287                 return result.throwable;
288         } catch (Throwable t)
289             fail(text("Expression threw ", typeid(t),
290                     " instead of the expected ", T.stringof, ":\n", t.msg), file, line);
291 
292         fail("Expression did not throw", file, line);
293         assert(0);
294     }();
295 }
296 
297 ///
298 @safe pure unittest {
299     void funcThrows(string msg) {
300         throw new Exception(msg);
301     }
302 
303     try {
304         auto exception = funcThrows("foo bar").shouldThrow;
305         assert(exception.msg == "foo bar");
306     } catch (Exception e) {
307         assert(false, "should not have thrown anything and threw: " ~ e.msg);
308     }
309 }
310 
311 ///
312 @safe pure unittest {
313     void func() {
314     }
315 
316     try {
317         func.shouldThrow;
318         assert(false, "Should never get here");
319     } catch (Exception e)
320         assert(e.msg == "Expression did not throw");
321 }
322 
323 ///
324 @safe pure unittest {
325     void funcAsserts() {
326         assert(false, "Oh noes");
327     }
328 
329     try {
330         funcAsserts.shouldThrow;
331         assert(false, "Should never get here");
332     } catch (Exception e)
333         assert(
334                 e.msg
335                 == "Expression threw core.exception.AssertError instead of the expected Exception:\nOh noes");
336 }
337 
338 /**
339  * Verify that expr throws the templated Exception class.
340  * This only succeeds if the expression throws an exception of
341  * the exact type of the template parameter.
342  * Returns: The caught throwable.
343  * Throws: UnitTestException on failure (when expr does not
344  * throw the expected exception)
345  */
346 auto shouldThrowExactly(T : Throwable = Exception, E)(lazy E expr,
347         in string file = __FILE__, in size_t line = __LINE__) {
348     import std.conv : text;
349 
350     const threw = threw!T(expr);
351     if (!threw)
352         fail("Expression did not throw", file, line);
353 
354     //Object.opEquals is @system and impure
355     const sameType = () @trusted { return threw.typeInfo == typeid(T); }();
356     if (!sameType)
357         fail(text("Expression threw wrong type ", threw.typeInfo,
358                 "instead of expected type ", typeid(T)), file, line);
359 
360     return threw.throwable;
361 }
362 
363 /**
364  * Verify that expr does not throw the templated Exception class.
365  * Throws: UnitTestException on failure
366  */
367 void shouldNotThrow(T : Throwable = Exception, E)(lazy E expr,
368         in string file = __FILE__, in size_t line = __LINE__) {
369     if (threw!T(expr))
370         fail("Expression threw", file, line);
371 }
372 
373 /**
374  * Verify that an exception is thrown with the right message
375  */
376 void shouldThrowWithMessage(T : Throwable = Exception, E)(lazy E expr, string msg,
377         string file = __FILE__, size_t line = __LINE__) {
378     auto threw = threw!T(expr);
379     if (!threw)
380         fail("Expression did not throw", file, line);
381 
382     threw.msg.shouldEqual(msg, file, line);
383 }
384 
385 ///
386 @safe pure unittest {
387     void funcThrows(string msg) {
388         throw new Exception(msg);
389     }
390 
391     funcThrows("foo bar").shouldThrowWithMessage!Exception("foo bar");
392     funcThrows("foo bar").shouldThrowWithMessage("foo bar");
393 }
394 
395 //@trusted because the user might want to catch a throwable
396 //that's not derived from Exception, such as RangeError
397 private auto threw(T : Throwable, E)(lazy E expr) @trusted {
398 
399     static struct ThrowResult {
400         bool threw;
401         TypeInfo typeInfo;
402         string msg;
403         immutable(T) throwable;
404 
405         T opCast(T)() @safe @nogc const pure if (is(T == bool)) {
406             return threw;
407         }
408     }
409 
410     import std.stdio;
411 
412     try {
413         expr();
414     } catch (T e) {
415         return ThrowResult(true, typeid(e), e.msg.dup, cast(immutable) e);
416     }
417 
418     return ThrowResult(false);
419 }
420 
421 // Formats output in different lines
422 private string[] formatValueInItsOwnLine(T)(in string prefix, scope auto ref T value) {
423 
424     import std.conv : to;
425     import std.traits : isSomeString;
426     import std.range.primitives : isInputRange;
427 
428     static if (isSomeString!T) {
429         // isSomeString is true for wstring and dstring,
430         // so call .to!string anyway
431         return [prefix ~ `"` ~ value.to!string ~ `"`];
432     } else static if (isInputRange!T) {
433         return formatRange(prefix, value);
434     } else {
435         return [prefix ~ convertToString(value)];
436     }
437 }
438 
439 // helper function for non-copyable types
440 string convertToString(T)(scope auto ref T value) { // std.conv.to sometimes is @system
441     import std.conv : to;
442     import std.traits : Unqual;
443 
444     static if (__traits(compiles, ()@trusted { return value.to!string; }()))
445         return () @trusted { return value.to!string; }();
446     else static if (__traits(compiles, value.toString)) {
447         static if (isObject!T)
448             return () @trusted { return (cast(Unqual!T) value).toString; }();
449         else
450             return value.toString;
451     } else
452         return T.stringof ~ "<cannot print>";
453 }
454 
455 private string[] formatRange(T)(in string prefix, scope auto ref T value) {
456     import std.conv : text;
457     import std.range : ElementType;
458     import std.algorithm : map, reduce, max;
459 
460     //some versions of `text` are @system
461     auto defaultLines = () @trusted { return [prefix ~ value.text]; }();
462 
463     static if (!isInputRange!(ElementType!T))
464         return defaultLines;
465     else {
466         import std.array : array;
467 
468         const maxElementSize = value.empty ? 0 : value.map!(a => a.array.length)
469             .reduce!max;
470         const tooBigForOneLine = (value.array.length > 5 && maxElementSize > 5)
471             || maxElementSize > 10;
472         if (!tooBigForOneLine)
473             return defaultLines;
474         return [prefix ~ "["] ~ value.map!(a => formatValueInItsOwnLine("              ",
475                 a).join("") ~ ",").array ~ "          ]";
476     }
477 }
478 
479 private enum isObject(T) = is(T == class) || is(T == interface);
480 
481 bool isEqual(V, E)(in auto ref V value, in auto ref E expected)
482         if (!isObject!V && !isFloatingPoint!V && !isFloatingPoint!E
483             && is(typeof(value == expected) == bool)) {
484     return value == expected;
485 }
486 
487 bool isEqual(V, E)(in V value, in E expected)
488         if (!isObject!V && (isFloatingPoint!V || isFloatingPoint!E)
489             && is(typeof(value == expected) == bool)) {
490     return value == expected;
491 }
492 
493 void shouldApproxEqual(V, E)(in V value, in E expected, double maxRelDiff = 1e-2,
494         double maxAbsDiff = 1e-5, string file = __FILE__, size_t line = __LINE__)
495         if (!isObject!V && (isFloatingPoint!V || isFloatingPoint!E)
496             && is(typeof(value == expected) == bool)) {
497     import std.math : approxEqual;
498 
499     if (!approxEqual(value, expected, maxRelDiff, maxAbsDiff)) {
500         const msg = formatValueInItsOwnLine("Expected approx: ", expected)
501             ~ formatValueInItsOwnLine("     Got       : ", value);
502         throw new UnitTestException(msg, file, line);
503     }
504 }
505 
506 ///
507 @safe unittest {
508     1.0.shouldApproxEqual(1.0001);
509 }
510 
511 bool isEqual(V, E)(scope V value, scope E expected)
512         if (!isObject!V && isInputRange!V && isInputRange!E
513             && !isSomeString!V && is(typeof(isEqual(value.front, expected.front)))) {
514 
515     while (!value.empty && !expected.empty) {
516         if (!isEqual(value.front, expected.front))
517             return false;
518         value.popFront;
519         expected.popFront;
520     }
521 
522     return value.empty && expected.empty;
523 }
524 
525 bool isEqual(V, E)(scope V value, scope E expected)
526         if (!isObject!V && isInputRange!V && isInputRange!E
527             && isSomeString!V && isSomeString!E && is(typeof(isEqual(value.front, expected.front)))) {
528     if (value.length != expected.length)
529         return false;
530     // prevent auto-decoding
531     foreach (i; 0 .. value.length)
532         if (value[i] != expected[i])
533             return false;
534 
535     return true;
536 }
537 
538 template IsField(A...) if (A.length == 1) {
539     enum IsField = __traits(compiles, A[0].init);
540 }
541 
542 bool isEqual(V, E)(scope V value, scope E expected) if (isObject!V && isObject!E) {
543     import std.meta : staticMap, Filter, staticIndexOf;
544 
545     static assert(is(typeof(() {
546                 string s1 = value.toString;
547                 string s2 = expected.toString;
548             })), "Cannot compare instances of " ~ V.stringof ~ " or "
549             ~ E.stringof ~ " unless toString is overridden for both");
550 
551     if (value is null && expected !is null)
552         return false;
553     if (value !is null && expected is null)
554         return false;
555     if (value is null && expected is null)
556         return true;
557 
558     // If it has opEquals, use it
559     static if (staticIndexOf!("opEquals", __traits(derivedMembers, V)) != -1) {
560         return value.opEquals(expected);
561     } else {
562 
563         template IsFieldOf(T, string s) {
564             static if (__traits(compiles, IsField!(typeof(__traits(getMember, T.init, s)))))
565                 enum IsFieldOf = IsField!(typeof(__traits(getMember, T.init, s)));
566             else
567                 enum IsFieldOf = false;
568         }
569 
570         auto members(T)(T obj) {
571             import std.typecons : Tuple;
572 
573             alias Member(string name) = typeof(__traits(getMember, T, name));
574             alias IsFieldOfT(string s) = IsFieldOf!(T, s);
575             alias FieldNames = Filter!(IsFieldOfT, __traits(allMembers, T));
576             alias FieldTypes = staticMap!(Member, FieldNames);
577 
578             Tuple!FieldTypes ret;
579             foreach (i, name; FieldNames)
580                 ret[i] = __traits(getMember, obj, name);
581 
582             return ret;
583         }
584 
585         static if (is(V == interface))
586             return false;
587         else
588             return members(value) == members(expected);
589     }
590 }
591 
592 /**
593  * Verify that rng is empty.
594  * Throws: UnitTestException on failure.
595  */
596 void shouldBeEmpty(R)(in auto ref R rng, in string file = __FILE__, in size_t line = __LINE__)
597         if (isInputRange!R) {
598     import std.conv : text;
599 
600     if (!rng.empty)
601         fail(text("Range not empty: ", rng), file, line);
602 }
603 
604 /**
605  * Verify that rng is empty.
606  * Throws: UnitTestException on failure.
607  */
608 void shouldBeEmpty(R)(auto ref shared(R) rng, in string file = __FILE__, in size_t line = __LINE__)
609         if (isInputRange!R) {
610     import std.conv : text;
611 
612     if (!rng.empty)
613         fail(text("Range not empty: ", rng), file, line);
614 }
615 
616 /**
617  * Verify that aa is empty.
618  * Throws: UnitTestException on failure.
619  */
620 void shouldBeEmpty(T)(auto ref T aa, in string file = __FILE__, in size_t line = __LINE__)
621         if (isAssociativeArray!T) {
622     //keys is @system
623     () @trusted {
624         if (!aa.keys.empty)
625             fail("AA not empty", file, line);
626     }();
627 }
628 
629 ///
630 @safe pure unittest {
631     int[] ints;
632     string[] strings;
633     string[string] aa;
634 
635     shouldBeEmpty(ints);
636     shouldBeEmpty(strings);
637     shouldBeEmpty(aa);
638 
639     ints ~= 1;
640     strings ~= "foo";
641     aa["foo"] = "bar";
642 }
643 
644 /**
645  * Verify that rng is not empty.
646  * Throws: UnitTestException on failure.
647  */
648 void shouldNotBeEmpty(R)(R rng, in string file = __FILE__, in size_t line = __LINE__)
649         if (isInputRange!R) {
650     if (rng.empty)
651         fail("Range empty", file, line);
652 }
653 
654 /**
655  * Verify that aa is not empty.
656  * Throws: UnitTestException on failure.
657  */
658 void shouldNotBeEmpty(T)(in auto ref T aa, in string file = __FILE__, in size_t line = __LINE__)
659         if (isAssociativeArray!T) {
660     //keys is @system
661     () @trusted {
662         if (aa.keys.empty)
663             fail("AA empty", file, line);
664     }();
665 }
666 
667 ///
668 @safe pure unittest {
669     int[] ints;
670     string[] strings;
671     string[string] aa;
672 
673     ints ~= 1;
674     strings ~= "foo";
675     aa["foo"] = "bar";
676 
677     shouldNotBeEmpty(ints);
678     shouldNotBeEmpty(strings);
679     shouldNotBeEmpty(aa);
680 }
681 
682 /**
683  * Verify that t is greater than u.
684  * Throws: UnitTestException on failure.
685  */
686 void shouldBeGreaterThan(T, U)(in auto ref T t, in auto ref U u,
687         in string file = __FILE__, in size_t line = __LINE__) {
688     import std.conv : text;
689 
690     if (t <= u)
691         fail(text(t, " is not > ", u), file, line);
692 }
693 
694 ///
695 @safe pure unittest {
696     shouldBeGreaterThan(7, 5);
697 }
698 
699 /**
700  * Verify that t is smaller than u.
701  * Throws: UnitTestException on failure.
702  */
703 void shouldBeSmallerThan(T, U)(in auto ref T t, in auto ref U u,
704         in string file = __FILE__, in size_t line = __LINE__) {
705     import std.conv : text;
706 
707     if (t >= u)
708         fail(text(t, " is not < ", u), file, line);
709 }
710 
711 ///
712 @safe pure unittest {
713     shouldBeSmallerThan(5, 7);
714 }
715 
716 /**
717  * Verify that t and u represent the same set (ordering is not important).
718  * Throws: UnitTestException on failure.
719  */
720 void shouldBeSameSetAs(V, E)(auto ref V value, auto ref E expected,
721         in string file = __FILE__, in size_t line = __LINE__)
722         if (isInputRange!V && isInputRange!E && is(typeof(value.front != expected.front) == bool)) {
723     if (!isSameSet(value, expected)) {
724         const msg = formatValueInItsOwnLine("Expected: ", expected) ~ formatValueInItsOwnLine("     Got: ",
725                 value);
726         throw new UnitTestException(msg, file, line);
727     }
728 }
729 
730 ///
731 @safe pure unittest {
732     import std.range : iota;
733 
734     auto inOrder = iota(4);
735     auto noOrder = [2, 3, 0, 1];
736     auto oops = [2, 3, 4, 5];
737 
738     inOrder.shouldBeSameSetAs(noOrder);
739     inOrder.shouldBeSameSetAs(oops).shouldThrow!UnitTestException;
740 
741     struct Struct {
742         int i;
743     }
744 
745     [Struct(1), Struct(4)].shouldBeSameSetAs([Struct(4), Struct(1)]);
746 }
747 
748 private bool isSameSet(T, U)(auto ref T t, auto ref U u) {
749     import std.algorithm : canFind;
750 
751     //sort makes the element types have to implement opCmp
752     //instead, try one by one
753     auto ta = t.array;
754     auto ua = u.array;
755     if (ta.length != ua.length)
756         return false;
757     foreach (element; ta) {
758         if (!ua.canFind(element))
759             return false;
760     }
761 
762     return true;
763 }
764 
765 /**
766  * Verify that value and expected do not represent the same set (ordering is not important).
767  * Throws: UnitTestException on failure.
768  */
769 void shouldNotBeSameSetAs(V, E)(auto ref V value, auto ref E expected,
770         in string file = __FILE__, in size_t line = __LINE__)
771         if (isInputRange!V && isInputRange!E && is(typeof(value.front != expected.front) == bool)) {
772     if (isSameSet(value, expected)) {
773         const msg = [
774             "Value:", formatValueInItsOwnLine("", value).join(""),
775             "is not expected to be equal to:",
776             formatValueInItsOwnLine("", expected).join("")
777         ];
778         throw new UnitTestException(msg, file, line);
779     }
780 }
781 
782 ///
783 @safe pure unittest {
784     auto inOrder = iota(4);
785     auto noOrder = [2, 3, 0, 1];
786     auto oops = [2, 3, 4, 5];
787 
788     inOrder.shouldNotBeSameSetAs(oops);
789     inOrder.shouldNotBeSameSetAs(noOrder).shouldThrow!UnitTestException;
790 }
791 
792 /**
793    If two strings represent the same JSON regardless of formatting
794  */
795 void shouldBeSameJsonAs(in string actual, in string expected,
796         in string file = __FILE__, in size_t line = __LINE__) @trusted // not @safe pure due to parseJSON
797         {
798     import std.json : parseJSON, JSONException;
799 
800     auto parse(in string str) {
801         try
802             return str.parseJSON;
803         catch (JSONException ex)
804             throw new UnitTestException("Error parsing JSON: " ~ ex.msg, file, line);
805     }
806 
807     parse(actual).toPrettyString.shouldEqual(parse(expected).toPrettyString, file, line);
808 }
809 
810 ///
811 @safe unittest { // not pure because parseJSON isn't pure
812     `{"foo": "bar"}`.shouldBeSameJsonAs(`{"foo": "bar"}`);
813     `{"foo":    "bar"}`.shouldBeSameJsonAs(`{"foo":"bar"}`);
814     `{"foo":"bar"}`.shouldBeSameJsonAs(`{"foo": "baz"}`).shouldThrow!UnitTestException;
815     try
816         `oops`.shouldBeSameJsonAs(`oops`);
817     catch (Exception e)
818         assert(e.msg == "Error parsing JSON: Unexpected character 'o'. (Line 1:1)");
819 }
820 
821 auto should(V)(scope auto ref V value) {
822 
823     import std.functional : forward;
824 
825     struct ShouldNot {
826 
827         bool opEquals(U)(auto ref U other, in string file = __FILE__, in size_t line = __LINE__) {
828             shouldNotEqual(forward!value, other, file, line);
829             return true;
830         }
831 
832         void opBinary(string op, R)(R range, in string file = __FILE__, in size_t line = __LINE__) const
833                 if (op == "in") {
834             shouldNotBeIn(forward!value, range, file, line);
835         }
836 
837         void opBinary(string op, R)(R range, in string file = __FILE__, in size_t line = __LINE__) const
838                 if (op == "~" && isInputRange!R) {
839             shouldThrow!UnitTestException(shouldBeSameSetAs(forward!value, range), file, line);
840         }
841 
842         void opBinary(string op, E)(in E expected, string file = __FILE__, size_t line = __LINE__)
843                 if (isFloatingPoint!E) {
844             shouldThrow!UnitTestException(shouldApproxEqual(forward!value, expected), file, line);
845         }
846     }
847 
848     struct Should {
849 
850         bool opEquals(U)(auto ref U other, in string file = __FILE__, in size_t line = __LINE__) {
851             shouldEqual(forward!value, other, file, line);
852             return true;
853         }
854 
855         void opBinary(string op, R)(R range, in string file = __FILE__, in size_t line = __LINE__) const
856                 if (op == "in") {
857             shouldBeIn(forward!value, range, file, line);
858         }
859 
860         void opBinary(string op, R)(R range, in string file = __FILE__, in size_t line = __LINE__) const
861                 if (op == "~" && isInputRange!R) {
862             shouldBeSameSetAs(forward!value, range, file, line);
863         }
864 
865         void opBinary(string op, E)(in E expected, string file = __FILE__, size_t line = __LINE__)
866                 if (isFloatingPoint!E) {
867             shouldApproxEqual(forward!value, expected, 1e-2, 1e-5, file, line);
868         }
869 
870         auto not() {
871             return ShouldNot();
872         }
873     }
874 
875     return Should();
876 }
877 
878 T be(T)(T sh) {
879     return sh;
880 }
881 
882 ///
883 @safe pure unittest {
884     1.should.be == 1;
885     1.should.not.be == 2;
886     1.should.be in [1, 2, 3];
887     4.should.not.be in [1, 2, 3];
888 }
889 
890 /**
891    Asserts that `lowerBound` <= `actual` < `upperBound`
892  */
893 void shouldBeBetween(A, L, U)(auto ref A actual, auto ref L lowerBound,
894         auto ref U upperBound, in string file = __FILE__, in size_t line = __LINE__) {
895     import std.conv : text;
896 
897     if (actual < lowerBound || actual >= upperBound)
898         fail(text(actual, " is not between ", lowerBound, " and ", upperBound), file, line);
899 }