1 module mocked.builder;
2 
3 import mocked.error;
4 import mocked.meta;
5 import std.array;
6 import std.format;
7 import std.meta;
8 import std.traits;
9 
10 interface Verifiable
11 {
12     void verify();
13 }
14 
15 final class Mocked(T) : Verifiable
16 {
17     T mock;
18     Repository!T* repository;
19 
20     this(T mock, ref Repository!T repository)
21     {
22         this.mock = mock;
23         this.repository = &repository;
24     }
25 
26     ref Repository!T expect()
27     {
28         return *this.repository;
29     }
30 
31     ref T get() @nogc nothrow pure @safe
32     {
33         return this.mock;
34     }
35 
36     /**
37      * Verifies that certain expectation requirements were satisfied.
38      *
39      * Throws: $(D_PSYMBOL ExpectationViolationException) if those issues occur.
40      */
41     void verify()
42     {
43         scope (failure)
44         {
45             static foreach (i, expectation; this.repository.ExpectationTuple)
46             {
47                 static foreach (j, Overload; expectation.Overloads)
48                 {
49                     this.repository.expectationTuple[i].overloads[j].clear();
50                 }
51             }
52         }
53 
54         static foreach (i, expectation; this.repository.ExpectationTuple)
55         {
56             static foreach (j, Overload; expectation.Overloads)
57             {
58                 if (!this.repository.expectationTuple[i].overloads[j].empty
59                         && this.repository.expectationTuple[i].overloads[j].front.repeat_ > 0)
60                 {
61                     throw expectationViolationException!T(expectation.name,
62                             this.repository.expectationTuple[i].overloads[j].front.arguments);
63                 }
64             }
65         }
66     }
67 
68     static if (is(T == class))
69     {
70         override size_t toHash()
71         {
72             return get().toHash();
73         }
74 
75         override string toString()
76         {
77             return get().toString();
78         }
79 
80         override int opCmp(Object o)
81         {
82             return get().opCmp(o);
83         }
84 
85         override bool opEquals(Object o)
86         {
87             return get().opEquals(o);
88         }
89     }
90 
91     alias get this;
92 }
93 
94 /**
95  * $(D_PSYMBOL Call) represents a single call of a mocked method.
96  *
97  * Params:
98  *     F = Function represented by this $(D_PSYMBOL Call).
99  */
100 struct Call(alias F)
101 {
102     /// Return type of the mocked method.
103     alias Return = ReturnType!F;
104 
105     // Parameters accepted by the mocked method.
106     alias ParameterTypes = .Parameters!F;
107 
108     static if (is(FunctionTypeOf!F PT == __parameters))
109     {
110         /// Arguments passed to set the expectation up.
111         alias Parameters = PT;
112     }
113     else
114     {
115         static assert(false, typeof(T).stringof ~ " is not a function");
116     }
117 
118     /// Attribute set of the mocked method.
119     alias qualifiers = AliasSeq!(__traits(getFunctionAttributes, F));
120 
121     private alias concatenatedQualifiers = unwords!qualifiers;
122 
123     mixin("alias CustomArgsComparator = bool delegate(ParameterTypes) "
124             ~ concatenatedQualifiers ~ ";");
125     mixin("alias Action = Return delegate(ParameterTypes) "
126             ~ concatenatedQualifiers ~ ";");
127 
128     bool passThrough_ = false;
129     bool ignoreArgs_ = false;
130     uint repeat_ = 1;
131     Exception exception;
132     CustomArgsComparator customArgsComparator_;
133     Action action_;
134 
135     /// Expected arguments if any.
136     alias Arguments = Maybe!ParameterTypes;
137 
138     /// ditto
139     Arguments arguments;
140 
141     static if (!is(Return == void))
142     {
143         Return return_ = Return.init;
144 
145         /**
146          * Set the value to return when method matching this expectation is called on a mock object.
147          *
148          * Params:
149          *     value = the value to return
150          *
151          * Returns: $(D_KEYWORD this).
152          */
153         public ref typeof(this) returns(Return value)
154         {
155             this.return_ = value;
156 
157             return this;
158         }
159     }
160 
161     /**
162      * Instead of returning or throwing a given value, pass the call through to
163      * the mocked type object.
164      *
165      * This is useful for example for enabling use of mock object in hashmaps
166      * by enabling `toHash` and `opEquals` of your class.
167      *
168      * Returns: $(D_KEYWORD this).
169      */
170     public ref typeof(this) passThrough()
171     {
172         this.passThrough_ = true;
173 
174         return this;
175     }
176 
177     deprecated("Just skip the argument setup")
178     public ref typeof(this) ignoreArgs()
179     {
180         this.ignoreArgs_ = true;
181 
182         return this;
183     }
184 
185     /**
186      * This expectation will match to any number of calls.
187      *
188      * Returns: $(D_KEYWORD this).
189      */
190     public ref typeof(this) repeatAny()
191     {
192         this.repeat_ = 0;
193 
194         return this;
195     }
196 
197     /**
198      * This expectation will match exactly $(D_PARAM times) times.
199      *
200      * Preconditions:
201      *
202      * $(D_CODE times > 0).
203      *
204      * Params:
205      *     times = The number of calls the expectation will match.
206      *
207      * Returns: $(D_KEYWORD this).
208      */
209     public ref typeof(this) repeat(uint times)
210     in (times > 0)
211     {
212         this.repeat_ = times;
213 
214         return this;
215     }
216 
217     public bool compareArguments(alias options)(ParameterTypes arguments)
218     {
219         if (this.customArgsComparator_ !is null)
220         {
221             return this.customArgsComparator_(arguments);
222         }
223         static foreach (i, argument; arguments)
224         {
225             if (!this.arguments.isNull && !options.equal(this.arguments.get!i, argument))
226             {
227                 return false;
228             }
229         }
230         return true;
231     }
232 
233     /**
234      * Allow providing custom argument comparator for matching calls to this expectation.
235      *
236      * Params:
237      *     comaprator = The functions used to compare the arguments.
238      *
239      * Returns: $(D_KEYWORD this).
240      */
241     deprecated("Use mocked.Comparator instead")
242     public ref typeof(this) customArgsComparator(CustomArgsComparator comparator)
243     in (comparator !is null)
244     {
245         this.customArgsComparator_ = comparator;
246 
247         return this;
248     }
249 
250     /**
251      * When the method which matches this expectation is called, throw the given
252      * exception. If there are any actions specified (via the action method),
253      * they will not be executed.
254      *
255      * Params:
256      *     exception = The exception to throw.
257      *
258      * Returns: $(D_KEYWORD this).
259      */
260     public ref typeof(this) throws(Exception exception)
261     {
262         this.exception = exception;
263 
264         return this;
265     }
266 
267     /**
268      * When the method which matches this expectation is called execute the
269      * given delegate. The delegate's signature must match the signature
270      * of the called method.
271      *
272      * The called method will return whatever the given delegate returns.
273      *
274      * Params:
275      *     callback = Callable should be called.
276      *
277      * Returns: $(D_KEYWORD this).
278      */
279     public ref typeof(this) action(Action callback)
280     {
281         this.action_ = callback;
282 
283         return this;
284     }
285 }
286 
287 /**
288  * Params:
289  *     F = Function to build this $(D_PSYMBOL Overload) from.
290  */
291 struct Overload(alias F)
292 {
293     /// Single mocked method call.
294     alias Call = .Call!F;
295 
296     /// Return type of the mocked method.
297     alias Return = Call.Return;
298 
299     // Parameters accepted by the mocked method.
300     alias ParameterTypes = Call.ParameterTypes;
301 
302     /// Arguments passed to set the expectation up.
303     alias Parameters = Call.Parameters;
304 
305     /// Attribute set of the mocked method.
306     alias qualifiers = Call.qualifiers;
307 
308     /// Expected arguments if any.
309     alias Arguments = Call.Arguments;
310 
311     /// Expected calls.
312     Call[] calls;
313 
314     /**
315      * Returns: Whether any expected calls are in the queue.
316      */
317     public @property bool empty()
318     {
319         return this.calls.empty;
320     }
321 
322     /**
323      * Returns: The next expected call.
324      */
325     public ref Call front()
326     in (!this.calls.empty)
327     {
328         return this.calls.front;
329     }
330 
331     public ref Call back()
332     in (!this.calls.empty)
333     {
334         return this.calls.back;
335     }
336 
337     /**
338       * Removes the next expected call from the queue.
339       */
340     public void popFront()
341     {
342         this.calls.popFront;
343     }
344 
345     public void popBack()
346     {
347         this.calls.popBack;
348     }
349 
350     /**
351      * Clears the queue.
352      */
353     public void clear()
354     {
355         this.calls = [];
356     }
357 }
358 
359 /**
360  * $(D_PSYMBOL ExpectationSetup) contains all overloads of a single method.
361  *
362  * Params:
363  *     T = Mocked type.
364  *     member = Mocked method name.
365  */
366 struct ExpectationSetup(T, string member)
367 {
368     enum string name = member;
369 
370     alias Overloads = staticMap!(Overload, __traits(getOverloads, T, member));
371 
372     Overloads overloads;
373 }
374 
375 /**
376  * $(D_PSYMBOL Repository) contains all mocked methods of a single class.
377  *
378  * Params:
379  *     T = Mocked type.
380  */
381 struct Repository(T)
382 if (isPolymorphicType!T)
383 {
384     private alias VirtualMethods = Filter!(ApplyLeft!(isVirtualMethod, T), __traits(allMembers, T));
385 
386     alias ExpectationTuple = staticMap!(ApplyLeft!(ExpectationSetup, T), VirtualMethods);
387     ExpectationTuple expectationTuple;
388 
389     static foreach (i, member; VirtualMethods)
390     {
391         static foreach (j, overload; ExpectationTuple[i].Overloads)
392         {
393             mixin(format!repositoryProperty(member, i, j));
394         }
395 
396         static if (!anySatisfy!(hasNoArguments, ExpectationTuple[i].Overloads))
397         {
398             mixin(format!repositoryProperty0(member, i));
399         }
400     }
401 }
402 
403 private enum string repositoryProperty0 = q{
404     ref auto %1$s(Args...)()
405     {
406         static if (Args.length == 0)
407         {
408             enum ptrdiff_t index = 0;
409         }
410         else
411         {
412             enum ptrdiff_t index = matchArguments!(Pack!Args, ExpectationTuple[%2$s].Overloads);
413         }
414         static assert(index >= 0,
415                 "%1$s overload with the given argument types could not be found");
416 
417         ExpectationTuple[%2$s].Overloads[index].Call call;
418         this.expectationTuple[%2$s].overloads[index].calls ~= call;
419         return this.expectationTuple[%2$s].overloads[index].back;
420     }
421 };
422 
423 private enum string repositoryProperty = q{
424     ref auto %1$s(overload.Parameters arguments)
425     {
426         overload.Call call;
427         call.arguments = arguments;
428         this.expectationTuple[%2$s].overloads[%3$s].calls ~= call;
429         return this.expectationTuple[%2$s].overloads[%3$s].back;
430     }
431 };
432 
433 private template matchArguments(Needle, Haystack...)
434 {
435     private template matchArgumentsImpl(ptrdiff_t i, Haystack...)
436     {
437         static if (Haystack.length == 0)
438         {
439             enum ptrdiff_t matchArgumentsImpl = -1;
440         }
441         else static if (__traits(isSame, Needle, Pack!(Haystack[0].ParameterTypes)))
442         {
443             enum ptrdiff_t matchArgumentsImpl = i;
444         }
445         else
446         {
447             enum ptrdiff_t matchArgumentsImpl = matchArgumentsImpl!(i + 1, Haystack[1 .. $]);
448         }
449     }
450     enum ptrdiff_t matchArguments = matchArgumentsImpl!(0, Haystack);
451 }
452 
453 private enum bool hasNoArguments(T) = T.Parameters.length == 0;
454 private enum isVirtualMethod(T, string member) =
455     __traits(isVirtualMethod, __traits(getMember, T, member));