1 /**
2  * Pijamas, a BDD assertion library for D.
3  *
4  * License: Licensed under the MIT license. See LICENSE for more information.
5  */
6 module pijamas.assertion;
7 
8 import std.algorithm : canFind, isSorted;
9 import std.range : isInputRange, isForwardRange, hasLength, ElementEncodingType, empty;
10 import std.regex : Regex, StaticRegex;
11 import std.traits : hasMember, isSomeString, isCallable, isArray, isAssociativeArray,
12   isImplicitlyConvertible, Unqual;
13 
14 import pijamas.exception;
15 
16 /**
17  * The function **should** it's an helper or syntax sugar to create the assertation.
18  * Because of D’s lookup shortcut syntax, one is able to use both `should(obj)` and `obj.should` to
19  * get an object wrapped around an Assertion instance
20  */
21 public Assertion!T should(T)(auto ref T context) {
22   return Assertion!T(context);
23 }
24 
25 /**
26  * The function **expect** it's an helper or syntax sugar to create the assertation.
27  * Because of D’s lookup shortcut syntax, one is able to use both `expect(obj)` and `obj.expect` to
28  * get an object wrapped around an Assertion instance
29  */
30 public alias expect = should;
31 
32 /// Class returned by should, that it's used to generate the fluent API
33 struct Assertion(T) {
34   private static bool callable = isCallable!T;
35   private bool negated = false;
36   private string operator = "be";
37   private T context;
38 
39   /// Creates a instance of Assertion, wrapping a value or object
40   this(T _context) @safe pure nothrow {
41     context = _context;
42   }
43   ///ditto
44   this(ref T _context) @safe pure nothrow {
45     context = _context;
46   }
47 
48   /// Identity function. Simply does nothing beyond making assertation more human friendly
49   alias be = id;
50   ///ditto
51   alias to = id;
52   ///ditto
53   alias as = id;
54   ///ditto
55   alias of = id;
56   ///ditto
57   alias a = id;
58   ///ditto
59   alias and = id;
60   ///ditto
61   alias have = id;
62   ///ditto
63   alias which = id;
64   ///ditto
65   Assertion id() @nogc @safe pure nothrow {
66     return this;
67   }
68 
69   /**
70    * This function negates the wrapper assertion. With it, one can express fluent assertions without much effort.
71    *
72    * Examples:
73    * ```
74    * 10.should.not.equal(2); // OK
75    * 10.should.not.equal(10); // Throws an Exception "expected 10 not to be 10"
76    * ```
77    */
78   Assertion not() @nogc @safe pure nothrow {
79     negated = !negated;
80     return this;
81   }
82 
83   version (MirNoGCException) {
84     // Sadly lazy not works with nogc
85     // Helper that evaluates the asserted expression
86     private U ok(U, V)(U expr, V value, string file = __FILE__, size_t line = __LINE__) @nogc @safe {
87       if (negated ? !expr : expr) {
88         return expr;
89       }
90       throw new AssertException(this.message(value).data(), file, line);
91     }
92 
93     // Helper that evaluates the asserted expression
94     private U ok(U)(U expr, string file = __FILE__, size_t line = __LINE__) @nogc @safe {
95       if (negated ? !expr : expr) {
96         return expr;
97       }
98       throw new AssertException(this.message().data(), file, line);
99     }
100 
101     import mir.format;
102 
103     // Generates string message when an assertation fails
104     private stringBuf message(U)(U other) @trusted {
105       import std.traits;
106       import mir.conv : to;
107       import mir.small_string;
108 
109       alias S = SmallString!512;
110       auto buf = stringBuf();
111       buf << "expected [" << T.stringof  << "]";
112       static if ((__traits(isScalar, T) && !isPointer!T) || is(T == struct) || isArray!T ) {
113         buf << " " << to!S(this.context);
114       } else {
115         if (this.context is null) {
116           buf << " null";
117         } //else {
118         //  buf << to!S(this.context);
119         //}
120       }
121       buf << " to ";
122       if (negated) {
123         buf << "not ";
124       }
125       buf << operator << " [" << U.stringof  << "]";
126       static if ((__traits(isScalar, U) && !isPointer!U) || is(U == struct) || isArray!U ) {
127         buf << " " << to!S(other);
128       } else {
129         if (other is null) {
130           buf << " null";
131         }// else {
132         //  buf << to!S(other);
133         //}
134       }
135       return buf;
136     }
137 
138     // Generates string message when an assertation fails
139     private stringBuf message() @trusted {
140       import std.traits;
141       import mir.conv : to;
142       import mir.small_string;
143 
144       alias S = SmallString!512;
145       auto buf = stringBuf();
146       buf << "expected [" << T.stringof  << "]";
147       static if ((__traits(isScalar, T) && !isPointer!T) || is(T == struct) || isArray!T ) {
148         buf << " " << to!S(this.context);
149       } else {
150         if (this.context is null) {
151           buf << " null";
152         //} else {
153         //  buf << to!S(this.context);
154         }
155       }
156       buf << " to ";
157       if (negated) {
158         buf << "not ";
159       }
160       buf << operator;
161       return buf;
162     }
163 
164   } else {
165 
166     // Helper that evaluates the asserted expression
167     private U ok(U, V)(lazy U expr, V value, string file = __FILE__, size_t line = __LINE__) @safe {
168       if (negated ? !expr : expr) {
169         return expr;
170       }
171       throw new AssertException(this.message(value), file, line);
172     }
173 
174     // Helper that evaluates the asserted expression
175     private U ok(U)(lazy U expr, string file = __FILE__, size_t line = __LINE__) @safe {
176       if (negated ? !expr : expr) {
177         return expr;
178       }
179       throw new AssertException(this.message(), file, line);
180     }
181 
182     // Generates string message when an assertation fails
183     private string message(U)(U other) @trusted {
184       import std.string : format;
185       import std.conv : to;
186       import std.traits : isPointer;
187 
188       static if (!isPointer!T) {
189         string contextStr = to!string(this.context);
190       } else {
191         string contextStr = (this.context is null) ? "null" : this.context.to!string;
192       }
193       static if (!isPointer!U) {
194         string otherStr = to!string(other);
195       } else {
196         string otherStr = (other is null) ? "null" : other.to!string;
197       }
198 
199       return format("expected [%s] %s to %s%s [%s] %s", T.stringof, context, (negated ? "not " : ""), operator,
200           U.stringof,  otherStr);
201     }
202 
203     // Generates string message when an assertation fails
204     private string message() @trusted {
205       import std.string : format;
206       import std.conv : to;
207       import std.traits : isPointer;
208 
209       static if (!isPointer!T) {
210         string contextStr = to!string(this.context);
211       } else {
212         string contextStr = (this.context is null) ? "null" : this.context.to!string;
213       }
214 
215       return format("expected [%s] %s to %s%s", T.stringof, contextStr, (negated ? "not " : ""), operator);
216     }
217   }
218 
219   /**
220    * Asserts for equality between two objects. Returns the value wrapped around the assertion.
221    *
222    * Examples:
223    * ```
224    * [1, 2, 3, 4].should.equal([1, 2, 3, 4]);
225    * 255.should.equal(10); // Throws an Exception "expected 255 to equal 10"
226    * ```
227    */
228   T equal(U)(auto ref U other, string file = __FILE__, size_t line = __LINE__) @trusted {
229     this.ok(context == other, other, file, line);
230     return context;
231   }
232 
233   // Ripped from std.math
234   private template CommonDefaultFor(T, U) {
235     import std.traits : CommonType;
236     import std.algorithm.comparison : min;
237 
238     alias baseT = FloatingPointBaseType!T;
239     alias baseU = FloatingPointBaseType!U;
240 
241     enum CommonType!(baseT, baseU) CommonDefaultFor = 10.0L ^^ -((min(baseT.dig, baseU.dig) + 1) / 2 + 1);
242   }
243 
244   // Ripped from std.math
245   private template FloatingPointBaseType(T) {
246     import std.traits : isFloatingPoint;
247     import std.range.primitives : ElementType;
248 
249     static if (isFloatingPoint!T) {
250       alias FloatingPointBaseType = Unqual!T;
251     } else static if (isFloatingPoint!(ElementType!(Unqual!T))) {
252       alias FloatingPointBaseType = Unqual!(ElementType!(Unqual!T));
253     } else {
254       alias FloatingPointBaseType = real;
255     }
256   }
257 
258   /**
259    * Asserts that a float type is aproximated equal. Returns the valued wrapped around the assertion
260    *
261    * Params:
262    *   other = Value to compare to compare.
263    *   maxRelDiff = Maximum allowable relative difference.
264    *   Setting to 0.0 disables this check. Default depends on the type of
265    *   `other` and the original valie: It is approximately half the number of decimal digits of
266    *   precision of the smaller type.
267    *   maxAbsDiff = Maximum absolute difference. This is mainly usefull
268    *   for comparing values to zero. Setting to 0.0 disables this check.
269    *   Defaults to `0.0`.
270    *   file = filename
271    *   line = line number inside of file
272    *
273    * Examples:
274    * ```
275    * double d = 0.1;
276    * double d2 = d + 1e-10;
277    * d.should.not.be.equal(d2);
278    * d.should.be.approxEqual(d2);
279    * ```
280    */
281   T approxEqual(U = double)(U other, U maxRelDiff = CommonDefaultFor!(T, U), U maxAbsDiff = 0.0,
282       string file = __FILE__, size_t line = __LINE__) @trusted
283   if (is(T : real) && __traits(isFloating, T) && is(U : real) && __traits(isFloating, U)) {
284     operator = "be approximated equal than";
285     static if (__traits(compiles, { import std.math : isClose; })) {
286       import std.math : isClose;
287 
288       this.ok(isClose(context, other, maxRelDiff, maxAbsDiff), other, file, line);
289     } else {
290       import std.math : approxEqual;
291 
292       this.ok(approxEqual(context, other, maxRelDiff, maxAbsDiff), other, file, line);
293     }
294     return context;
295   }
296 
297   /**
298    * Alias to approxEqual
299    *
300    * Examples:
301    * ```
302    * double d = 0.1;
303    * double d2 = d + 1e-10;
304    * d.should.not.be.close(d2);
305    * d.should.be.close(d2);
306    * ```
307    */
308   alias close = approxEqual;
309 
310   /**
311    * Asserts whether a value exists - currently simply compares it with null, if it is a pointer, a class or a string.
312    * Returns the value wrapped around the assertion.
313    *
314    * Examples:
315    * ```
316    * auto exists = "I exist!";
317    * should(exists).exist;
318    * string doesntexist;
319    * doesntexist.should.exist; // Throws an Exception "expected null to exist"
320    * Object aObject;
321    * aClass.should.not.exists;
322    * ```
323    */
324   T exist(string file = __FILE__, size_t line = __LINE__) @safe {
325     operator = "be not null or empty";
326     import std.traits : isPointer, isSomeString;
327 
328     static if (isPointer!T || isSomeString!T || __traits(compiles, () {
329         T t;
330         assert(t is null);
331       })) {
332       if (context is null) {
333         this.ok(false, file, line);
334       }
335     }
336     return context;
337   }
338 
339   /**
340    * Asserts if a value is bigger than another value. Returns the result.
341    *
342    * Examples:
343    * ```
344    * "z".should.be.biggerThan("a");
345    * 10.should.be.biggerThan(1);
346    * ```
347    */
348   bool biggerThan(U)(U other, string file = __FILE__, size_t line = __LINE__) @trusted {
349     operator = "be bigger than";
350     return this.ok(context > other, other, file, line);
351   }
352 
353   /**
354    * Asserts if a value is bigger or equal than another value. Returns the result.
355    *
356    * Examples:
357    * ```
358    * "z".should.be.biggerOrEqualThan("a");
359    * 10.should.be.biggerOrEqualThan(10);
360    * 20.should.be.biggerOrEqualThan(10);
361    * ```
362    */
363   bool biggerOrEqualThan(U)(U other, string file = __FILE__, size_t line = __LINE__) @trusted {
364     operator = "be bigger or equal than";
365     return this.ok(context >= other, other, file, line);
366   }
367 
368   /**
369    * Asserts if a value is smaller than another value. Returns the result.
370    *
371    * Examples:
372    * ```
373    * 10.should.be.smallerThan(100);
374    * false.should.be.smallerThan(true);
375    * ```
376    */
377   bool smallerThan(U)(U other, string file = __FILE__, size_t line = __LINE__) @trusted {
378     operator = "be smaller than";
379     return this.ok(context < other, other, file, line);
380   }
381 
382   /**
383    * Asserts if a value is smaller or euqal than another value. Returns the result.
384    *
385    * Examples:
386    * ```
387    * 10.should.be.smallerOrEqualThan(100);
388    * 10.should.be.smallerOrEqualThan(10);
389    * false.should.be.smallerOrEqualThan(true);
390    * ```
391    */
392   bool smallerOrEqualThan(U)(U other, string file = __FILE__, size_t line = __LINE__) @trusted {
393     operator = "be smaller or equal than";
394     return this.ok(context <= other, other, file, line);
395   }
396 
397   static if (isForwardRange!T && __traits(compiles, context.isSorted)) {
398     /**
399      * Asserts whether a forward range is sorted.
400      *
401      * Examples:
402      * ```
403      * [1, 2, 3, 4].should.be.sorted;
404      * [1, 2, 0, 4].should.not.be.sorted;
405      * ```
406      */
407     bool sorted(string file = __FILE__, size_t line = __LINE__) @trusted {
408       operator = "be sorted";
409       return this.ok(context.isSorted, file, line);
410     }
411   }
412 
413   static if (isAssociativeArray!T) {
414     /**
415      * Asserts for an associative array to have a key equal to other.
416      *
417      * Examples:
418      * ```
419      * ["something": 10].should.have.key("something");
420      * ```
421      */
422     void key(U)(U other, string file = __FILE__, size_t line = __LINE__) @trusted {
423       operator = "have key";
424       this.ok(!(other !in context), other, file, line);
425     }
426   }
427 
428   static if (isInputRange!T || isArray!T || isAssociativeArray!T) {
429 
430     version (MirNoGCException) {
431       /**
432       * Asserts for an input range wrapped around an Assertion to contain/include a value.
433       *
434       * Examples:
435       * ```
436       * [1, 2, 3, 4].should.include(3);
437       * "something".should.not.include('o');
438       * "something".should.include("th");
439       * ```
440       */
441       U include(U)(U other, string file = __FILE__, size_t line = __LINE__) @trusted @nogc
442       if (!isAssociativeArray!T) {
443         operator = "contain value";
444         bool found = false;
445         foreach(value ; context) {
446           if (value == other) {
447             found = true;
448             break;
449           }
450         }
451         this.ok(found, other, file, line);
452         return other;
453       }
454 
455       /// ditto
456       U include(U)(U other, string file = __FILE__, size_t line = __LINE__) @trusted
457       if (isAssociativeArray!T) {
458         auto pool = context.values;
459         operator = "contain value";
460         this.ok(canFind(pool, other), other, file, line);
461         return other;
462       }
463     } else {
464 
465       /**
466       * Asserts for an input range wrapped around an Assertion to contain/include a value.
467       *
468       * Examples:
469       * ```
470       * [1, 2, 3, 4].should.include(3);
471       * "something".should.not.include('o');
472       * "something".should.include("th");
473       * ```
474       */
475       U include(U)(U other, string file = __FILE__, size_t line = __LINE__) @trusted {
476         static if (isAssociativeArray!T) {
477           auto pool = context.values;
478         } else {
479           auto pool = context;
480         }
481         operator = "contain value";
482         this.ok(canFind(pool, other), other, file, line);
483         return other;
484       }
485     }
486 
487     ///ditto
488     alias value = include;
489     ///ditto
490     alias contain = include;
491   }
492 
493   static if (hasLength!T || hasMember!(T, "string") || isSomeString!T) {
494     /**
495      * Asserts for the .length property or function value to equal some value.
496      *
497      * Examples:
498      * ```
499      * [1, 2, 3, 4].should.have.length(4);
500      * "abcdefg".should.have.length(0);
501      * // ^^ - Throws an Exception "expected 'abcdefg' to have length of 0"
502      * ```
503      */
504     U length(U)(U len, string file = __FILE__, size_t line = __LINE__) @trusted {
505       operator = "have length of";
506       this.ok(context.length == len, len, file, line);
507       return len;
508     }
509 
510     /**
511      * Asserts for the .length property or function value to be equal to 0.
512      *
513      * Examples:
514      * ```
515      * [].should.be.empty;
516      * "".should.be.empty;
517      * ```
518      */
519     bool empty(string file = __FILE__, size_t line = __LINE__) @trusted {
520       operator = "is empty";
521       return this.ok(context.length == 0, file, line);
522     }
523   }
524 
525   import std.regex : Regex, isRegexFor;
526   import std.traits : isSomeString;
527 
528   static if (isSomeString!T) {
529 
530     /**
531      * Asserts for a string wrapped around the Assertion to match a regular expression.
532      *
533      * Examples:
534      * ```
535      * "something weird".should.match(`[a-z]+`);
536      * "something weird".should.match(regex(`[a-z]+`));
537      * "something 2 weird".should.not.match(ctRegex!(`^[a-z]+$`));
538      * "1234numbers".should.match(`[0-9]+[a-z]+`);
539      * "1234numbers".should.not.match(`^[a-z]+`);
540      * ```
541      */
542     auto match(RegEx)(RegEx re, string file = __FILE__, size_t line = __LINE__) @safe
543     if (isSomeString!T && isRegexFor!(RegEx, T)) {
544       import std.regex : match;
545 
546       auto m = match(context, re);
547       operator = "match";
548       this.ok(!m.empty, re, file, line);
549       return m;
550     }
551 
552     ///ditto
553     auto match(U)(U re, string file = __FILE__, size_t line = __LINE__) @safe
554     if (isSomeString!T && isSomeString!U) {
555       import std.regex : regex, match;
556 
557       auto m = match(context, regex(re));
558       operator = "match";
559       this.ok(!m.empty, re, file, line);
560       return m;
561     }
562   }
563 
564   static if (is(T == bool)) {
565     /**
566      * Asserts for a boolean value to be equal to true.
567      *
568      * Examples:
569      * ```
570      * true.should.be.True;
571      * (1 == 1).should.be.True;
572      * ```
573      */
574     bool True(string file = __FILE__, size_t line = __LINE__) @safe {
575       return this.ok(context == true, true, file, line);
576     }
577 
578     /**
579      * Asserts for a boolean value to be equal to true.
580      *
581      * Examples:
582      * ```
583      * false.should.be.False;
584      * (1 != 1).should.be.False;
585      * ```
586      */
587     bool False(string file = __FILE__, size_t line = __LINE__) @safe {
588       return !this.ok(context == false, false, file, line);
589     }
590   }
591 
592   static if (isCallable!T) {
593     /**
594      * Asserts whether a callable object wrapped around the assertion throws an exception of type T.
595      *
596      * Examples:
597      * ```
598      * void throwing()
599      * {
600      *   throw new Exception("I throw with 0!");
601      * }
602 
603      * should(&throwing).Throw!Exception;
604 
605      * void notThrowing()
606      * {
607      *   return;
608      * }
609      *
610      * should(&notThrowing).not.Throw;
611      * ```
612      */
613     void Throw(T : Throwable = Exception)(string file = __FILE__, size_t line = __LINE__) @trusted {
614       operator = "throw";
615       bool thrown = false;
616       try {
617         this.context();
618       } catch (T) {
619         thrown = true;
620       }
621       this.ok(thrown, file, line);
622     }
623   }
624 
625 }
626 
627 @("Should(v)")
628 @safe unittest {
629   //  it("returns an Assertion", {
630   assert(is(typeof(10.should) == Assertion!int));
631   assert(is(typeof(10f.expect) == Assertion!float));
632 }
633 
634 @("Should Assertion")
635 @safe unittest {
636   //  it("can be instantiated for ranges of structs without `opCmp`", {
637   struct Test {
638     int a;
639     int b;
640   }
641 
642   cast(void)[Test(2, 3)].should;
643 }
644 
645 version (MirNoGCException) {
646   @("Should Assertion.message")
647   unittest {
648     //  it("returns the correct message for binary operators",
649     {
650       auto a = Assertion!int(10);
651       a.operator = "equal";
652       assert(a.message(20).data == "expected [int] 10 to equal [int] 20");
653 
654       auto a2 = Assertion!size_t(23);
655       a2.operator = "be greater that";
656       assert(a2.be.message(20).data == "expected [ulong] 23 to be greater that [int] 20");
657     }
658 
659     //  it("returns the correct message for unary operators",
660     {
661       auto a = Assertion!string("function");
662       a.operator = "throw";
663       assert(a.message.data == "expected [string] function to throw");
664 
665       auto a2 = Assertion!int(23);
666       a2.operator = "exist";
667       assert(a2.message.data == "expected [int] 23 to exist");
668 
669       struct S {
670         int x;
671 
672         /*
673         void toString(C = char, W)(scope ref W w) scope const
674         {
675           w.put("#C#");
676         }
677         */
678       }
679 
680       auto a3 = Assertion!(S*)(null);
681       a3.operator = "exist";
682       assert(a3.message.data == "expected [S*] null to exist");
683     }
684 
685     //  it("returns the correct message for negated operators",
686     {
687       auto a = Assertion!int(10);
688       a.operator = "be";
689       assert(a.not.message(false).data == "expected [int] 10 to not be [bool] false");
690     }
691   }
692 } else {
693   @("Should Assertion.message")
694   unittest {
695     //  it("returns the correct message for binary operators",
696     {
697       auto a = Assertion!int(10);
698       a.operator = "equal";
699       assert(a.message(20) == "expected [int] 10 to equal [int] 20");
700 
701       auto a2 = Assertion!size_t(23);
702       a2.operator = "be greater that";
703       assert(a2.be.message(20) == "expected [ulong] 23 to be greater that [int] 20");
704 
705     }
706 
707     //  it("returns the correct message for unary operators",
708     {
709       auto a = Assertion!string("function");
710       a.operator = "throw";
711       assert(a.message == "expected [string] function to throw");
712 
713       auto a2 = Assertion!int(23);
714       a2.operator = "exist";
715       assert(a2.message == "expected [int] 23 to exist");
716 
717       class C {
718         int x;
719       }
720 
721       C cl = null;
722       auto a3 = Assertion!C(cl);
723       a3.operator = "exist";
724       assert(a3.message == "expected [C] null to exist");
725 
726       auto a4 = Assertion!(C*)(null);
727       a4.operator = "exist";
728       assert(a4.be.message == "expected [C*] null to exist");
729     }
730 
731     //  it("returns the correct message for negated operators",
732     {
733       auto a = Assertion!int(10);
734       a.operator = "be";
735       assert(a.not.message(false) == "expected [int] 10 to not be [bool] false");
736     }
737   }
738 }