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));