1 module mocked.error;
2 
3 import std.algorithm;
4 import std.array;
5 import std.conv;
6 import std.format;
7 import std.traits;
8 import std.typecons;
9 
10 /**
11  * Constructs an $(D_PSYMBOL UnexpectedCallError).
12  *
13  * Params:
14  *     T = Object type.
15  *     Args = $(D_PARAM name)'s argument types.
16  *     name = Unexpected call name.
17  *     arguments = $(D_PARAM name)'s arguments.
18  *     file = File.
19  *     line = Line number.
20  *     nextInChain = The next error.
21  *
22  * Returns: $(D_PSYMBOL UnexpectedCallError).
23  */
24 UnexpectedCallError unexpectedCallError(T, Args...)(
25         string name, Args arguments,
26         string file = __FILE__, size_t line = __LINE__,
27         Throwable nextInChain = null)
28 {
29     return new UnexpectedCallError(formatName!T(name),
30             formatArguments!Args(arguments), file, line, nextInChain);
31 }
32 
33 private string[] formatArguments(Args...)(ref Args arguments)
34 {
35     string[] formattedArguments;
36     auto spec = singleSpec("%s");
37     auto writer = appender!(char[])();
38 
39     static foreach (i, Arg; Args)
40     {
41         static if (isSomeString!Arg)
42         {
43             formattedArguments ~= format!"%(%s %)"([arguments[i]]);
44         }
45         else
46         {
47             writer.clear();
48             writer.formatValue(arguments[i], spec);
49             formattedArguments ~= writer[].idup;
50         }
51     }
52     return formattedArguments;
53 }
54 
55 private string formatName(T)(string name)
56 {
57     return format!"%s.%s"(T.stringof, name);
58 }
59 
60 /**
61  * Thrown when an unexpected call occurs.
62  */
63 final class UnexpectedCallError : Error
64 {
65     private string name;
66     private string[] arguments;
67 
68     /**
69      * Constructs an $(D_PSYMBOL UnexpectedCallError).
70      *
71      * Params:
72      *     name = Unexpected call name.
73      *     arguments = $(D_PARAM name)'s arguments.
74      *     file = File.
75      *     line = Line number.
76      *     nextInChain = The next error.
77      */
78     this(string name, string[] arguments,
79             string file = __FILE__, size_t line = __LINE__,
80             Throwable nextInChain = null) pure @safe
81     {
82         this.name = name;
83         this.arguments = arguments;
84 
85         const message = format!"Unexpected call: %s(%-(%s, %))"(this.name, this.arguments);
86 
87         super(message, file, line, nextInChain);
88     }
89 }
90 
91 /**
92  * Constructs an $(D_PSYMBOL UnexpectedArgumentError).
93  *
94  * $(D_PARAM arguments) contains both actual and expected arguments. First the
95  * actual arguments are given. The last argument in $(D_PARAM Args) is a
96  * $(D_PSYMBOL Maybe) containing the expected arguments.
97  *
98  * Params:
99  *     T = Object type.
100  *     Args = $(D_PARAM name)'s actual and expected arguments.
101  *     name = Unexpected call name.
102  *     arguments = $(D_PARAM name)'s actual and expected arguments.
103  *     file = File.
104  *     line = Line number.
105  *     nextInChain = The next error.
106  */
107 UnexpectedArgumentError unexpectedArgumentError(T, Args...)(
108         string name, Args arguments,
109         string file = __FILE__, size_t line = __LINE__,
110         Throwable nextInChain = null)
111 {
112     ExpectationPair[] formattedArguments;
113     auto spec = singleSpec("%s");
114     auto writer = appender!(char[])();
115 
116     static foreach (i, Arg; Args[0 .. $ - 1])
117     {{
118         static if (isSomeString!Arg)
119         {
120             const expected = format!"%(%s %)"([arguments[$ - 1].get!i]);
121             const actual = format!"%(%s %)"([arguments[i]]);
122         }
123         else
124         {
125             writer.clear();
126             writer.formatValue(arguments[$ - 1].get!i, spec);
127             const expected = writer[].idup;
128 
129             writer.clear();
130             writer.formatValue(arguments[i], spec);
131             const actual = writer[].idup;
132         }
133 
134         formattedArguments ~= ExpectationPair(actual, expected);
135     }}
136 
137     return new UnexpectedArgumentError(formatName!T(name),
138             formattedArguments, file, line, nextInChain);
139 }
140 
141 /// Pair containing the expected argument and the argument from the actual call.
142 private alias ExpectationPair = Tuple!(string, "actual", string, "expected");
143 
144 /**
145  * Error thrown when a method has been called with arguments that don't match
146  * the expected ones.
147  */
148 final class UnexpectedArgumentError : Error
149 {
150     private string name;
151     private ExpectationPair[] arguments;
152 
153     /**
154      * Constructs an $(D_PSYMBOL UnexpectedArgumentError).
155      *
156      * Params:
157      *     name = Unexpected call name.
158      *     arguments = $(D_PARAM name)'s actual and expected arguments.
159      *     file = File.
160      *     line = Line number.
161      *     nextInChain = The next error.
162      */
163     this(string name, ExpectationPair[] arguments,
164             string file, size_t line, Throwable nextInChain) pure @safe
165     {
166         this.name = name;
167         this.arguments = arguments;
168 
169         auto message = appender!string();
170 
171         message ~= "Expectation failure:\n";
172 
173         auto actual = arguments.map!(argument => argument.actual);
174         auto expected = arguments.map!(argument => argument.expected);
175 
176         message ~= format!"  Expected: %s(%-(%s, %))\n"(this.name, expected);
177         message ~= format!"  but got:  %s(%-(%s, %))"(this.name, actual);
178 
179         super(message.data, file, line, nextInChain);
180     }
181 }
182 
183 /**
184  * Constructs an $(D_PSYMBOL OutOfOrderCallError).
185  *
186  * Params:
187  *     T = Object type.
188  *     Args = $(D_PARAM name)'s arguments.
189  *     name = Unexpected call name.
190  *     arguments = $(D_PARAM name)'s arguments.
191  *     expected = Expected position in the call queue.
192  *     got = Actual position in the call queue.
193  *     file = File.
194  *     line = Line number.
195  *     nextInChain = The next error.
196  */
197 OutOfOrderCallError outOfOrderCallError(T, Args...)(
198         string name, Args arguments,
199         size_t expected, size_t got,
200         string file = __FILE__, size_t line = __LINE__,
201         Throwable nextInChain = null)
202 {
203     return new OutOfOrderCallError(formatName!T(name),
204             formatArguments!Args(arguments), expected, got, file, line, nextInChain);
205 }
206 
207 /**
208  * `OutOfOrderCallError` is thrown only if the checking the call order among
209  * methods of the same class is enabled. The error is thrown if a method is
210  * called earlier than expected.
211  */
212 final class OutOfOrderCallError : Error
213 {
214     private string name;
215     private string[] arguments;
216 
217     /**
218      * Constructs an $(D_PSYMBOL OutOfOrderCallError).
219      *
220      * Params:
221      *     name = Unexpected call name.
222      *     arguments = $(D_PARAM name)'s arguments.
223      *     expected = Expected position in the call queue.
224      *     got = Actual position in the call queue.
225      *     file = File.
226      *     line = Line number.
227      *     nextInChain = The next error.
228      */
229     this(string name, string[] arguments,
230             size_t expected, size_t got,
231             string file, size_t line, Throwable nextInChain) pure @safe
232     {
233         this.name = name;
234         this.arguments = arguments;
235 
236         string  message = format!"%s(%-(%s, %)) called too early:\n"(this.name, this.arguments);
237         message ~= format!"Expected at position: %s, but got: %s"(expected, got);
238 
239         super(message, file, line, nextInChain);
240     }
241 }
242 
243 /**
244  * Constructs an $(D_PSYMBOL ExpectationViolationException).
245  *
246  * Params:
247  *     T = Object type.
248  *     MaybeArgs = $(D_PARAM name)'s argument types.
249  *     name = Call name.
250  *     arguments = $(D_PARAM name)'s arguments.
251  *     file = File.
252  *     line = Line number.
253  *     nextInChain = The next error.
254  */
255 ExpectationViolationException expectationViolationException(T, MaybeArgs)(
256         string name, MaybeArgs arguments,
257         string file = __FILE__, size_t line = __LINE__,
258         Throwable nextInChain = null)
259 {
260     string[] formattedArguments;
261     auto writer = appender!(char[])();
262     auto spec = singleSpec("%s");
263 
264     if (!arguments.isNull)
265     {
266         static foreach (i; 0 .. MaybeArgs.length)
267         {
268             static if (isSomeString!(MaybeArgs.Types[i]))
269             {
270                 formattedArguments ~= format!"%(%s %)"([arguments.get!i]);
271             }
272             else
273             {
274                 writer.clear();
275                 writer.formatValue(arguments.get!i, spec);
276                 formattedArguments ~= writer[].idup;
277             }
278         }
279     }
280     
281     return new ExpectationViolationException(formatName!T(name),
282             formattedArguments, file, line, nextInChain);
283 }
284 
285 /**
286  * Expected the method to be called n times, but called m times,
287  * where m < n.
288  * Same as unexpected call, but with expected arguments instead of the actual ones.
289  * Thrown if a method was expected to be called, but wasn't.
290  */
291 final class ExpectationViolationException : Exception
292 {
293     private string name;
294     private string[] arguments;
295 
296     /**
297      * Constructs an $(D_PSYMBOL ExpectationViolationException).
298      *
299      * Params:
300      *     name = Call name.
301      *     arguments = $(D_PARAM name)'s arguments.
302      *     file = File.
303      *     line = Line number.
304      *     nextInChain = The next error.
305      */
306     this(string name, string[] arguments, string file, size_t line, Throwable nextInChain)
307     {
308         this.name = name;
309         this.arguments = arguments;
310 
311         const message = this.arguments is null
312             ? format!"Expected method not called: %s"(this.name)
313             : format!"Expected method not called: %s(%-(%s, %))"(this.name, this.arguments);
314 
315         super(message, file, line, nextInChain);
316     }
317 }