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