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