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