1 module dmocks.repository;
2 
3 import dmocks.action;
4 import std.algorithm;
5 import std.array;
6 import dmocks.call;
7 import dmocks.expectation;
8 import dmocks.model;
9 import dmocks.util;
10 import std.traits;
11 
12 package:
13 
14 class MockRepository
15 {
16     // TODO: split this up somehow!
17     private bool _allowDefaults = false;
18     private bool _recording = true;
19     private bool _ordered = false;
20     private bool _allowUnexpected = false;
21     private bool _fallback = false; // next recorded call is a fallback call (calls that should always succeed)
22 
23     private Call[] _unexpectedCalls = [];
24     private GroupExpectation _rootGroupExpectation;
25     private CallExpectation _lastRecordedCallExpectation; // stores last call added to _lastGroupExpectation
26     private Call _lastRecordedCall; // stores last call with which _lastRecordedCallExpectation was created
27     private GroupExpectation _lastGroupExpectation; // stores last group added to _rootGroupExpectation
28 
29     private GroupExpectation _fallbackGroupExpectation; // stores fallback methods, like toString and opEquals
30     private Call _lastRecordedFallbackCall; // like _lastRecordedCall for fallback
31     private CallExpectation _lastRecordedFallbackCallExpectation; // like _lastRecordedCallExpectation for fallback
32 
33     private void CheckLastCallSetup()
34     {
35         if (_allowDefaults || _lastRecordedCallExpectation is null || _lastRecordedCallExpectation.action.hasAction)
36         {
37             return;
38         }
39 
40         throw new MocksSetupException(
41                 "Last expectation: if you do not specify the AllowDefaults option, you need to return a value, throw an exception, execute a delegate, or pass through to base function. The expectation is: " ~ _lastRecordedCallExpectation
42                 .toString());
43     }
44 
45     this()
46     {
47         _rootGroupExpectation = createGroupExpectation(false);
48         _fallbackGroupExpectation = createGroupExpectation(false);
49         Ordered(false);
50     }
51 
52     void AllowDefaults(bool value)
53     {
54         _allowDefaults = value;
55     }
56 
57     void AllowUnexpected(bool value)
58     {
59         _allowUnexpected = value;
60     }
61 
62     bool AllowUnexpected()
63     {
64         return _allowUnexpected;
65     }
66 
67     bool Recording()
68     {
69         return _recording;
70     }
71 
72     bool Ordered()
73     {
74         return _ordered;
75     }
76 
77     void Replay()
78     {
79         CheckLastCallSetup();
80         _recording = false;
81     }
82 
83     void BackToRecord()
84     {
85         _recording = true;
86     }
87 
88     void Ordered(bool value)
89     {
90         debugLog("SETTING ORDERED: %s", value);
91         _ordered = value;
92         _lastGroupExpectation = createGroupExpectation(_ordered);
93         _rootGroupExpectation.addExpectation(_lastGroupExpectation);
94     }
95 
96     void Record(CallExpectation expectation, Call call)
97     {
98         CheckLastCallSetup();
99         if (_fallback)
100         {
101             _fallbackGroupExpectation.addExpectation(expectation);
102             _lastRecordedFallbackCallExpectation = expectation;
103             _lastRecordedFallbackCall = call;
104             _fallback = false;
105             return;
106         }
107         _lastGroupExpectation.addExpectation(expectation);
108         _lastRecordedCallExpectation = expectation;
109         _lastRecordedCall = call;
110     }
111 
112     @trusted public auto MethodCall(alias METHOD, ARGS...)(MockId mocked, string name, ARGS args)
113     {
114         alias ReturnType!(FunctionTypeOf!(METHOD)) TReturn;
115 
116         auto call = createCall!METHOD(mocked, name, args);
117         debugLog("checking Recording...");
118         if (Recording)
119         {
120             auto expectation = createExpectation!(METHOD)(mocked, name, args);
121 
122             Record(expectation, call);
123             return ReturnOrPass!(TReturn).init;
124         }
125 
126         debugLog("checking for matching expectation...");
127         auto expectation = Match(call);
128 
129         debugLog("checking if expectation is null...");
130         if (expectation is null)
131         {
132             if (AllowUnexpected())
133                 return ReturnOrPass!(TReturn).init;
134             throw new ExpectationViolationError(buildExpectationError);
135         }
136 
137         auto rope = expectation.action.getActor().act!(TReturn, ARGS)(args);
138         debugLog("returning...");
139         return rope;
140     }
141 
142     CallExpectation Match(Call call)
143     {
144         auto exp = _rootGroupExpectation.match(call);
145         if (exp is null)
146         {
147             exp = _fallbackGroupExpectation.match(call);
148             if (exp is null)
149             {
150                 _unexpectedCalls ~= call;
151             }
152         }
153         return exp;
154     }
155 
156     CallExpectation LastRecordedCallExpectation()
157     {
158         return _lastRecordedCallExpectation;
159     }
160 
161     Call LastRecordedCall()
162     {
163         return _lastRecordedCall;
164     }
165 
166     package CallExpectation LastRecordedFallbackCallExpectation()
167     {
168         return _lastRecordedFallbackCallExpectation;
169     }
170 
171     package Call LastRecordedFallbackCall()
172     {
173         return _lastRecordedFallbackCall;
174     }
175 
176     void Verify(bool checkUnmatchedExpectations, bool checkUnexpectedCalls)
177     {
178         auto expectationError = buildExpectationError(checkUnmatchedExpectations, checkUnexpectedCalls);
179 
180         if (!expectationError.empty)
181         {
182             throw new ExpectationViolationException(expectationError);
183         }
184     }
185 
186     string buildExpectationError(bool checkUnmatchedExpectations = true, bool checkUnexpectedCalls = true)
187     {
188         auto expectationError = appender!string;
189         auto unexpectedCalls = _unexpectedCalls;
190         Expectation rootGroupExpectation = _rootGroupExpectation;
191 
192         void walkExpectations(ref Expectation expectation)
193         {
194             if (auto groupExpectation = cast(GroupExpectation) expectation)
195             {
196                 auto expectations = groupExpectation.expectations.dup;
197 
198                 foreach (ref subExpectation; expectations)
199                 {
200                     walkExpectations(subExpectation);
201                 }
202                 expectations = expectations.remove!(a => a is null);
203 
204                 if (expectations.empty)
205                 {
206                     expectation = null;
207                     return;
208                 }
209                 auto newExpectation = new GroupExpectation;
210 
211                 newExpectation.expectations = expectations;
212                 newExpectation.ordered = groupExpectation.ordered;
213                 newExpectation.repeatInterval = groupExpectation.repeatInterval;
214                 expectation = newExpectation;
215                 return;
216             }
217             if (auto callExpectation = cast(CallExpectation) expectation)
218             {
219                 if (callExpectation.satisfied) return;
220 
221                 alias pred = unexpectedCall => callExpectation.name.matches(unexpectedCall.name);
222 
223                 unexpectedCalls.filter!pred.each!((unexpectedCall) {
224                     // name matches: assume they are meant to match up, generate parameter diff
225                     expectationError ~= "\n";
226                     expectationError ~= CallExpectationDiff(unexpectedCall, callExpectation);
227                     expectation = null;
228                 });
229                 unexpectedCalls = unexpectedCalls.remove!pred;
230                 return;
231             }
232             assert(false, "unknown subclass of expectation");
233         }
234         if (checkUnmatchedExpectations && checkUnexpectedCalls)
235         {
236             walkExpectations(rootGroupExpectation);
237         }
238         if (checkUnmatchedExpectations && rootGroupExpectation && !rootGroupExpectation.satisfied)
239         {
240             expectationError ~= "\n";
241             expectationError ~= rootGroupExpectation.toString();
242         }
243         if (checkUnexpectedCalls && !unexpectedCalls.empty)
244         {
245             expectationError ~= "\n";
246             expectationError ~= UnexpectedCallsReport(unexpectedCalls);
247         }
248 
249         return expectationError.data;
250     }
251 
252     package void RecordFallback(T)(lazy T methodCall)
253     {
254         _fallback = true;
255         methodCall();
256         assert(_fallback == false);
257         return;
258     }
259 
260     static string UnexpectedCallsReport(Call[] unexpectedCalls)
261     {
262         import std.array;
263 
264         auto apndr = appender!(string);
265         apndr.put("Unexpected calls(calls):\n");
266         foreach (Call ev; unexpectedCalls)
267         {
268             apndr.put(ev.toString());
269             apndr.put("\n");
270         }
271         return apndr.data;
272     }
273 
274     static string CallExpectationDiff(Call call, CallExpectation callExpectation)
275     {
276         import std.array;
277         auto apndr = appender!(string);
278         apndr.put("Mismatched call:\n  ");
279         apndr.put(callExpectation.diffToString(call));
280         return apndr.data;
281     }
282 
283     @("repository record/replay")
284     unittest
285     {
286         MockRepository r = new MockRepository();
287         assert(r.Recording());
288         r.Replay();
289         assert(!r.Recording());
290         r.BackToRecord();
291         assert(r.Recording());
292     }
293 
294     @("test for correctly formulated template")
295     unittest
296     {
297         class A
298         {
299             public void a()
300             {
301             }
302         }
303 
304         auto a = new A;
305         auto c = new MockRepository();
306         auto mid = new MockId;
307         //c.Call!(a.a)(mid, "a");
308         static assert(__traits(compiles, c.MethodCall!(a.a)(mid, "a")));
309     }
310 }