1 module dshould.basic;
2 
3 import std.string : empty;
4 import dshould.ShouldType;
5 public import dshould.ShouldType : should;
6 
7 /**
8  * The word `.not` negates the current phrase.
9  */
10 public auto not(Should)(Should should) pure
11 if (isInstanceOf!(ShouldType, Should))
12 {
13     should.allowOnlyWords!().before!"not";
14 
15     return should.addWord!"not";
16 }
17 
18 /**
19  * The word `.be` indicates a test for identity.
20  * For value types, this is equivalent to equality.
21  * It takes one parameter and terminates the phrase.
22  */
23 public void be(Should, T)(Should should, T expected, Fence _ = Fence(), string file = __FILE__, size_t line = __LINE__)
24 if (isInstanceOf!(ShouldType, Should) && !should.hasWord!"approximately")
25 {
26     import std.format : format;
27 
28     with (should)
29     {
30         allowOnlyWords!("not").before!"be";
31 
32         enum isNullType = is(T == typeof(null));
33         // only types that can have toString need to disambiguate
34         enum isReferenceType = is(T == class) || is(T == interface);
35 
36         auto got = should.got();
37 
38         static if (hasWord!"not")
39         {
40             const refInfo = isReferenceType ? "different reference than " : "not ";
41 
42             static if (isNullType)
43             {
44                 check(got !is null, "non-null", "null", file, line);
45             }
46             else
47             {
48                 check(
49                     got !is expected,
50                     format("%s%s", refInfo, expected.quote),
51                     isReferenceType ? "same reference" : "it",
52                     file, line
53                 );
54             }
55         }
56         else
57         {
58             const refInfo = isReferenceType ? "same reference as " : "";
59 
60             static if (is(T == typeof(null)))
61             {
62                 check(got is null, "null", format("%s", got.quote), file, line);
63             }
64             else
65             {
66                 check(
67                     got is expected,
68                     format("%s%s", refInfo, expected.quote),
69                     format("%s", got.quote),
70                     file, line
71                 );
72             }
73         }
74     }
75 }
76 
77 ///
78 pure @safe unittest
79 {
80     2.should.be(2);
81     2.should.not.be(5);
82 }
83 
84 ///
85 unittest
86 {
87     (new Object).should.not.be(new Object);
88     (new Object).should.not.be(null);
89     (cast(Object) null).should.be(null);
90 }
91 
92 ///
93 unittest
94 {
95     (cast(void delegate()) null).should.be(null);
96 }
97 
98 /**
99  * When called without parameters, `.be` is a filler word for `.greater`, `.less` or `.equal`.
100  */
101 public auto be(Should)(Should should) pure
102 if (isInstanceOf!(ShouldType, Should))
103 {
104     should.allowOnlyWords!("not").before!"be";
105 
106     return should.addWord!"be";
107 }
108 
109 /**
110  * The word `.equal` tests for equality.
111  * It takes one parameter and terminates the phrase.
112  * Its parameter is the expected value for the left-hand side of the should phrase.
113  */
114 public void equal(Should, T)(Should should, T expected, Fence _ = Fence(), string file = __FILE__, size_t line = __LINE__)
115 if (isInstanceOf!(ShouldType, Should) && !should.hasWord!"approximately")
116 {
117     should.equal.numericCheck(expected, file, line);
118 }
119 
120 ///
121 pure @safe unittest
122 {
123     5.should.equal(5);
124     5.should.not.equal(6);
125 }
126 
127 ///
128 unittest
129 {
130     (new Object).should.not.equal(new Object);
131 }
132 
133 ///
134 unittest
135 {
136     auto obj = new Object;
137 
138     obj.should.equal(obj);
139     obj.should.be(obj);
140 }
141 
142 ///
143 unittest
144 {
145     class SameyClass
146     {
147         override bool opEquals(Object o) { return true; }
148     }
149 
150     (new SameyClass).should.not.be(new SameyClass);
151     (new SameyClass).should.equal(new SameyClass);
152 }
153 
154 /**
155  * When called without parameters, `.equal` must be terminated by `.greater` or `.less`.
156  * .should.be.equal.greater(...) is equivalent to .should.be.greater.equal(...)
157  * is equivalent to assert(got >= expected).
158  */
159 public auto equal(Should)(Should should)
160 if (isInstanceOf!(ShouldType, Should))
161 {
162     should.allowOnlyWords!("not", "be", "greater", "less").before!"equal";
163 
164     return should.addWord!"equal";
165 }
166 
167 ///
168 pure @safe unittest
169 {
170     5.should.not.be.greater.equal(6);
171     5.should.be.greater.equal(5);
172     5.should.be.greater.equal(4);
173     5.should.not.be.greater.equal(6);
174 }
175 
176 /**
177  * The word `.greater` tests that the left-hand side is greater than the expected value.
178  * It takes one parameter and terminates the phrase.
179  */
180 public void greater(Should, T)(Should should, T expected, Fence _ = Fence(), string file = __FILE__, size_t line = __LINE__)
181 if (isInstanceOf!(ShouldType, Should))
182 {
183     should.greater.numericCheck(expected, file, line);
184 }
185 
186 ///
187 pure @safe unittest
188 {
189     5.should.not.be.greater(6);
190     5.should.not.be.greater(5);
191     5.should.be.greater(4);
192 }
193 
194 /**
195  * When called without parameters, `.greater` must be terminated by `.equal`, indicating `>=`.
196  */
197 public auto greater(Should)(Should should)
198 if (isInstanceOf!(ShouldType, Should))
199 {
200     should.allowOnlyWords!("not", "be", "equal").before!"greater";
201     should.requireWord!"be".before!"greater";
202 
203     return should.addWord!"greater";
204 }
205 
206 /**
207  * The word `.less` tests that the left-hand side is less than the expected value.
208  * It takes one parameter and terminates the phrase.
209  */
210 public void less(Should, T)(Should should, T expected, Fence _ = Fence(), string file = __FILE__, size_t line = __LINE__)
211 if (isInstanceOf!(ShouldType, Should))
212 {
213     should.less.numericCheck(expected, file, line);
214 }
215 
216 ///
217 pure @safe unittest
218 {
219     5.should.be.less(6);
220 }
221 
222 /**
223  * When called without parameters, `.less` must be terminated by `.equal`, indicating `<=`.
224  */
225 public auto less(Should)(Should should)
226 if (isInstanceOf!(ShouldType, Should))
227 {
228     should.allowOnlyWords!("not", "be", "equal").before!"less";
229     should.requireWord!"be".before!"less";
230 
231     return should.addWord!"less";
232 }
233 
234 private void numericCheck(Should, T)(Should should, T expected, string file, size_t line)
235 if (isInstanceOf!(ShouldType, Should))
236 {
237     import std.format : format;
238 
239     with (should)
240     {
241         enum equalPart = hasWord!"equal" ? "==" : "";
242         enum equalPartShort = hasWord!"equal" ? "=" : "";
243         enum lessPart = hasWord!"less" ? "<" : "";
244         enum greaterPart = hasWord!"greater" ? ">" : "";
245 
246         auto got = should.got();
247 
248         enum comparison = lessPart ~ greaterPart;
249 
250         enum combined = comparison ~ (comparison.empty ? equalPart : equalPartShort);
251 
252         static if (should.hasWord!"not")
253         {
254             enum checkString = "!(%s " ~ combined ~ " %s)";
255             enum message = "not " ~ combined ~ " %s";
256         }
257         else
258         {
259             enum checkString = "%s " ~ combined ~ " %s";
260             enum message = combined ~ " %s";
261         }
262 
263         check(
264             mixin(format!checkString("got", "expected")),
265             format("value %s", message.format(expected.quote)),
266             format("%s", got.quote),
267             file, line
268         );
269     }
270 }
271 
272 /**
273  * This could be in a separate file, say approx.d,
274  * if doing so didn't crash dmd.
275  * see https://issues.dlang.org/show_bug.cgi?id=18839
276  */
277 
278 private struct ErrorValue
279 {
280     @disable this();
281 
282     private this(double value) pure @safe
283     {
284         this.value = value;
285     }
286 
287     double value;
288 }
289 
290 public auto error(double value) pure @safe
291 {
292     return ErrorValue(value);
293 }
294 
295 /**
296  * `.approximately` is a word indicating an approximate value comparison.
297  * When using .approximately, only the words `.be` and `.equal` may be used, though they may appear before or after.
298  * Each must be called with an additional parameter, `error = <float>`, indicating the amount of permissible error.
299  */
300 public auto approximately(Should)(
301     Should should, double expected, ErrorValue error,
302     Fence _ = Fence(), string file = __FILE__, size_t line = __LINE__
303 )
304 if (isInstanceOf!(ShouldType, Should))
305 {
306     static assert(
307         should.hasWord!"be" || should.hasWord!"equal",
308         `bad grammar: expected "be" or "equal" before "approximately"`
309     );
310 
311     should.allowOnlyWords!("be", "equal", "not").before!"approximately";
312 
313     return should
314         .addWord!"approximately"
315         .approximateCheck(expected, error, file, line);
316 }
317 
318 ///
319 unittest
320 {
321     5.should.be.approximately(5.1, error = 0.11);
322     5.should.approximately.be(5.1, error = 0.11);
323     0.should.approximately.equal(1.0, error = 1.1);
324     0.should.approximately.equal(-1.0, error = 1.1);
325     0.should.not.approximately.equal(1, error = 0.1);
326     42.3.should.be.approximately(42.3, error = 1e-3);
327 }
328 
329 public void be(Should, T)(Should should, T expected, ErrorValue error, Fence _ = Fence(), string file = __FILE__, size_t line = __LINE__)
330 if (isInstanceOf!(ShouldType, Should) && should.hasWord!"approximately")
331 {
332     import std.traits : isDynamicArray;
333 
334     static if (isDynamicArray!T)
335     {
336         pragma(msg, "reference comparison of dynamic array: this is probably not what you want.");
337     }
338 
339     should.allowOnlyWords!("approximately", "not").before!"equal";
340 
341     return should.approximateCheck(expected, error, file, line);
342 }
343 
344 public void equal(Should, T)(Should should, T expected, ErrorValue error, Fence _ = Fence(), string file = __FILE__, size_t line = __LINE__)
345 if (isInstanceOf!(ShouldType, Should) && should.hasWord!"approximately")
346 {
347     should.allowOnlyWords!("approximately", "not").before!"be";
348 
349     return should.approximateCheck(expected, error, file, line);
350 }
351 
352 public auto approximately(Should)(Should should)
353 if (isInstanceOf!(ShouldType, Should))
354 {
355     return should.addWord!"approximately";
356 }
357 
358 private void approximateCheck(Should, T)(Should should, T expected, ErrorValue error, string file, size_t line)
359 if (isInstanceOf!(ShouldType, Should))
360 {
361     import std.format : format;
362     import std.math : abs;
363 
364     with (should)
365     {
366         auto got = should.got();
367 
368         static if (hasWord!"not")
369         {
370             check(
371                 abs(expected - got) >= error.value,
372                 format("value outside %s ± %s", expected, error.value),
373                 format("%s", got),
374                 file, line
375             );
376         }
377         else
378         {
379             check(
380                 abs(expected - got) < error.value,
381                 format("%s ± %s", expected, error.value),
382                 format("%s", got),
383                 file, line
384             );
385         }
386     }
387 }
388 
389 private string quote(T)(T t)
390 {
391     import std.format : format;
392 
393     static if (is(T: string))
394     {
395         return format("'%s'", t);
396     }
397     else
398     {
399         return format("%s", t);
400     }
401 }