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)(auto ref V value, auto ref E expected, in string file = __FILE__, in size_t line = __LINE__)
87 {
88     if (!isEqual(value, expected))
89     {
90         const msg = formatValueInItsOwnLine("Expected: ", expected) ~
91                     formatValueInItsOwnLine("     Got: ", value);
92         throw new UnitTestException(msg, file, line);
93     }
94 }
95 
96 ///
97 @safe pure unittest {
98     shouldEqual(true, true);
99     shouldEqual(false, false);
100     shouldEqual(1, 1) ;
101     shouldEqual("foo", "foo") ;
102     shouldEqual([2, 3], [2, 3]) ;
103 
104     shouldEqual(iota(3), [0, 1, 2]);
105     shouldEqual([[0, 1], [0, 1, 2]], [[0, 1], [0, 1, 2]]);
106     shouldEqual([[0, 1], [0, 1, 2]], [iota(2), iota(3)]);
107     shouldEqual([iota(2), iota(3)], [[0, 1], [0, 1, 2]]);
108 
109 }
110 
111 
112 /**
113  * Verify that two values are not the same.
114  * Throws: UnitTestException on failure
115  */
116 void shouldNotEqual(V, E)(V value, E expected, in string file = __FILE__, in size_t line = __LINE__)
117 {
118     if (isEqual(value, expected))
119     {
120         const msg = ["Value:",
121                      formatValueInItsOwnLine("", value).join(""),
122                      "is not expected to be equal to:",
123                      formatValueInItsOwnLine("", expected).join("")
124             ];
125         throw new UnitTestException(msg, file, line);
126     }
127 }
128 
129 ///
130 @safe pure unittest
131 {
132     shouldNotEqual(true, false);
133     shouldNotEqual(1, 2);
134     shouldNotEqual("f", "b");
135     shouldNotEqual([2, 3], [2, 3, 4]);
136 }
137 
138 ///
139 @safe unittest {
140     shouldNotEqual(1.0, 2.0);
141 }
142 
143 
144 
145 /**
146  * Verify that the value is null.
147  * Throws: UnitTestException on failure
148  */
149 void shouldBeNull(T)(in auto ref T value, in string file = __FILE__, in size_t line = __LINE__)
150 {
151     if (value !is null)
152         fail("Value is not null", file, line);
153 }
154 
155 ///
156 @safe pure unittest
157 {
158     shouldBeNull(null);
159 }
160 
161 
162 /**
163  * Verify that the value is not null.
164  * Throws: UnitTestException on failure
165  */
166 void shouldNotBeNull(T)(in auto ref T value, in string file = __FILE__, in size_t line = __LINE__)
167 {
168     if (value is null)
169         fail("Value is null", file, line);
170 }
171 
172 ///
173 @safe pure unittest
174 {
175     class Foo
176     {
177         this(int i) { this.i = i; }
178         override string toString() const
179         {
180             import std.conv: to;
181             return i.to!string;
182         }
183         int i;
184     }
185 
186     shouldNotBeNull(new Foo(4));
187 }
188 
189 enum isLikeAssociativeArray(T, K) = is(typeof({
190     if(K.init in T) { }
191     if(K.init !in T) { }
192 }));
193 
194 static assert(isLikeAssociativeArray!(string[string], string));
195 static assert(!isLikeAssociativeArray!(string[string], int));
196 
197 
198 /**
199  * Verify that the value is in the container.
200  * Throws: UnitTestException on failure
201 */
202 void shouldBeIn(T, U)(in auto ref T value, in auto ref U container, in string file = __FILE__, in size_t line = __LINE__)
203     if (isLikeAssociativeArray!(U, T))
204 {
205     import std.conv: to;
206 
207     if (value !in container)
208     {
209         fail(formatValueInItsOwnLine("Value ", value) ~ formatValueInItsOwnLine("not in ", container),
210              file, line);
211     }
212 }
213 
214 ///
215 @safe pure unittest {
216     5.shouldBeIn([5: "foo"]);
217 
218     struct AA {
219         int onlyKey;
220         bool opBinaryRight(string op)(in int key) const {
221             return key == onlyKey;
222         }
223     }
224 
225     5.shouldBeIn(AA(5));
226 }
227 
228 /**
229  * Verify that the value is in the container.
230  * Throws: UnitTestException on failure
231  */
232 void shouldBeIn(T, U)(in auto ref T value, U container, in string file = __FILE__, in size_t line = __LINE__)
233     if (!isLikeAssociativeArray!(U, T) && isInputRange!U)
234 {
235     import std.algorithm: find;
236     import std.conv: to;
237 
238     if (find(container, value).empty)
239     {
240         fail(formatValueInItsOwnLine("Value ", value) ~ formatValueInItsOwnLine("not in ", container),
241              file, line);
242     }
243 }
244 
245 ///
246 @safe pure unittest
247 {
248     shouldBeIn(4, [1, 2, 4]);
249     shouldBeIn("foo", ["foo" : 1]);
250 }
251 
252 
253 /**
254  * Verify that the value is not in the container.
255  * Throws: UnitTestException on failure
256  */
257 void shouldNotBeIn(T, U)(in auto ref T value, in auto ref U container,
258                          in string file = __FILE__, in size_t line = __LINE__)
259     if (isLikeAssociativeArray!(U, T))
260 {
261     import std.conv: to;
262 
263     if (value in container)
264     {
265         fail(formatValueInItsOwnLine("Value ", value) ~ formatValueInItsOwnLine("is in ", container),
266              file, line);
267     }
268 }
269 
270 ///
271 @safe pure unittest {
272     5.shouldNotBeIn([4: "foo"]);
273 
274     struct AA {
275         int onlyKey;
276         bool opBinaryRight(string op)(in int key) const {
277             return key == onlyKey;
278         }
279     }
280 
281     5.shouldNotBeIn(AA(4));
282 }
283 
284 
285 /**
286  * Verify that the value is not in the container.
287  * Throws: UnitTestException on failure
288  */
289 void shouldNotBeIn(T, U)(in auto ref T value, U container,
290                          in string file = __FILE__, in size_t line = __LINE__)
291     if (!isLikeAssociativeArray!(U, T) && isInputRange!U)
292 {
293     import std.algorithm: find;
294     import std.conv: to;
295 
296     if (!find(container, value).empty)
297     {
298         fail(formatValueInItsOwnLine("Value ", value) ~ formatValueInItsOwnLine("is in ", container),
299              file, line);
300     }
301 }
302 
303 ///
304 @safe unittest
305 {
306     auto arrayRangeWithoutLength(T)(T[] array)
307     {
308         struct ArrayRangeWithoutLength(T)
309         {
310         private:
311             T[] array;
312         public:
313             T front() const @property
314             {
315                 return array[0];
316             }
317 
318             void popFront()
319             {
320                 array = array[1 .. $];
321             }
322 
323             bool empty() const @property
324             {
325                 return array.empty;
326             }
327         }
328         return ArrayRangeWithoutLength!T(array);
329     }
330 
331     shouldNotBeIn(3.5, [1.1, 2.2, 4.4]);
332     shouldNotBeIn(1.0, [2.0 : 1, 3.0 : 2]);
333     shouldNotBeIn(1, arrayRangeWithoutLength([2, 3, 4]));
334 }
335 
336 /**
337  * Verify that expr throws the templated Exception class.
338  * This succeeds if the expression throws a child class of
339  * the template parameter.
340  * Returns: The caught throwable.
341  * Throws: UnitTestException on failure (when expr does not
342  * throw the expected exception)
343  */
344 auto shouldThrow(T : Throwable = Exception, E)
345                 (lazy E expr, in string file = __FILE__, in size_t line = __LINE__)
346 {
347     import std.conv: text;
348 
349     return () @trusted { // @trusted because of catching Throwable
350         try {
351            const result = threw!T(expr);
352            if (result) return result.throwable;
353         } catch(Throwable t)
354             fail(text("Expression threw ", typeid(t), " instead of the expected ", T.stringof, ":\n", t.msg), file, line);
355 
356         fail("Expression did not throw", file, line);
357         assert(0);
358     }();
359 }
360 
361 ///
362 @safe pure unittest {
363     void funcThrows(string msg) { throw new Exception(msg); }
364     try {
365         auto exception = funcThrows("foo bar").shouldThrow;
366         assert(exception.msg == "foo bar");
367     } catch(Exception e) {
368         assert(false, "should not have thrown anything and threw: " ~ e.msg);
369     }
370 }
371 
372 ///
373 @safe pure unittest {
374     void func() {}
375     try {
376         func.shouldThrow;
377         assert(false, "Should never get here");
378     } catch(Exception e)
379         assert(e.msg == "Expression did not throw");
380 }
381 
382 ///
383 @safe pure unittest {
384     void funcAsserts() { assert(false, "Oh noes"); }
385     try {
386         funcAsserts.shouldThrow;
387         assert(false, "Should never get here");
388     } catch(Exception e)
389         assert(e.msg ==
390                "Expression threw core.exception.AssertError instead of the expected Exception:\nOh noes");
391 }
392 
393 
394 /**
395  * Verify that expr throws the templated Exception class.
396  * This only succeeds if the expression throws an exception of
397  * the exact type of the template parameter.
398  * Returns: The caught throwable.
399  * Throws: UnitTestException on failure (when expr does not
400  * throw the expected exception)
401  */
402 auto shouldThrowExactly(T : Throwable = Exception, E)(lazy E expr,
403     in string file = __FILE__, in size_t line = __LINE__)
404 {
405     import std.conv: text;
406 
407     const threw = threw!T(expr);
408     if (!threw)
409         fail("Expression did not throw", file, line);
410 
411     //Object.opEquals is @system and impure
412     const sameType = () @trusted { return threw.typeInfo == typeid(T); }();
413     if (!sameType)
414         fail(text("Expression threw wrong type ", threw.typeInfo,
415             "instead of expected type ", typeid(T)), file, line);
416 
417     return threw.throwable;
418 }
419 
420 /**
421  * Verify that expr does not throw the templated Exception class.
422  * Throws: UnitTestException on failure
423  */
424 void shouldNotThrow(T: Throwable = Exception, E)(lazy E expr,
425     in string file = __FILE__, in size_t line = __LINE__)
426 {
427     if (threw!T(expr))
428         fail("Expression threw", file, line);
429 }
430 
431 
432 /**
433  * Verify that an exception is thrown with the right message
434  */
435 void shouldThrowWithMessage(T : Throwable = Exception, E)(lazy E expr,
436                                                           string msg,
437                                                           string file = __FILE__,
438                                                           size_t line = __LINE__) {
439     auto threw = threw!T(expr);
440     if (!threw)
441         fail("Expression did not throw", file, line);
442 
443     threw.throwable.msg.shouldEqual(msg, file, line);
444 }
445 
446 ///
447 @safe pure unittest {
448     void funcThrows(string msg) { throw new Exception(msg); }
449     funcThrows("foo bar").shouldThrowWithMessage!Exception("foo bar");
450     funcThrows("foo bar").shouldThrowWithMessage("foo bar");
451 }
452 
453 
454 //@trusted because the user might want to catch a throwable
455 //that's not derived from Exception, such as RangeError
456 private auto threw(T : Throwable, E)(lazy E expr) @trusted
457 {
458 
459     static struct ThrowResult
460     {
461         bool threw;
462         TypeInfo typeInfo;
463         immutable(T) throwable;
464 
465         T opCast(T)() @safe @nogc const pure if (is(T == bool))
466         {
467             return threw;
468         }
469     }
470 
471     import std.stdio;
472     try
473     {
474         expr();
475     }
476     catch (T e)
477     {
478         return ThrowResult(true, typeid(e), cast(immutable)e);
479     }
480 
481     return ThrowResult(false);
482 }
483 
484 
485 
486 void fail(in string output, in string file, in size_t line) @safe pure
487 {
488     throw new UnitTestException([output], file, line);
489 }
490 
491 void fail(in string[] lines, in string file, in size_t line) @safe pure
492 {
493     throw new UnitTestException(lines, file, line);
494 }
495 
496 
497 // Formats output in different lines
498 private string[] formatValueInItsOwnLine(T)(in string prefix, auto ref T value) {
499 
500     import std.conv: to;
501 
502     static if(isSomeString!T) {
503         // isSomeString is true for wstring and dstring,
504         // so call .to!string anyway
505         return [ prefix ~ `"` ~ value.to!string ~ `"`];
506     } else static if(isInputRange!T) {
507         return formatRange(prefix, value);
508     } else {
509         return [prefix ~ convertToString(value)];
510     }
511 }
512 
513 // helper function for non-copyable types
514 string convertToString(T)(in auto ref T value) { // std.conv.to sometimes is @system
515     import std.conv: to;
516     import std.traits: Unqual;
517 
518     static if(__traits(compiles, value.to!string))
519         return () @trusted { return value.to!string; }();
520     else static if(__traits(compiles, value.toString)) {
521         static if(isObject!T)
522             return () @trusted { return (cast(Unqual!T)value).toString; }();
523         else
524             return value.toString;
525     } else
526         return T.stringof ~ "<cannot print>";
527 }
528 
529 
530 private string[] formatRange(T)(in string prefix, T value) {
531     import std.conv: to;
532     import std.range: ElementType;
533     import std.algorithm: map, reduce, max;
534 
535     //some versions of `to` are @system
536     auto defaultLines = () @trusted { return [prefix ~ value.to!string]; }();
537 
538     static if (!isInputRange!(ElementType!T))
539         return defaultLines;
540     else
541     {
542         import std.array: array;
543         const maxElementSize = value.empty ? 0 : value.map!(a => a.array.length).reduce!max;
544         const tooBigForOneLine = (value.array.length > 5 && maxElementSize > 5) || maxElementSize > 10;
545         if (!tooBigForOneLine)
546             return defaultLines;
547         return [prefix ~ "["] ~
548             value.map!(a => formatValueInItsOwnLine("              ", a).join("") ~ ",").array ~
549             "          ]";
550     }
551 }
552 
553 private enum isObject(T) = is(T == class) || is(T == interface);
554 
555 bool isEqual(V, E)(in auto ref V value, in auto ref E expected)
556  if (!isObject!V &&
557      (!isInputRange!V || !isInputRange!E) &&
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 }