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