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 /**
15  * The word `.equal` tests its parameter for equality with the left-hand side.
16  * If the parameters are strings, a colored diff is used.
17  */
18 public void equal(Should, T)(Should should, T value, Fence _ = Fence(), string file = __FILE__, size_t line = __LINE__)
19 if (isInstanceOf!(ShouldType, Should))
20 {
21     import dshould.prettyprint : prettyprint;
22     import std.conv : to;
23     import std.json : JSONValue;
24     import std.traits : Unqual;
25 
26     static if (is(typeof(should.got()) == string) && is(T == string) && !should.hasWord!"not")
27     {
28         dshould.stringcmp.equal(should, value, Fence(), file, line);
29     }
30     else static if (!should.hasWord!"not")
31     {
32         static if (
33             is(Unqual!T == JSONValue)
34             && is(Unqual!(typeof(should.got())) == JSONValue))
35         {
36             should.allowOnlyWords!().before!"equal (string)";
37             should.terminateChain;
38 
39             auto got = should.got();
40             const gotString = got.toPrettyString;
41             const valueString = value.toPrettyString;
42         }
43         else static if (
44             __traits(compiles, T.init.toString())
45             && __traits(compiles, typeof(should.got()).init.toString()))
46         {
47             should.allowOnlyWords!().before!"equal (string)";
48             should.terminateChain;
49 
50             auto got = should.got();
51             const gotString = got.toString().prettyprint;
52             const valueString = value.toString().prettyprint;
53         }
54         else static if (
55             __traits(compiles, T.init[0].toString())
56             && __traits(compiles, typeof(should.got()).init[0].toString()))
57         {
58             should.allowOnlyWords!().before!"equal (string)";
59             should.terminateChain;
60 
61             auto got = should.got();
62             const gotString = got.to!string.prettyprint;
63             const valueString = value.to!string.prettyprint;
64         }
65 
66     }
67 
68     static if (__traits(compiles, got))
69     {
70         if (got != value)
71         {
72             stringCmpError(gotString, valueString, false, file, line);
73         }
74     }
75     else
76     {
77         dshould.basic.equal(should, value, Fence(), file, line);
78     }
79 }
80 
81 public auto equal(Should)(Should should)
82 if (isInstanceOf!(ShouldType, Should))
83 {
84     return dshould.basic.equal(should);
85 }
86 
87 /// be is equivalent to equal for the .should.be(value) case
88 public void be(Should, T)(Should should, T value, Fence _ = Fence(), string file = __FILE__, size_t line = __LINE__)
89 if (isInstanceOf!(ShouldType, Should))
90 {
91     static if (should.hasNoWords && !is(T == typeof(null)))
92     {
93         return equal(should, value, Fence(), file, line);
94     }
95     else
96     {
97         // be is basic.be for all other cases
98         return dshould.basic.be(should, value, Fence(), file, line);
99     }
100 }
101 
102 // be is basic.be for all other cases
103 public auto be(Should)(Should should)
104 if (isInstanceOf!(ShouldType, Should))
105 {
106     return dshould.basic.be(should);
107 }
108 
109 // be is basic.be for all other cases
110 public auto be(Should, T)(
111     Should should, T expected, ErrorValue error, Fence _ = Fence(), string file = __FILE__, size_t line = __LINE__)
112 if (isInstanceOf!(ShouldType, Should) && should.hasWord!"approximately")
113 {
114     return dshould.basic.be(should, expected, error, Fence(), file, line);
115 }
116 
117 /**
118  * The word `.throwA` (or `.throwAn`) expects its left-hand side expression to throw an exception.
119  * Instead of the cumbersome `.where.msg.should.equal("msg")`, the `msg` of the Exception to expect
120  * can be passed directly.
121  */
122 public template throwA(T : Throwable)
123 {
124     void throwA(Should)(Should should, string msgTest, Fence _ = Fence(), string file = __FILE__, size_t line = __LINE__)
125     if (isInstanceOf!(ShouldType, Should))
126     {
127         dshould.thrown.throwA!T(should, Fence(), file, line).where.msg.should.equal(msgTest, Fence(), file, line);
128     }
129 
130     auto throwA(Should)(Should should, Fence _ = Fence(), string file = __FILE__, size_t line = __LINE__)
131     if (isInstanceOf!(ShouldType, Should))
132     {
133         return dshould.thrown.throwA!T(should, Fence(), file, line);
134     }
135 }
136 
137 /// ditto
138 public alias throwAn = throwA;
139 
140 @("because defines reason for assertion")
141 unittest
142 {
143     2.should.be(5).because("string A")
144         .should.throwA!FluentException.where.reason.should.equal("string A");
145 }
146 
147 @("compares messages in throwA string overload")
148 unittest
149 {
150     2.should.be(5).because("string A")
151         .should.throwA!FluentException("string B")
152         .should.throwA!FluentException;
153 }
154 
155 @("prints informative errors for int comparison")
156 unittest
157 {
158     2.should.be(3).should.throwA!FluentException("Test failed: expected value == 3, but got 2");
159 }
160 
161 @("prints informative errors for object comparison")
162 unittest
163 {
164     Object obj;
165 
166     obj.should.not.be(null)
167         .should.throwA!FluentException("Test failed: expected non-null, but got null");
168 
169     obj = new Object;
170 
171     obj.should.be(null)
172         .should.throwA!FluentException("Test failed: expected null, but got object.Object");
173 
174     obj.should.not.be.same.as(obj)
175         .should.throwA!FluentException(
176             "Test failed: expected different reference than object.Object, but got same reference");
177 
178     obj.should.be.same.as(new Object)
179         .should.throwA!FluentException(
180             "Test failed: expected same reference as object.Object, but got object.Object");
181 }
182 
183 @("prints informative errors for inequalities")
184 unittest
185 {
186     2.should.be.greater.equal(5)
187         .should.throwA!FluentException("Test failed: expected value >= 5, but got 2");
188 
189     2.should.not.be.less.equal(5)
190         .should.throwA!FluentException("Test failed: expected value not <= 5, but got 2");
191 }
192 
193 @("prints informative errors for range emptiness")
194 unittest
195 {
196     (int[]).init.should.not.be.empty
197         .should.throwA!FluentException("Test failed: expected nonempty range, but got []");
198 
199     [5].should.be.empty
200         .should.throwA!FluentException("Test failed: expected empty range, but got [5]");
201 }
202 
203 @("prettyprints json values for comparison")
204 unittest
205 {
206     import std.json : parseJSON;
207 
208     const expected = `Test failed: expected ` ~ `
209  {
210 ` ~ red(`-    "b": "Bar"`) ~ `
211  }, but got ` ~ `
212  {
213 ` ~ green(`+    "a": "Foo"`) ~ `
214  }`;
215 
216     const left = parseJSON(`{"a": "Foo"}`);
217     const right = parseJSON(`{"b": "Bar"}`);
218 
219     left.should.equal(right)
220         .should.throwA!FluentException(expected);
221     left.should.be(right)
222         .should.throwA!FluentException(expected);
223 }
224 
225 @("prints informative errors for approximate checks")
226 unittest
227 {
228     2.should.approximately.be(4, error=0.5)
229         .should.throwA!FluentException("Test failed: expected 4 ± 0.5, but got 2");
230 
231     (2.4).should.not.approximately.be(2, error=0.5)
232         .should.throwA!FluentException("Test failed: expected value outside 2 ± 0.5, but got 2.4");
233 }
234 
235 @("asserts when forgetting to terminate should expression")
236 unittest
237 {
238     void test()
239     {
240         2.should;
241     }
242 
243     test.should.throwAn!Exception("unterminated should-chain!");
244 }
245 
246 @("exceptions in the lhs don't set off the unterminated-chain error")
247 unittest
248 {
249     int foo()
250     {
251         throw new Exception("");
252     }
253 
254     foo.should.be(3).should.throwAn!Exception;
255 }
256 
257 @("nullable equality")
258 unittest
259 {
260     import std.typecons : Nullable;
261 
262     Nullable!int(42).should.not.equal(Nullable!int());
263 
264     Nullable!int(42).should.equal(Nullable!int()).should.throwA!FluentException
265         ("Test failed: expected Nullable.null, but got 42");
266 }
267 
268 @("nullable equality")
269 unittest
270 {
271     import std.typecons : Nullable;
272 
273     Nullable!string().should.not.equal("");
274 
275     Nullable!string().should.equal("").should.throwA!FluentException
276         ("Test failed: expected value == '', but got Nullable!string.null");
277 }
278 
279 @("exception thrown by value is not hijacked by unterminated should-chain error")
280 unittest
281 {
282     int foo()
283     {
284         throw new Exception("foo");
285     }
286 
287     foo.should.equal(2).should.throwAn!Exception("foo");
288     2.should.equal(foo).should.throwAn!Exception("foo");
289 }
290 
291 @("compare two unequal values with the same toString")
292 unittest
293 {
294     class Class
295     {
296         override bool opEquals(const Object other) const
297         {
298             return false;
299         }
300 
301         override string toString() const
302         {
303             return "Class";
304         }
305     }
306 
307     auto first = new Class;
308     auto second = new Class;
309 
310     (first == second).should.be(false);
311     first.should.equal(second).should.throwAn!Exception("Test failed: expected Class, but got Class");
312 }