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