Skip to content

Commit

Permalink
Merge pull request #662 from stakx/invocationshape
Browse files Browse the repository at this point in the history
Extract invocation matching logic into `InvocationShape`
  • Loading branch information
stakx committed Aug 24, 2018
2 parents 1f85444 + 5a1f061 commit f852ba2
Show file tree
Hide file tree
Showing 5 changed files with 189 additions and 116 deletions.
162 changes: 162 additions & 0 deletions src/Moq/InvocationShape.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Describes the "shape" of an invocation against which concrete <see cref="Invocation"/>s can be matched.
/// </summary>
internal readonly struct InvocationShape
{
private readonly MethodInfo method;
private readonly IMatcher[] argumentMatchers;

public InvocationShape(MethodInfo method, IReadOnlyList<Expression> arguments)
{
this.method = method;
this.argumentMatchers = GetArgumentMatchers(arguments, method.GetParameters());
}

public InvocationShape(MethodInfo method, IMatcher[] argumentMatchers)
{
this.method = method;
this.argumentMatchers = argumentMatchers;
}

public IReadOnlyList<IMatcher> 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<Expression> 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<Type>
{
public static readonly AssignmentCompatibilityTypeComparer Instance = new AssignmentCompatibilityTypeComparer();

public bool Equals(Type x, Type y)
{
return y.IsAssignableFrom(x);
}

int IEqualityComparer<Type>.GetHashCode(Type obj) => throw new NotSupportedException();
}
}
}
96 changes: 7 additions & 89 deletions src/Moq/MethodCall.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,13 +82,12 @@ public IVerifies Raises(Action<TMock> eventExpression, params object[] args)

internal partial class MethodCall : ICallbackResult, IVerifies, IThrowsResult
{
private IMatcher[] argumentMatchers;
private Action<object[]> 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;
Expand All @@ -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<Expression> 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<Expression> 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<KeyValuePair<int, object>> GetOutValues(IReadOnlyList<Expression> arguments, ParameterInfo[] parameters)
{
List<KeyValuePair<int, object>> outValues = null;
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -488,18 +418,6 @@ public string Format()
return builder.ToString();
}

private sealed class AssignmentCompatibilityTypeComparer : IEqualityComparer<Type>
{
public static AssignmentCompatibilityTypeComparer Instance { get; } = new AssignmentCompatibilityTypeComparer();

public bool Equals(Type x, Type y)
{
return y.IsAssignableFrom(x);
}

int IEqualityComparer<Type>.GetHashCode(Type obj) => throw new NotSupportedException();
}

private sealed class RaiseEventResponse
{
private Mock mock;
Expand Down
Loading

0 comments on commit f852ba2

Please sign in to comment.