1 module mocked.mocker;
2 
3 import mocked.error;
4 import mocked.meta;
5 import mocked.option;
6 import std.algorithm;
7 import std.array;
8 import std.container.dlist;
9 import std.conv;
10 import std.format : format;
11 import std.meta;
12 import std.traits;
13 
14 /**
15  * Used to save heterogeneous repositories in a single container and verify
16  * their expectations at the end.
17  */
18 interface Verifiable
19 {
20     /**
21      * Verifies that certain expectation requirements were satisfied.
22      *
23      * Throws: $(D_PSYMBOL ExpectationViolationException) if those issues occur.
24      */
25     void verify();
26 }
27 
28 private enum string mockCode = q{
29     auto expectationTuple = getExpectationTuple(expectationSetup);
30     auto overloads = &expectationTuple.methods[j].overloads[i];
31 
32     const matchAtIndex = (*overloads).countUntil!(call =>
33             (expectationTuple.ordered && call.repeat_ != 0)
34                 || call.compareArguments!Options(arguments)
35     );
36 
37     if (matchAtIndex == -1)
38     {
39         throw unexpectedCallError!(typeof(super), Overload.ParameterTypes)(expectation.name, arguments);
40     }
41     expectationTuple.actualCall += matchAtIndex;
42     auto matchedElement = &overloads.calls[matchAtIndex];
43 
44     if (matchedElement.repeat_ > 0
45             && !matchedElement.compareArguments!Options(arguments))
46     {
47         auto overloadArguments = matchedElement.arguments;
48 
49         overloads.clear();
50 
51         throw unexpectedArgumentError!(typeof(super),
52                 Overload.ParameterTypes, Overload.Arguments)(
53                 expectation.name, arguments, overloadArguments);
54     }
55 
56     if (expectationTuple.ordered
57             && matchedElement.repeat_ == 1
58             && matchedElement.index != ++expectationTuple.actualCall)
59     {
60         throw outOfOrderCallError!(typeof(super), Overload.ParameterTypes)(
61                     expectation.name, arguments,
62                     matchedElement.index,
63                     expectationTuple.actualCall);
64     }
65 
66     scope(exit)
67     {
68         if (matchedElement.repeat_ > 1)
69         {
70             --matchedElement.repeat_;
71         }
72         else if (matchedElement.repeat_ == 1)
73         {
74             overloads.calls = overloads.calls[0 .. matchAtIndex]
75                 ~ overloads.calls[matchAtIndex + 1 .. $];
76         }
77     }
78 
79     static if (is(Overload.Return == void))
80     {
81         if (matchedElement.action_ !is null)
82         {
83             matchedElement.action_(arguments);
84         }
85     }
86     else static if (is(T == interface))
87     {
88         Overload.Return ret = matchedElement.action_ is null
89             ? matchedElement.return_
90             : matchedElement.action_(arguments);
91     }
92     else
93     {
94         Overload.Return ret = matchedElement.passThrough_
95             ? __traits(getMember, super, expectation.name)(arguments)
96             : matchedElement.action_ is null
97             ? matchedElement.return_
98             : matchedElement.action_(arguments);
99     }
100 
101     static if (!is(T == interface) && is(Overload.Return == void))
102     {
103         if (matchedElement.passThrough_)
104         {
105             __traits(getMember, super, expectation.name)(arguments);
106         }
107     }
108 
109     static if (![Overload.qualifiers].canFind("nothrow"))
110     {
111         if (matchedElement.exception !is null)
112         {
113             throw matchedElement.exception;
114         }
115     }
116     static if (!is(Overload.Return == void))
117     {
118         return ret;
119     }
120 };
121 
122 private auto getExpectationTuple(T)(ref T expectationSetup)
123 {
124     alias R = typeof(cast() typeof(expectationSetup.expectationTuple).init)*;
125 
126     return cast(R) &expectationSetup.expectationTuple;
127 }
128 
129 private enum string stubCode = q{
130     auto overloads = getExpectationTuple(expectationSetup)
131         .methods[j].overloads[i];
132     auto match = overloads.find!(call => call.compareArguments!Options(arguments));
133 
134     static if (is(Overload.Return == void))
135     {
136         if (match.front.action_ !is null)
137         {
138             match.front.action_(arguments);
139         }
140     }
141     else static if (is(T == interface))
142     {
143         Overload.Return ret = match.front.action_ is null
144             ? match.front.return_
145             : match.front.action_(arguments);
146     }
147     else
148     {
149         Overload.Return ret = match.front.passThrough_
150             ? __traits(getMember, super, expectation.name)(arguments)
151             : match.front.action_ is null
152             ? match.front.return_
153             : match.front.action_(arguments);
154     }
155 
156     static if (!is(T == interface) && is(Overload.Return == void))
157     {
158         if (match.front.passThrough_)
159         {
160             __traits(getMember, super, expectation.name)(arguments);
161         }
162     }
163 
164     static if (![Overload.qualifiers].canFind("nothrow"))
165     {
166         if (match.front.exception !is null)
167         {
168             throw match.front.exception;
169         }
170     }
171     static if (!is(Overload.Return == void))
172     {
173         return ret;
174     }
175 };
176 
177 /**
178  * Mocker instance with default options.
179  *
180  * See_Also: $(D_PSYMBOL Factory), $(D_PSYMBOL configure).
181  */
182 alias Mocker = Factory!(Options!());
183 
184 /**
185  * Constructs a mocker with options passed as $(D_PARAM Args).
186  *
187  * Params:
188  *     Args = Mocker options.
189  *
190  * See_Also: $(D_PSYMBOL Factory), $(D_PSYMBOL Mocker).
191  */
192 auto configure(Args...)()
193 {
194     return Factory!(Options!Args)();
195 }
196 
197 /**
198  * A class through which one creates mock objects and manages expectations
199  * about calls to their methods.
200  *
201  * Params:
202  *     Options = Mocker $(D_PSYMBOL Options).
203  *
204  * See_Also: $(D_PSYMBOL Mocker), $(D_PSYMBOL CustomMocker).
205  */
206 struct Factory(Options)
207 {
208     private DList!Verifiable repositories;
209 
210     /**
211      * Mocks the type $(D_PARAM T).
212      *
213      * Params:
214      *     T = The type to mock.
215      *     Args = Constructor parameter types.
216      *     args = Constructor arguments.
217      *
218      * Returns: A mock builder.
219      */
220     auto mock(T, Args...)(Args args)
221     {
222         mixin NestedMock!(T, Options, mockCode, Args);
223 
224         auto mock = new Mock(args);
225         auto mocked = new Mocked!T(mock, mock.expectationSetup);
226 
227         this.repositories.insertBack(mocked);
228         return mocked;
229     }
230 
231     /**
232      * Stubs the type $(D_PARAM T).
233      *
234      * Params:
235      *     T = The type to stub.
236      *     Args = Constructor parameter types.
237      *     args = Constructor arguments.
238      *
239      * Returns: A stub builder.
240      */
241     auto stub(T, Args...)(Args args)
242     if (isPolymorphicType!T)
243     {
244         mixin NestedMock!(T, Options, stubCode, Args);
245 
246         auto stub = new Mock(args);
247 
248         return new Stubbed!T(stub, stub.expectationSetup);
249     }
250 
251     /**
252      * Verifies that certain expectation requirements were satisfied.
253      *
254      * Throws: $(D_PSYMBOL ExpectationViolationException) if those issues occur.
255      */
256     void verify()
257     {
258         this.repositories.each!(repository => repository.verify);
259     }
260 
261     ~this()
262     {
263         verify;
264     }
265 }
266 
267 /**
268  * Mock builder.
269  *
270  * Params:
271  *     T = Mocked type.
272  */
273 final class Mocked(T) : Builder!T, Verifiable
274 {
275     /**
276      * Params:
277      *     mock = Mocked object.
278      */
279     this(T mock, ref Repository!T repository)
280     in (mock !is null)
281     {
282         super(mock, repository);
283     }
284 
285     /**
286      * Returns: Repository used to set up expectations.
287      */
288     ref Repository!T expect()
289     {
290         return *this.repository;
291     }
292 
293     /**
294      * Verifies that certain expectation requirements were satisfied.
295      *
296      * Throws: $(D_PSYMBOL ExpectationViolationException) if those issues occur.
297      */
298     void verify()
299     {
300         scope (failure)
301         {
302             static foreach (i, expectation; this.repository.expectationTuple.Methods)
303             {
304                 static foreach (j, Overload; expectation.Overloads)
305                 {
306                     this.expect.expectationTuple.methods[i].overloads[j].clear();
307                 }
308             }
309         }
310 
311         static foreach (i, expectation; this.repository.expectationTuple.Methods)
312         {
313             static foreach (j, Overload; expectation.Overloads)
314             {
315                 if (!this.expect.expectationTuple.methods[i].overloads[j].empty
316                         && this.expect.expectationTuple.methods[i].overloads[j].front.repeat_ > 0)
317                 {
318                     throw expectationViolationException!T(expectation.name,
319                             this.expect.expectationTuple.methods[i].overloads[j].front.arguments);
320                 }
321             }
322         }
323     }
324 
325     /**
326      * Accept expected calls in a random order.
327      *
328      * Returns: $(D_KEYWORD this).
329      */
330     public typeof(this) unordered()
331     {
332         this.repository.expectationTuple.ordered = false;
333         return this;
334     }
335 
336     alias get this;
337 }
338 
339 /**
340  * Stub builder.
341  *
342  * Params:
343  *     T = Mocked type.
344  */
345 final class Stubbed(T) : Builder!T
346 {
347     /**
348      * Params:
349      *     mock = Mocked object.
350      */
351     this(T mock, ref Repository!T repository)
352     in (mock !is null)
353     {
354         super(mock, repository);
355     }
356 
357     /**
358      * Returns: Repository used to set up stubbed methods.
359      */
360     ref Repository!T stub()
361     {
362         return *this.repository;
363     }
364 
365     alias get this;
366 }
367 
368 /**
369  * $(D_PSYMBOL Call) represents a single call of a mocked method.
370  *
371  * Params:
372  *     F = Function represented by this $(D_PSYMBOL Call).
373  */
374 struct Call(alias F)
375 {
376     /// Return type of the mocked method.
377     private alias Return = ReturnType!F;
378 
379     // Parameters accepted by the mocked method.
380     private alias ParameterTypes = .Parameters!F;
381 
382     static if (is(FunctionTypeOf!F PT == __parameters))
383     {
384         /// Arguments passed to set the expectation up.
385         private alias Parameters = PT;
386     }
387     else
388     {
389         static assert(false, typeof(T).stringof ~ " is not a function");
390     }
391 
392     /// Attribute set of the mocked method.
393     private alias qualifiers = AliasSeq!(__traits(getFunctionAttributes, F));
394 
395     private enum concatenatedQualifiers = [qualifiers].join(" ");
396 
397     mixin("alias Action = Return delegate(ParameterTypes) "
398             ~ concatenatedQualifiers ~ ";");
399 
400     private bool passThrough_ = false;
401     private size_t index = 0;
402     private uint repeat_ = 1;
403     private Exception exception;
404     private Action action_;
405 
406     @disable this();
407 
408     /**
409      * Params:
410      *     index = Call is expected to be at position $(D_PARAM index) among all
411      *             calls on the given mock.
412      */
413     public this(size_t index)
414     {
415         this.index = index;
416     }
417 
418     /// Expected arguments if any.
419     private alias Arguments = Maybe!ParameterTypes;
420 
421     /// ditto
422     private Arguments arguments;
423 
424     static if (!is(Return == void))
425     {
426         private Return return_ = Return.init;
427 
428         /**
429          * Set the value to return when method matching this expectation is called on a mock object.
430          *
431          * Params:
432          *     value = the value to return
433          *
434          * Returns: $(D_KEYWORD this).
435          */
436         public ref typeof(this) returns(Return value)
437         {
438             move(value, this.return_);
439 
440             return this;
441         }
442     }
443 
444     /**
445      * Instead of returning or throwing a given value, pass the call through to
446      * the mocked type object.
447      *
448      * This is useful for example for enabling use of mock object in hashmaps
449      * by enabling `toHash` and `opEquals` of your class.
450      *
451      * Returns: $(D_KEYWORD this).
452      */
453     public ref typeof(this) passThrough()
454     {
455         this.passThrough_ = true;
456 
457         return this;
458     }
459 
460     /**
461      * This expectation will match to any number of calls.
462      *
463      * Returns: $(D_KEYWORD this).
464      */
465     public ref typeof(this) repeatAny()
466     {
467         this.repeat_ = 0;
468 
469         return this;
470     }
471 
472     /**
473      * This expectation will match exactly $(D_PARAM times) times.
474      *
475      * Preconditions:
476      *
477      * $(D_CODE times > 0).
478      *
479      * Params:
480      *     times = The number of calls the expectation will match.
481      *
482      * Returns: $(D_KEYWORD this).
483      */
484     public ref typeof(this) repeat(uint times)
485     in (times > 0)
486     {
487         this.repeat_ = times;
488 
489         return this;
490     }
491 
492     public bool compareArguments(Options)(ParameterTypes arguments)
493     {
494         Options options;
495 
496         static foreach (i, argument; arguments)
497         {
498             if (!this.arguments.isNull && !options.equal(this.arguments.get!i, argument))
499             {
500                 return false;
501             }
502         }
503         return true;
504     }
505 
506     /**
507      * When the method which matches this expectation is called, throw the given
508      * exception. If there are any actions specified (via the action method),
509      * they will not be executed.
510      *
511      * Params:
512      *     exception = The exception to throw.
513      *
514      * Returns: $(D_KEYWORD this).
515      */
516     public ref typeof(this) throws(Exception exception)
517     {
518         this.exception = exception;
519 
520         return this;
521     }
522 
523     /**
524      * Creates an exception of type $(D_PARAM E) and throws it when the method
525      * which matches this expectation is called.
526      *
527      * Params:
528      *     E = Exception type to throw.
529      *     msg = The error message to put in the exception if it is thrown.
530      *     file = The source file of the caller.
531      *     line = The line number of the caller.
532      *
533      * Returns: $(D_KEYWORD this).
534      */
535     public ref typeof(this) throws(E : Exception = Exception)(
536             string msg,
537             string file = __FILE__, size_t line = __LINE__)
538     {
539         this.exception = new E(msg, file, line);
540 
541         return this;
542     }
543 
544     /**
545      * When the method which matches this expectation is called execute the
546      * given delegate. The delegate's signature must match the signature
547      * of the called method.
548      *
549      * The called method will return whatever the given delegate returns.
550      *
551      * Params:
552      *     callback = Callable should be called.
553      *
554      * Returns: $(D_KEYWORD this).
555      */
556     public ref typeof(this) action(Action callback)
557     {
558         this.action_ = callback;
559 
560         return this;
561     }
562 }
563 
564 /**
565  * Function overload representation.
566  *
567  * Params:
568  *     F = Function to build this $(D_PSYMBOL Overload) from.
569  */
570 private struct Overload(alias F)
571 {
572     /// Single mocked method call.
573     alias Call = .Call!F;
574 
575     /// Return type of the mocked method.
576     alias Return = Call.Return;
577 
578     // Parameters accepted by the mocked method.
579     alias ParameterTypes = Call.ParameterTypes;
580 
581     /// Arguments passed to set the expectation up.
582     alias Parameters = Call.Parameters;
583 
584     /// Attribute set of the mocked method.
585     alias qualifiers = Call.qualifiers;
586 
587     /// Expected arguments if any.
588     alias Arguments = Call.Arguments;
589 
590     /// Expected calls.
591     Call[] calls;
592 
593     /**
594      * Returns: Whether any expected calls are in the queue.
595      */
596     @property bool empty()
597     {
598         return this.calls.empty;
599     }
600 
601     /**
602      * Returns: The next expected call.
603      */
604     ref Call front()
605     in (!this.calls.empty)
606     {
607         return this.calls.front;
608     }
609 
610     /**
611      * Returns: The last expected call.
612      */
613     ref Call back()
614     in (!this.calls.empty)
615     {
616         return this.calls.back;
617     }
618 
619     /**
620       * Removes the next expected call from the queue.
621       */
622     void popFront()
623     {
624         this.calls.popFront;
625     }
626 
627     /**
628       * Removes the last expected call from the queue.
629       */
630     void popBack()
631     {
632         this.calls.popBack;
633     }
634 
635     /**
636      * Clears the queue.
637      */
638     void clear()
639     {
640         this.calls = [];
641     }
642 
643     /**
644      * Returns: Number of the queue.
645      */
646     @property size_t length()
647     {
648         return this.calls.length;
649     }
650 
651     /**
652      * Returns: $(D_KEYWORD this).
653      */
654     public Overload save()
655     {
656         return this;
657     }
658 
659     /**
660      * Params:
661      *     i = Index.
662      *
663      * Returns: The element at index $(D_PARAM i).
664      */
665     public ref Call opIndex(size_t i)
666     in (i < this.calls.length)
667     {
668         return this.calls[i];
669     }
670 }
671 
672 /**
673  * $(D_PSYMBOL ExpectationSetup) contains all overloads of a single method.
674  *
675  * Params:
676  *     T = Mocked type.
677  *     member = Mocked method name.
678  */
679 private struct ExpectationSetup(T, string member)
680 {
681     enum string name = member;
682 
683     enum bool isVirtualMethod(alias F) = __traits(isVirtualMethod, F);
684     alias VirtualMethods = Filter!(isVirtualMethod, __traits(getOverloads, T, member));
685     alias Overloads = staticMap!(Overload, VirtualMethods);
686 
687     Overloads overloads;
688 }
689 
690 /**
691  * $(D_PSYMBOL Repository) contains all mocked methods of a single class.
692  *
693  * Params:
694  *     T = Mocked type.
695  */
696 private template Repository(T)
697 if (isPolymorphicType!T)
698 {
699     enum isVirtualMethod(string member) =
700         __traits(isVirtualMethod, __traits(getMember, T, member));
701     alias allMembers = __traits(allMembers, T);
702     alias VirtualMethods = Filter!(isVirtualMethod, allMembers);
703 
704     struct Configuration
705     {
706         alias Methods = staticMap!(ApplyLeft!(ExpectationSetup, T), VirtualMethods);
707 
708         Methods methods;
709         private size_t lastCall_;
710         public size_t actualCall;
711         bool ordered = true;
712 
713         public @property size_t lastCall()
714         {
715             return ++this.lastCall_;
716         }
717     }
718 
719     struct Repository
720     {
721         Configuration expectationTuple;
722 
723         static foreach (i, member; VirtualMethods)
724         {
725             static foreach (j, overload; Configuration.Methods[i].Overloads)
726             {
727                 mixin(format!repositoryProperty(member, i, j));
728             }
729 
730             static if (!anySatisfy!(hasNoArguments, Configuration.Methods[i].Overloads))
731             {
732                 mixin(format!repositoryProperty0(member, i));
733             }
734         }
735     }
736 }
737 
738 private enum string repositoryProperty0 = q{
739     ref auto %1$s(Args...)()
740     {
741         static if (Args.length == 0)
742         {
743             enum ptrdiff_t index = 0;
744         }
745         else
746         {
747             enum ptrdiff_t index = matchArguments!(Pack!Args, Configuration.Methods[%2$s].Overloads);
748         }
749         static assert(index >= 0,
750                 "%1$s overload with the given argument types could not be found");
751 
752         this.expectationTuple.methods[%2$s].overloads[index].calls ~=
753             Configuration.Methods[%2$s].Overloads[index].Call(this.expectationTuple.lastCall);
754         return this.expectationTuple.methods[%2$s].overloads[index].back;
755     }
756 };
757 
758 private enum string repositoryProperty = q{
759     ref auto %1$s(overload.Parameters arguments)
760     {
761         this.expectationTuple.methods[%2$s].overloads[%3$s].calls ~=
762             overload.Call(this.expectationTuple.lastCall);
763         this.expectationTuple.methods[%2$s].overloads[%3$s].back.arguments = arguments;
764         return this.expectationTuple.methods[%2$s].overloads[%3$s].back;
765     }
766 };
767 
768 private template matchArguments(Needle, Haystack...)
769 {
770     private template matchArgumentsImpl(ptrdiff_t i, Haystack...)
771     {
772         static if (Haystack.length == 0)
773         {
774             enum ptrdiff_t matchArgumentsImpl = -1;
775         }
776         else static if (__traits(isSame, Needle, Pack!(Haystack[0].ParameterTypes)))
777         {
778             enum ptrdiff_t matchArgumentsImpl = i;
779         }
780         else
781         {
782             enum ptrdiff_t matchArgumentsImpl = matchArgumentsImpl!(i + 1, Haystack[1 .. $]);
783         }
784     }
785     enum ptrdiff_t matchArguments = matchArgumentsImpl!(0, Haystack);
786 }
787 
788 private enum bool hasNoArguments(T) = T.Parameters.length == 0;
789 
790 /**
791  * Mock builder used by the mocks and stubs.
792  *
793  * Params:
794  *     T = Mocked type.
795  */
796 abstract class Builder(T)
797 {
798     protected Repository!T* repository;
799 
800     /// Mocked object instance.
801     protected T mock;
802 
803     invariant(mock !is null);
804 
805     /**
806      * Params:
807      *     mock = Mocked object.
808      */
809     this(T mock, ref Repository!T repository)
810     in (mock !is null)
811     {
812         this.mock = mock;
813         this.repository = &repository;
814     }
815 
816     /**
817      * Returns: Mocked object.
818      */
819     T get() @nogc nothrow pure @safe
820     {
821         return this.mock;
822     }
823 
824     static if (is(T == class))
825     {
826         /**
827          * Forward default object methods to the mock.
828          */
829         override size_t toHash()
830         {
831             return get().toHash();
832         }
833 
834         /// ditto
835         override string toString()
836         {
837             return get().toString();
838         }
839 
840         /// ditto
841         override int opCmp(Object o)
842         {
843             return get().opCmp(o);
844         }
845 
846         /// ditto
847         override bool opEquals(Object o)
848         {
849             return get().opEquals(o);
850         }
851     }
852 }
853 
854 private mixin template NestedMock(T, Options, string overloadingCode, Args...)
855 {
856     final class Mock : T
857     {
858         import std.string : join;
859 
860         private Repository!T expectationSetup;
861 
862         static if (__traits(hasMember, T, "__ctor") && Args.length > 0)
863         {
864             this(ref Args args)
865             {
866                 super(args);
867             }
868         }
869         else static if (__traits(hasMember, T, "__ctor"))
870         {
871             this()
872             {
873                 super(Parameters!(T.__ctor).init);
874             }
875         }
876 
877         static foreach (j, expectation; expectationSetup.expectationTuple.Methods)
878         {
879             static foreach (i, Overload; expectation.Overloads)
880             {
881                 mixin(["override", Overload.qualifiers, "Overload.Return", expectation.name].join(" ") ~ q{
882                     (Overload.ParameterTypes arguments)
883                     {
884                         mixin(overloadingCode);
885                     }
886                 });
887             }
888         }
889     }
890 }