From d225e2cd52239bc62f4f8bfe852a9660f88ef184 Mon Sep 17 00:00:00 2001 From: stakx Date: Fri, 24 Aug 2018 15:37:14 +0200 Subject: [PATCH 1/4] Extract invocation matching into `InvocationShape` This moves all logic from `MethodCall` that is related to invocation matching into a dedicated new type, `InvocationShape`. This seems use- ful mainly because of two reasons: 1. The invocation matching becomes more easily testable once it is better isolated. 2. Verification methods such as `mock.Verify(expression)` no longer need to instantiate a transient `MethodCall` (which at its heart really represents a setup) in order to match invocations. They will be able to use the much lighter-weight `InvocationShape` in- stead. --- src/Moq/InvocationShape.cs | 132 +++++++++++++++++++++++++++++++++++++ src/Moq/MethodCall.cs | 81 +++-------------------- 2 files changed, 140 insertions(+), 73 deletions(-) create mode 100644 src/Moq/InvocationShape.cs diff --git a/src/Moq/InvocationShape.cs b/src/Moq/InvocationShape.cs new file mode 100644 index 000000000..3aa1f3010 --- /dev/null +++ b/src/Moq/InvocationShape.cs @@ -0,0 +1,132 @@ +//Copyright (c) 2007. Clarius Consulting, Manas Technology Solutions, InSTEDD +//https://github.com/moq/moq4 +//All rights reserved. +// +//Redistribution and use in source and binary forms, +//with or without modification, are permitted provided +//that the following conditions are met: +// +// * Redistributions of source code must retain the +// above copyright notice, this list of conditions and +// the following disclaimer. +// +// * Redistributions in binary form must reproduce +// the above copyright notice, this list of conditions +// and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// * Neither the name of Clarius Consulting, Manas Technology Solutions or InSTEDD nor the +// names of its contributors may be used to endorse +// or promote products derived from this software +// without specific prior written permission. +// +//THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +//CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +//INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +//MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +//DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +//CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +//SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +//BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +//SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +//INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +//WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +//NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +//OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +//SUCH DAMAGE. +// +//[This is the BSD license, see +// http://www.opensource.org/licenses/bsd-license.php] + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace Moq +{ + /// + /// Describes the "shape" of an invocation against which concrete s can be matched. + /// + internal readonly struct InvocationShape + { + private readonly MethodInfo method; + private readonly IMatcher[] argumentMatchers; + + public InvocationShape(MethodInfo method, IMatcher[] argumentMatchers) + { + this.method = method; + this.argumentMatchers = argumentMatchers; + } + + public IReadOnlyList ArgumentMatchers => this.argumentMatchers; + + public MethodInfo Method => this.method; + + public bool IsMatch(Invocation invocation) + { + var arguments = invocation.Arguments; + if (this.argumentMatchers.Length != arguments.Length) + { + return false; + } + + if (this.IsEqualMethodOrOverride(invocation.Method)) + { + for (int i = 0, n = this.argumentMatchers.Length; i < n; ++i) + { + if (this.argumentMatchers[i].Matches(arguments[i]) == false) + { + return false; + } + } + + return true; + } + + return false; + } + + private bool IsEqualMethodOrOverride(MethodInfo invocationMethod) + { + var method = this.method; + + if (invocationMethod == method) + { + return true; + } + + if (method.DeclaringType.IsAssignableFrom(invocationMethod.DeclaringType)) + { + if (!method.Name.Equals(invocationMethod.Name, StringComparison.Ordinal) || + method.ReturnType != invocationMethod.ReturnType || + !method.IsGenericMethod && + !invocationMethod.HasSameParameterTypesAs(method)) + { + return false; + } + + if (method.IsGenericMethod && !invocationMethod.GetGenericArguments().SequenceEqual(method.GetGenericArguments(), AssignmentCompatibilityTypeComparer.Instance)) + { + return false; + } + + return true; + } + + return false; + } + + private sealed class AssignmentCompatibilityTypeComparer : IEqualityComparer + { + public static readonly AssignmentCompatibilityTypeComparer Instance = new AssignmentCompatibilityTypeComparer(); + + public bool Equals(Type x, Type y) + { + return y.IsAssignableFrom(x); + } + + int IEqualityComparer.GetHashCode(Type obj) => throw new NotSupportedException(); + } + } +} diff --git a/src/Moq/MethodCall.cs b/src/Moq/MethodCall.cs index ed32b3096..152c507d5 100644 --- a/src/Moq/MethodCall.cs +++ b/src/Moq/MethodCall.cs @@ -82,13 +82,12 @@ public IVerifies Raises(Action eventExpression, params object[] args) internal partial class MethodCall : ICallbackResult, IVerifies, IThrowsResult { - private IMatcher[] argumentMatchers; private Action callbackResponse; private int callCount; private Condition condition; + private InvocationShape expectation; private int? expectedMaxCallCount; private string failMessage; - private MethodInfo method; private Mock mock; #if !NETCORE private string originalCallerFilePath; @@ -108,23 +107,20 @@ internal partial class MethodCall : ICallbackResult, IVerifies, IThrowsResult public MethodCall(Mock mock, Condition condition, LambdaExpression originalExpression, MethodInfo method, IMatcher[] argumentMatchers) { this.condition = condition; - this.method = method; + this.expectation = new InvocationShape(method, argumentMatchers); this.mock = mock; this.originalExpression = originalExpression; - - this.argumentMatchers = argumentMatchers; this.outValues = null; } public MethodCall(Mock mock, Condition condition, LambdaExpression originalExpression, MethodInfo method, IReadOnlyList arguments) { + var parameters = method.GetParameters(); + this.condition = condition; - this.method = method; + this.expectation = new InvocationShape(method, GetArgumentMatchers(arguments, parameters)); this.mock = mock; this.originalExpression = originalExpression; - - var parameters = method.GetParameters(); - this.argumentMatchers = GetArgumentMatchers(arguments, parameters); this.outValues = GetOutValues(arguments, parameters); this.SetFileInfo(); @@ -179,7 +175,7 @@ public string FailMessage set => this.failMessage = value; } - public MethodInfo Method => this.method; + public MethodInfo Method => this.expectation.Method; public Mock Mock => this.mock; @@ -241,26 +237,7 @@ public void SetOutParameters(Invocation invocation) public bool Matches(Invocation invocation) { - var arguments = invocation.Arguments; - if (this.argumentMatchers.Length != arguments.Length) - { - return false; - } - - if (this.IsEqualMethodOrOverride(invocation.Method)) - { - for (int i = 0, n = this.argumentMatchers.Length; i < n; ++i) - { - if (this.argumentMatchers[i].Matches(arguments[i]) == false) - { - return false; - } - } - - return condition == null || condition.IsTrue; - } - - return false; + return this.expectation.IsMatch(invocation) && (condition == null || condition.IsTrue); } public void EvaluatedSuccessfully() @@ -336,7 +313,7 @@ protected virtual void SetCallbackWithoutArguments(Action callback) protected virtual void SetCallbackWithArguments(Delegate callback) { - var expectedParams = this.method.GetParameters(); + var expectedParams = this.Method.GetParameters(); var actualParams = callback.GetMethodInfo().GetParameters(); if (!callback.HasCompatibleParameterList(expectedParams)) @@ -368,36 +345,6 @@ public void Verifiable(string failMessage) this.failMessage = failMessage; } - private bool IsEqualMethodOrOverride(MethodInfo invocationMethod) - { - var method = this.method; - - if (invocationMethod == method) - { - return true; - } - - if (method.DeclaringType.IsAssignableFrom(invocationMethod.DeclaringType)) - { - if (!method.Name.Equals(invocationMethod.Name, StringComparison.Ordinal) || - method.ReturnType != invocationMethod.ReturnType || - !method.IsGenericMethod && - !invocationMethod.HasSameParameterTypesAs(method)) - { - return false; - } - - if (method.IsGenericMethod && !invocationMethod.GetGenericArguments().SequenceEqual(method.GetGenericArguments(), AssignmentCompatibilityTypeComparer.Instance)) - { - return false; - } - - return true; - } - - return false; - } - public IVerifies AtMostOnce() => this.AtMost(1); public IVerifies AtMost(int callCount) @@ -488,18 +435,6 @@ public string Format() return builder.ToString(); } - private sealed class AssignmentCompatibilityTypeComparer : IEqualityComparer - { - public static AssignmentCompatibilityTypeComparer Instance { get; } = new AssignmentCompatibilityTypeComparer(); - - public bool Equals(Type x, Type y) - { - return y.IsAssignableFrom(x); - } - - int IEqualityComparer.GetHashCode(Type obj) => throw new NotSupportedException(); - } - private sealed class RaiseEventResponse { private Mock mock; From 70c571436bd955af9f927dbe260cf6773558cc20 Mon Sep 17 00:00:00 2001 From: stakx Date: Fri, 24 Aug 2018 15:54:28 +0200 Subject: [PATCH 2/4] Move argument -> argument matcher transform If we want to be able to use `InvocationShape` in verification methods as well (as described in the previous commit message), it's likely that this type will have to be able to accept arguments, instead of argument matchers. Therefore, extract that logic from `MethodCall` and move it into `InvocationShape'. --- src/Moq/InvocationShape.cs | 23 +++++++++++++++++++++++ src/Moq/MethodCall.cs | 21 ++------------------- 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/src/Moq/InvocationShape.cs b/src/Moq/InvocationShape.cs index 3aa1f3010..11ae8164f 100644 --- a/src/Moq/InvocationShape.cs +++ b/src/Moq/InvocationShape.cs @@ -40,7 +40,9 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; +using System.Linq.Expressions; using System.Reflection; namespace Moq @@ -53,6 +55,12 @@ internal readonly struct InvocationShape private readonly MethodInfo method; private readonly IMatcher[] argumentMatchers; + public InvocationShape(MethodInfo method, IReadOnlyList arguments) + { + this.method = method; + this.argumentMatchers = GetArgumentMatchers(arguments, method.GetParameters()); + } + public InvocationShape(MethodInfo method, IMatcher[] argumentMatchers) { this.method = method; @@ -117,6 +125,21 @@ private bool IsEqualMethodOrOverride(MethodInfo invocationMethod) return false; } + private static IMatcher[] GetArgumentMatchers(IReadOnlyList arguments, ParameterInfo[] parameters) + { + Debug.Assert(arguments != null); + Debug.Assert(parameters != null); + Debug.Assert(arguments.Count == parameters.Length); + + var n = parameters.Length; + var argumentMatchers = new IMatcher[n]; + for (int i = 0; i < n; ++i) + { + argumentMatchers[i] = MatcherFactory.CreateMatcher(arguments[i], parameters[i]); + } + return argumentMatchers; + } + private sealed class AssignmentCompatibilityTypeComparer : IEqualityComparer { public static readonly AssignmentCompatibilityTypeComparer Instance = new AssignmentCompatibilityTypeComparer(); diff --git a/src/Moq/MethodCall.cs b/src/Moq/MethodCall.cs index 152c507d5..334734721 100644 --- a/src/Moq/MethodCall.cs +++ b/src/Moq/MethodCall.cs @@ -115,32 +115,15 @@ public MethodCall(Mock mock, Condition condition, LambdaExpression originalExpre public MethodCall(Mock mock, Condition condition, LambdaExpression originalExpression, MethodInfo method, IReadOnlyList arguments) { - var parameters = method.GetParameters(); - this.condition = condition; - this.expectation = new InvocationShape(method, GetArgumentMatchers(arguments, parameters)); + this.expectation = new InvocationShape(method, arguments); this.mock = mock; this.originalExpression = originalExpression; - this.outValues = GetOutValues(arguments, parameters); + this.outValues = GetOutValues(arguments, method.GetParameters()); this.SetFileInfo(); } - private static IMatcher[] GetArgumentMatchers(IReadOnlyList arguments, ParameterInfo[] parameters) - { - Debug.Assert(arguments != null); - Debug.Assert(parameters != null); - Debug.Assert(arguments.Count == parameters.Length); - - var n = parameters.Length; - var argumentMatchers = new IMatcher[n]; - for (int i = 0; i < n; ++i) - { - argumentMatchers[i] = MatcherFactory.CreateMatcher(arguments[i], parameters[i]); - } - return argumentMatchers; - } - private static List> GetOutValues(IReadOnlyList arguments, ParameterInfo[] parameters) { List> outValues = null; From a4beda3108c51909acf2f395742087307a5d2457 Mon Sep 17 00:00:00 2001 From: stakx Date: Fri, 24 Aug 2018 15:48:50 +0200 Subject: [PATCH 3/4] "Linearise" invocation matching conditions Rewrite the match conditions in such a way that the positive match al- ways happens at the methods' end; all earlier conditions lead to an early return and a negative match. This makes the changed methods somewhat easier to read. --- src/Moq/InvocationShape.cs | 51 ++++++++++++++++++++++---------------- 1 file changed, 29 insertions(+), 22 deletions(-) diff --git a/src/Moq/InvocationShape.cs b/src/Moq/InvocationShape.cs index 11ae8164f..73fed9a56 100644 --- a/src/Moq/InvocationShape.cs +++ b/src/Moq/InvocationShape.cs @@ -79,50 +79,57 @@ public bool IsMatch(Invocation invocation) return false; } - if (this.IsEqualMethodOrOverride(invocation.Method)) + if (invocation.Method != this.method && !this.IsOverride(invocation.Method)) { - for (int i = 0, n = this.argumentMatchers.Length; i < n; ++i) + return false; + } + + for (int i = 0, n = this.argumentMatchers.Length; i < n; ++i) + { + if (this.argumentMatchers[i].Matches(arguments[i]) == false) { - if (this.argumentMatchers[i].Matches(arguments[i]) == false) - { - return false; - } + return false; } - - return true; } - return false; + return true; } - private bool IsEqualMethodOrOverride(MethodInfo invocationMethod) + private bool IsOverride(MethodInfo invocationMethod) { var method = this.method; - if (invocationMethod == method) + if (!method.DeclaringType.IsAssignableFrom(invocationMethod.DeclaringType)) { - return true; + return false; } - if (method.DeclaringType.IsAssignableFrom(invocationMethod.DeclaringType)) + if (!method.Name.Equals(invocationMethod.Name, StringComparison.Ordinal)) { - if (!method.Name.Equals(invocationMethod.Name, StringComparison.Ordinal) || - method.ReturnType != invocationMethod.ReturnType || - !method.IsGenericMethod && - !invocationMethod.HasSameParameterTypesAs(method)) + return false; + } + + if (method.ReturnType != invocationMethod.ReturnType) + { + return false; + } + + if (method.IsGenericMethod) + { + if (!invocationMethod.GetGenericArguments().SequenceEqual(method.GetGenericArguments(), AssignmentCompatibilityTypeComparer.Instance)) { return false; } - - if (method.IsGenericMethod && !invocationMethod.GetGenericArguments().SequenceEqual(method.GetGenericArguments(), AssignmentCompatibilityTypeComparer.Instance)) + } + else + { + if (!invocationMethod.HasSameParameterTypesAs(method)) { return false; } - - return true; } - return false; + return true; } private static IMatcher[] GetArgumentMatchers(IReadOnlyList arguments, ParameterInfo[] parameters) From 5a1f061bfda8b1cbbee1a68b408d1b618a819e00 Mon Sep 17 00:00:00 2001 From: stakx Date: Fri, 24 Aug 2018 23:34:38 +0200 Subject: [PATCH 4/4] Decouple verification methods from `MethodCall` This realises one of the two benefits claimed in an earlier commit message: verification methods are modified to use `InvocationShape` instead of newing up transient `MethodCall` instances (which represent setups and are much heavier-weight). --- src/Moq/Mock.cs | 34 ++++++++++++++------------------- src/Moq/MockException.cs | 4 ++-- src/Moq/Obsolete/Mock.Legacy.cs | 9 ++++----- 3 files changed, 20 insertions(+), 27 deletions(-) diff --git a/src/Moq/Mock.cs b/src/Moq/Mock.cs index b72587e6b..7dc839344 100644 --- a/src/Moq/Mock.cs +++ b/src/Moq/Mock.cs @@ -313,8 +313,8 @@ internal static void Verify( var (obj, method, args) = expression.GetCallInfo(mock); ThrowIfVerifyExpressionInvolvesUnsupportedMember(expression, method); - var expected = new MethodCall(mock, null, expression, method, args) { FailMessage = failMessage }; - VerifyCalls(GetTargetMock(obj, mock), expected, expression, times); + var expectation = new InvocationShape(method, args); + VerifyCalls(GetTargetMock(obj, mock), expectation, expression, times, failMessage); } internal static void Verify( @@ -335,11 +335,8 @@ internal static void Verify( var (obj, method, args) = expression.GetCallInfo(mock); ThrowIfVerifyExpressionInvolvesUnsupportedMember(expression, method); - var expected = new MethodCallReturn(mock, null, expression, method, args) - { - FailMessage = failMessage - }; - VerifyCalls(GetTargetMock(obj, mock), expected, expression, times); + var expectation = new InvocationShape(method, args); + VerifyCalls(GetTargetMock(obj, mock), expectation, expression, times, failMessage); } } @@ -353,11 +350,8 @@ internal static void VerifyGet( var method = expression.ToPropertyInfo().GetGetMethod(true); ThrowIfVerifyExpressionInvolvesUnsupportedMember(expression, method); - var expected = new MethodCallReturn(mock, null, expression, method, new Expression[0]) - { - FailMessage = failMessage - }; - VerifyCalls(GetTargetMock(((MemberExpression)expression.Body).Expression, mock), expected, expression, times); + var expectation = new InvocationShape(method, new IMatcher[0]); + VerifyCalls(GetTargetMock(((MemberExpression)expression.Body).Expression, mock), expectation, expression, times, failMessage); } internal static void VerifySet( @@ -369,14 +363,14 @@ internal static void VerifySet( { Mock targetMock = null; LambdaExpression expression = null; - var expected = SetupSetImpl>(mock, setterExpression, (m, expr, method, value) => + var expectation = SetupSetImpl(mock, setterExpression, (m, expr, method, value) => { targetMock = m; expression = expr; - return new MethodCall(m, null, expr, method, value) { FailMessage = failMessage }; + return new InvocationShape(method, value); }); - VerifyCalls(targetMock, expected, expression, times); + VerifyCalls(targetMock, expectation, expression, times, failMessage); } internal static void VerifyNoOtherCalls(Mock mock) @@ -424,17 +418,18 @@ internal static void VerifyNoOtherCalls(Mock mock) private static void VerifyCalls( Mock targetMock, - MethodCall expected, + InvocationShape expectation, LambdaExpression expression, - Times times) + Times times, + string failMessage) { var allInvocations = targetMock.MutableInvocations.ToArray(); - var matchingInvocations = allInvocations.Where(expected.Matches).ToArray(); + var matchingInvocations = allInvocations.Where(expectation.IsMatch).ToArray(); var matchingInvocationCount = matchingInvocations.Length; if (!times.Verify(matchingInvocationCount)) { var setups = targetMock.Setups.ToArrayLive(oc => AreSameMethod(oc.SetupExpression, expression)); - throw MockException.NoMatchingCalls(expected, setups, allInvocations, expression, times, matchingInvocationCount); + throw MockException.NoMatchingCalls(failMessage, setups, allInvocations, expression, times, matchingInvocationCount); } else { @@ -617,7 +612,6 @@ private static TCall SetupSetImpl( Action setterExpression, Func callFactory) where T : class - where TCall : MethodCall { using (var context = new FluentMockContext()) { diff --git a/src/Moq/MockException.cs b/src/Moq/MockException.cs index 1ffd9a036..0eedeba1f 100644 --- a/src/Moq/MockException.cs +++ b/src/Moq/MockException.cs @@ -103,7 +103,7 @@ internal static MockException MoreThanNCalls(MethodCall setup, int maxInvocation /// Returns the exception to be thrown when finds no invocations (or the wrong number of invocations) that match the specified expectation. /// internal static MockException NoMatchingCalls( - MethodCall expected, + string failMessage, IEnumerable setups, IEnumerable invocations, LambdaExpression expression, @@ -112,7 +112,7 @@ internal static MockException NoMatchingCalls( { return new MockException( MockExceptionReason.NoMatchingCalls, - times.GetExceptionMessage(expected.FailMessage, expression.PartialMatcherAwareEval().ToStringFixed(), callCount) + + times.GetExceptionMessage(failMessage, expression.PartialMatcherAwareEval().ToStringFixed(), callCount) + Environment.NewLine + FormatSetupsInfo() + Environment.NewLine + FormatInvocations()); diff --git a/src/Moq/Obsolete/Mock.Legacy.cs b/src/Moq/Obsolete/Mock.Legacy.cs index 25510d2cc..7b3f79332 100644 --- a/src/Moq/Obsolete/Mock.Legacy.cs +++ b/src/Moq/Obsolete/Mock.Legacy.cs @@ -41,6 +41,8 @@ using System; using System.Linq.Expressions; +using Moq.Matchers; + namespace Moq { // Keeps legacy implementations. @@ -57,11 +59,8 @@ internal static void VerifySet( var method = expression.ToPropertyInfo().SetMethod; ThrowIfVerifyExpressionInvolvesUnsupportedMember(expression, method); - var expected = new SetterMethodCall(mock, expression, method) - { - FailMessage = failMessage - }; - VerifyCalls(GetTargetMock(((MemberExpression)expression.Body).Expression, mock), expected, expression, times); + var expectation = new InvocationShape(method, new IMatcher[] { AnyMatcher.Instance }); + VerifyCalls(GetTargetMock(((MemberExpression)expression.Body).Expression, mock), expectation, expression, times, failMessage); } } }