1 module exceeds_expectations.expect_not;
2 
3 import colorize;
4 import exceeds_expectations.exceptions;
5 import exceeds_expectations.pretty_print;
6 import exceeds_expectations.utils;
7 import std.algorithm;
8 import std.conv;
9 import std.file;
10 import std.math;
11 import std.range;
12 import std.traits;
13 
14 
15 /**
16  * Provides negated versions of the usual expectations in [Expect].
17  */
18 struct ExpectNot(TReceived)
19 {
20     private const(TReceived) received;
21     private immutable string filePath;
22     private immutable size_t line;
23     private bool completed = false;
24 
25     package this(const(TReceived) received, string filePath, size_t line)
26     {
27         this.received = received;
28         this.filePath = filePath;
29         this.line = line;
30     }
31 
32     ~this()
33     {
34         if (!completed)
35         {
36             throw new InvalidExpectationException(
37                 "`expect` was called but no assertion was made at " ~
38                 filePath ~ "(" ~ line.to!string ~ "): \n\n" ~
39                 formatCode(readText(filePath), line, 2) ~ "\n",
40                 filePath, line
41             );
42         }
43     }
44 
45 
46     private void fail(string description)
47     {
48         string locationString = "Failing expectation at " ~ filePath ~ "(" ~ line.to!string ~ ")";
49 
50         throw new FailingExpectationException(
51             description,
52             locationString,
53             filePath, line
54         );
55     }
56 
57 
58     /// Checks that `received != expected` and throws a
59     /// [FailingExpectationException] otherwise.
60     public void toEqual(TExpected)(const auto ref TExpected expected)
61     if (canCompareForEquality!(TReceived, TExpected))
62     {
63         completed = true;
64 
65         if (received == expected)
66         {
67             fail(
68                 formatDifferences(prettyPrint(expected), prettyPrint(received), true)
69             );
70         }
71     }
72 
73     /// Checks that `predicate(received)` returns false and throws a
74     /// [FailingExpectationException] otherwise.
75     public void toSatisfy(bool delegate(const(TReceived)) predicate)
76     {
77         completed = true;
78 
79         if (predicate(received))
80         {
81             fail(
82                 "Received: ".color(fg.light_red) ~ prettyPrint(received) ~ "\n" ~
83                 "Received value satisfied the predicate, but was expected not to."
84             );
85         }
86     }
87 
88     /// Checks that `predicate(received)` returns false for at least one of the
89     /// given `predicates` and throws a [FailingExpectationException] otherwise.
90     public void toSatisfyAll(bool delegate(const(TReceived))[] predicates...)
91     {
92         completed = true;
93 
94         if (predicates.length == 0)
95         {
96             throw new InvalidExpectationException(
97                 "Missing predicates at " ~ filePath ~ "(" ~ line.to!string ~ "): \n" ~
98                 "\n" ~ formatCode(readText(filePath), line, 2) ~ "\n",
99                 filePath, line
100             );
101         }
102 
103         if (predicates.length == 1)
104         {
105             toSatisfy(predicates[0]);
106             return;
107         }
108 
109 
110         auto results = predicates.map!(p => p(received));
111         immutable size_t numPassed = count!(e => e)(results);
112 
113         if (numPassed >= predicates.length)
114         {
115             fail(
116                 "Received: ".color(fg.light_red) ~ prettyPrint(received) ~ "\n" ~
117                 "Received value satisfied all predicates, but was expected to fail at least one."
118             );
119         }
120     }
121 
122     /// Checks that `predicate(received)` returns false for all `predicates` and
123     /// throws a [FailingExpectationException] otherwise.
124     public void toSatisfyAny(bool delegate(const(TReceived))[] predicates...)
125     {
126         completed = true;
127 
128         if (predicates.length == 0)
129         {
130             throw new InvalidExpectationException(
131                 "Missing predicates at " ~ filePath ~ "(" ~ line.to!string ~ "): \n" ~
132                 "\n" ~ formatCode(readText(filePath), line, 2) ~ "\n",
133                 filePath, line
134             );
135         }
136 
137         if (predicates.length == 1)
138         {
139             toSatisfy(predicates[0]);
140             return;
141         }
142 
143         auto results = predicates.map!(p => p(received));
144         immutable size_t numPassed = count!(e => e)(results);
145 
146         if (numPassed > 0)
147         {
148             size_t[] passingIndices =
149                 results
150                 .zip(iota(0, results.length))
151                 .filter!(tup => tup[0])
152                 .map!(tup => tup[1])
153                 .array;
154 
155             immutable string description =
156                 numPassed == predicates.length ?
157                 "Received value satisfied all predicates" :
158                 (
159                     "Received value satisfied " ~
160                     (
161                         (
162                             numPassed == 1 ?
163                             "predicate at index " :
164                             "predicates at indices "
165                         ) ~ humanReadableNumbers(passingIndices)
166                     )
167                 ) ~ ", but was expected not to satisfy any.";
168 
169             fail(
170                 "Received: ".color(fg.light_red) ~ prettyPrint(received) ~ "\n" ~
171                 description
172             );
173         }
174 
175     }
176 
177     /**
178      * Checks that `received.isClose(expected, maxRelDiff, maxAbsDiff)` and
179      * throws a [FailingExpectationException] if it is.
180      *
181      * `maxRelDiff` and `maxAbsDiff` have the same default values as in
182      * [std.math.isClose].
183      *
184      * See_Also: [std.math.isClose]
185      */
186     public void toApproximatelyEqual(TExpected, F : real)(
187         const auto ref TExpected expected,
188         F maxRelDiff = CommonDefaultFor!(TReceived, TExpected),
189         F maxAbsDiff = 0.0
190     )
191     if (
192         __traits(compiles, received.isClose(expected, maxRelDiff, maxAbsDiff))
193     )
194     {
195         completed = true;
196 
197         if (received.isClose(expected, maxRelDiff, maxAbsDiff))
198         {
199             fail(
200                 formatDifferences(prettyPrint(expected), prettyPrint(received), true) ~
201                 formatApproxDifferences(expected, received, maxRelDiff, maxAbsDiff)
202             );
203         }
204     }
205 
206     /// Checks that `received !is expected` and throws a
207     /// [FailingExpectationException] otherwise.
208     public void toBe(TExpected)(const auto ref TExpected expected)
209     {
210         completed = true;
211 
212         if (received is expected)
213         {
214             fail(
215                 "Arguments reference the same object (received is expected == true)"
216             );
217         }
218     }
219 
220     /** Checks if received is a `TExpected` or a sub-type of it. If it is, a
221      * [FailingExpectationException] is thrown.
222      *
223      * Note: `null` is considered not to be a sub-type of any class or
224      * interface.
225      */
226     public void toBeOfType(TExpected)()
227     if ((is(TExpected == class) || is(TExpected == interface)) &&
228         (is(TReceived == class) || is(TReceived == interface)))
229     {
230         completed = true;
231 
232         if (cast(TExpected) received)
233         {
234             TypeInfo receivedTypeInfo = typeid(received);
235 
236             static if (is(TReceived == interface))
237             {
238                 receivedTypeInfo = typeid(cast(Object) received);
239             }
240 
241             fail(
242                 formatTypeDifferences(
243                     typeid(TExpected),
244                     receivedTypeInfo,
245                     true
246                 )
247             );
248         }
249     }
250 
251     /**
252      * Calls `received` and catches any exceptions thrown by it. There are three
253      * possible outcomes:
254      *
255      * - `received` throws a `TExpected` or one of its sub-types. A
256      *   [FailingExpectationException] is thrown.
257      *
258      * - `received` doesn't throw a `TExpected`, but does throw something else.
259      *   The expectation passes.
260      *
261      * - `received` doesn't throw anything. The expectation passes.
262      */
263     public void toThrow(TExpected : Throwable = Throwable)()
264     if (isCallable!TReceived)
265     {
266         completed = true;
267 
268         try
269         {
270             received();
271         }
272         catch (Throwable e)             // @suppress(dscanner.suspicious.catch_em_all)
273         {
274             if (!(cast(TExpected) e)) return;
275 
276             fail(
277                 formatTypeDifferences(
278                     typeid(TExpected),
279                     typeid(e),
280                     true
281                 )
282             );
283         }
284     }
285 }