1 module dshould;
2 
3 import dshould.ShouldType;
4 
5 public import dshould.ShouldType : because, should;
6 public import dshould.basic;
7 public import dshould.contain;
8 public import dshould.empty;
9 public import dshould.stringcmp;
10 public import dshould.thrown;
11 
12 // dispatch based on type
13 /**
14  * The word `.equal` tests its parameter for equality with the left-hand side.
15  * If the parameters are strings, a colored diff is used.
16  */
17 public void equal(Should, T)(Should should, T value, Fence _ = Fence(), string file = __FILE__, size_t line = __LINE__)
18 if (isInstanceOf!(ShouldType, Should))
19 {
20     import std.json : JSONValue;
21     import std.traits : Unqual;
22 
23     static if (is(typeof(should.got()) == string) && is(T == string) && !should.hasWord!"not")
24     {
25         dshould.stringcmp.equal(should, value, Fence(), file, line);
26     }
27     else static if (
28         is(Unqual!T == JSONValue)
29         && is(Unqual!(typeof(should.got())) == JSONValue)
30         && !should.hasWord!"not"
31     )
32     {
33         should.terminateChain;
34         should.got().toPrettyString.should.equal(value.toPrettyString);
35     }
36     else
37     {
38         dshould.basic.equal(should, value, Fence(), file, line);
39     }
40 }
41 
42 public auto equal(Should)(Should should)
43 if (isInstanceOf!(ShouldType, Should))
44 {
45     return dshould.basic.equal(should);
46 }
47 
48 /**
49  * The word `.throwA` (or `.throwAn`) expects its left-hand side expression to throw an exception.
50  * Instead of the cumbersome `.where.msg.should.equal("msg")`, the `msg` of the Exception to expect
51  * can be passed directly.
52  */
53 public template throwA(T : Throwable)
54 {
55     void throwA(Should)(Should should, string msgTest, Fence _ = Fence(), string file = __FILE__, size_t line = __LINE__)
56     if (isInstanceOf!(ShouldType, Should))
57     {
58         dshould.thrown.throwA!T(should, Fence(), file, line).where.msg.should.equal(msgTest, Fence(), file, line);
59     }
60 
61     auto throwA(Should)(Should should, Fence _ = Fence(), string file = __FILE__, size_t line = __LINE__)
62     if (isInstanceOf!(ShouldType, Should))
63     {
64         return dshould.thrown.throwA!T(should, Fence(), file, line);
65     }
66 }
67 
68 /// ditto
69 public alias throwAn = throwA;
70 
71 @("because defines reason for assertion")
72 unittest
73 {
74     2.should.be(5).because("string A")
75         .should.throwA!FluentException.where.reason.should.equal("string A");
76 }
77 
78 @("compares messages in throwA string overload")
79 unittest
80 {
81     2.should.be(5).because("string A")
82         .should.throwA!FluentException("string B")
83         .should.throwA!FluentException;
84 }
85 
86 @("prints informative errors for int comparison")
87 unittest
88 {
89     2.should.be(3).should.throwA!FluentException("Test failed: expected 3, but got 2");
90 }
91 
92 @("prints informative errors for object comparison")
93 unittest
94 {
95     Object obj;
96 
97     obj.should.not.be(null)
98         .should.throwA!FluentException("Test failed: expected non-null, but got null");
99 
100     obj = new Object;
101 
102     obj.should.be(null)
103         .should.throwA!FluentException("Test failed: expected null, but got object.Object");
104 
105     obj.should.not.be(obj)
106         .should.throwA!FluentException(
107             "Test failed: expected different reference than object.Object, but got same reference");
108 
109     obj.should.be(new Object)
110         .should.throwA!FluentException(
111             "Test failed: expected same reference as object.Object, but got object.Object");
112 }
113 
114 @("prints informative errors for inequalities")
115 unittest
116 {
117     2.should.be.greater.equal(5)
118         .should.throwA!FluentException("Test failed: expected value >= 5, but got 2");
119 
120     2.should.not.be.less.equal(5)
121         .should.throwA!FluentException("Test failed: expected value not <= 5, but got 2");
122 }
123 
124 @("prints informative errors for range emptiness")
125 unittest
126 {
127     [].should.not.be.empty
128         .should.throwA!FluentException("Test failed: expected nonempty range, but got []");
129 
130     [5].should.be.empty
131         .should.throwA!FluentException("Test failed: expected empty range, but got [5]");
132 }
133 
134 @("prettyprints json values for comparison")
135 unittest
136 {
137     import std.json : parseJSON;
138 
139     const expected = `Test failed: expected ' {
140 ` ~ red(`-    "b": "Bar"`) ~ `
141  }
142 ', but got '
143  {
144 ` ~ green(`+    "a": "Foo"`) ~ `
145  }'`;
146 
147     const left = parseJSON(`{"a": "Foo"}`);
148     const right = parseJSON(`{"b": "Bar"}`);
149 
150     left.should.equal(right)
151         .should.throwA!FluentException(expected);
152 }
153 
154 @("prints informative errors for approximate checks")
155 unittest
156 {
157     2.should.approximately.be(4, error=0.5)
158         .should.throwA!FluentException("Test failed: expected 4 ± 0.5, but got 2");
159 
160     (2.4).should.not.approximately.be(2, error=0.5)
161         .should.throwA!FluentException("Test failed: expected value outside 2 ± 0.5, but got 2.4");
162 }
163 
164 @("asserts when forgetting to terminate should expression")
165 unittest
166 {
167     import core.exception : AssertError;
168 
169     void test()
170     {
171         2.should;
172     }
173 
174     test.should.throwAn!AssertError("unterminated should-chain!");
175 }
176 
177 @("exceptions in the lhs don't set off the unterminated-chain error")
178 unittest
179 {
180     int foo()
181     {
182         throw new Exception("");
183     }
184 
185     foo.should.be(3).should.throwAn!Exception;
186 }
187 
188 @("nullable equality")
189 unittest
190 {
191     import std.typecons : Nullable;
192 
193     Nullable!int(42).should.not.equal(Nullable!int());
194 
195     Nullable!int(42).should.equal(Nullable!int()).should.throwA!FluentException
196         ("Test failed: expected value == Nullable!int.null, but got 42");
197 }