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