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 }