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 @("equal is pure @safe")
148 pure @safe unittest
149 {
150     import std.datetime : TimeOfDay;
151 
152     "Hello World".should.equal("Hello World");
153     TimeOfDay(1, 2, 3).should.equal(TimeOfDay(1, 2, 3));
154 }
155 
156 @("because defines reason for assertion")
157 unittest
158 {
159     2.should.be(5).because("string A")
160         .should.throwA!FluentException.where.reason.should.equal("string A");
161 }
162 
163 @("compares messages in throwA string overload")
164 unittest
165 {
166     2.should.be(5).because("string A")
167         .should.throwA!FluentException("string B")
168         .should.throwA!FluentException;
169 }
170 
171 @("prints informative errors for int comparison")
172 unittest
173 {
174     2.should.be(3).should.throwA!FluentException("Test failed: expected 3, but got 2");
175 }
176 
177 @("prints informative errors for object comparison")
178 unittest
179 {
180     Object obj;
181 
182     obj.should.not.be(null)
183         .should.throwA!FluentException("Test failed: expected non-null, but got null");
184 
185     obj = new Object;
186 
187     obj.should.be(null)
188         .should.throwA!FluentException("Test failed: expected null, but got object.Object");
189 
190     obj.should.not.be.same.as(obj)
191         .should.throwA!FluentException(
192             "Test failed: expected different reference than object.Object, but got same reference");
193 
194     obj.should.be.same.as(new Object)
195         .should.throwA!FluentException(
196             "Test failed: expected same reference as object.Object, but got object.Object");
197 }
198 
199 @("prints informative errors for inequalities")
200 unittest
201 {
202     2.should.be.greater.equal(5)
203         .should.throwA!FluentException("Test failed: expected value >= 5, but got 2");
204 
205     2.should.not.be.less.equal(5)
206         .should.throwA!FluentException("Test failed: expected value not <= 5, but got 2");
207 }
208 
209 @("prints informative errors for range emptiness")
210 unittest
211 {
212     (int[]).init.should.not.be.empty
213         .should.throwA!FluentException("Test failed: expected nonempty range, but got []");
214 
215     [5].should.be.empty
216         .should.throwA!FluentException("Test failed: expected empty range, but got [5]");
217 }
218 
219 @("prettyprints json values for comparison")
220 unittest
221 {
222     import std.json : parseJSON;
223 
224     const expected = `Test failed: expected ` ~ `
225  {
226 ` ~ red(`-    "b": "Bar"`) ~ `
227  }, but got ` ~ `
228  {
229 ` ~ green(`+    "a": "Foo"`) ~ `
230  }`;
231 
232     const left = parseJSON(`{"a": "Foo"}`);
233     const right = parseJSON(`{"b": "Bar"}`);
234 
235     left.should.equal(right)
236         .should.throwA!FluentException(expected);
237     left.should.be(right)
238         .should.throwA!FluentException(expected);
239 }
240 
241 @("prints informative errors for approximate checks")
242 unittest
243 {
244     2.should.approximately.be(4, error=0.5)
245         .should.throwA!FluentException("Test failed: expected 4 ± 0.5, but got 2");
246 
247     (2.4).should.not.approximately.be(2, error=0.5)
248         .should.throwA!FluentException("Test failed: expected value outside 2 ± 0.5, but got 2.4");
249 }
250 
251 @("asserts when forgetting to terminate should expression")
252 unittest
253 {
254     void test()
255     {
256         2.should;
257     }
258 
259     test.should.throwAn!Exception("unterminated should-chain!");
260 }
261 
262 @("exceptions in the lhs don't set off the unterminated-chain error")
263 unittest
264 {
265     int foo()
266     {
267         throw new Exception("");
268     }
269 
270     foo.should.be(3).should.throwAn!Exception;
271 }
272 
273 @("nullable equality")
274 unittest
275 {
276     import std.typecons : Nullable;
277 
278     Nullable!int(42).should.not.equal(Nullable!int());
279 
280     Nullable!int(42).should.equal(Nullable!int()).should.throwA!FluentException
281         ("Test failed: expected Nullable.null, but got 42");
282 }
283 
284 @("nullable equality with value")
285 unittest
286 {
287     import std.typecons : Nullable;
288 
289     Nullable!string().should.not.equal("");
290 
291     Nullable!string().should.equal("")
292         .should.throwA!FluentException("Test failed: expected '', but got Nullable.null");
293 }
294 
295 @("value equality with nullable")
296 unittest
297 {
298     import std.typecons : Nullable;
299 
300     "".should.not.equal(Nullable!string());
301 
302     "".should.equal(Nullable!string())
303         .should.throwA!FluentException("Test failed: expected Nullable.null, but got ''");
304 }
305 
306 @("exception thrown by value is not hijacked by unterminated should-chain error")
307 unittest
308 {
309     int foo()
310     {
311         throw new Exception("foo");
312     }
313 
314     foo.should.equal(2).should.throwAn!Exception("foo");
315     2.should.equal(foo).should.throwAn!Exception("foo");
316 }
317 
318 @("compare two unequal values with the same toString")
319 unittest
320 {
321     class Class
322     {
323         override bool opEquals(const Object other) const
324         {
325             return false;
326         }
327 
328         override string toString() const
329         {
330             return "Class";
331         }
332     }
333 
334     auto first = new Class;
335     auto second = new Class;
336 
337     (first == second).should.be(false);
338     first.should.equal(second)
339         .should.throwAn!Exception("Test failed: expected Class, but got Class");
340 }
341 
342 @("nullable should be null")
343 unittest
344 {
345     import std.typecons : Nullable;
346 
347     Nullable!int().should.not.beNull
348         .should.throwA!FluentException("Test failed: expected non-null Nullable, but got Nullable.null");
349     Nullable!int(5).should.beNull
350         .should.throwA!FluentException("Test failed: expected Nullable.null, but got 5");
351 }
352 
353 @("object should be null")
354 unittest
355 {
356     Object.init.should.not.beNull.should.throwA!FluentException("Test failed: expected non-null, but got null");
357     (new Object).should.beNull.should.throwA!FluentException("Test failed: expected null, but got object.Object");
358 }