diff --git a/src/Moq/InvocationShape.cs b/src/Moq/InvocationShape.cs new file mode 100644 index 000000000..73fed9a56 --- /dev/null +++ b/src/Moq/InvocationShape.cs @@ -0,0 +1,162 @@ +//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.Diagnostics; +using System.Linq; +using System.Linq.Expressions; +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, IReadOnlyList arguments) + { + this.method = method; + this.argumentMatchers = GetArgumentMatchers(arguments, method.GetParameters()); + } + + 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 (invocation.Method != this.method && !this.IsOverride(invocation.Method)) + { + return false; + } + + for (int i = 0, n = this.argumentMatchers.Length; i < n; ++i) + { + if (this.argumentMatchers[i].Matches(arguments[i]) == false) + { + return false; + } + } + + return true; + } + + private bool IsOverride(MethodInfo invocationMethod) + { + var method = this.method; + + if (!method.DeclaringType.IsAssignableFrom(invocationMethod.DeclaringType)) + { + return false; + } + + if (!method.Name.Equals(invocationMethod.Name, StringComparison.Ordinal)) + { + return false; + } + + if (method.ReturnType != invocationMethod.ReturnType) + { + return false; + } + + if (method.IsGenericMethod) + { + if (!invocationMethod.GetGenericArguments().SequenceEqual(method.GetGenericArguments(), AssignmentCompatibilityTypeComparer.Instance)) + { + return false; + } + } + else + { + if (!invocationMethod.HasSameParameterTypesAs(method)) + { + return false; + } + } + + return true; + } + + 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(); + + 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..334734721 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,43 +107,23 @@ 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) { this.condition = condition; - this.method = method; + this.expectation = new InvocationShape(method, arguments); this.mock = mock; this.originalExpression = originalExpression; - - var parameters = method.GetParameters(); - this.argumentMatchers = GetArgumentMatchers(arguments, parameters); - 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; @@ -179,7 +158,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 +220,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 +296,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 +328,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 +418,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; 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); } } }