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