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