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