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