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 }