Skip to content

Commit

Permalink
Merge pull request #908 from stakx/generic-type-argument-matchers
Browse files Browse the repository at this point in the history
Add support for generic type argument matchers (It.IsAnyType and friends)
  • Loading branch information
stakx authored Aug 31, 2019
2 parents d6eaa92 + 1d572e5 commit 61f420f
Show file tree
Hide file tree
Showing 39 changed files with 1,325 additions and 122 deletions.
30 changes: 30 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,36 @@ The format is loosely based on [Keep a Changelog](http://keepachangelog.com/en/1

* Added support for lambda expressions while creating a mock through `new Mock<SomeType>(() => new SomeType("a", "b"))` and `repository.Create<SomeType>(() => new SomeType("a", "b"))`. This makes the process of mocking a class without a parameterless constructor simpler (compiler syntax checker...). (@frblondin, #884)

* Support for matching generic type arguments: `mock.Setup(m => m.Method<It.IsAnyType>(...))`. (@stakx, #908)

The standard type matchers are:

- `It.IsAnyType` &mdash; matches any type
- `It.IsSubtype<T>` &mdash; matches `T` and proper subtypes of `T`
- `It.IsValueType` &mdash; matches only value types

You can create your own custom type matchers:

```csharp
[TypeMatcher]
class Either<A, B> : ITypeMatcher
{
public bool Matches(Type type) => type == typeof(A) || type == typeof(B);
}
```

* In order to support type matchers (see bullet point above), some new overloads have been added to existing methods:

- `setup.Callback(new InvocationAction(invocation => ...))`,
`setup.Returns(new InvocationFunc(invocation => ...))`:

The lambda specified in these new overloads will receive an `IInvocation` representing the current invocation from which type arguments as well as arguments can be discovered.

- `Match.Create<T>((object argument, Type parameterType) => ..., ...)`,
`It.Is<T>((object argument, Type parameterType) => ...)`:

Used to create custom matchers that work with type matchers. When a type matcher is used for `T`, the `argument` received by the custom matchers is untyped (`object`), and its actual type (or rather the type of the parameter for which the argument was passed) is provided via an additional parameter `parameterType`. (@stakx, #908)

#### Fixed

* Moq does not mock explicit interface implementation and `protected virtual` correctly. (@oddbear, #657)
Expand Down
3 changes: 2 additions & 1 deletion src/Moq/Capture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@ public static T In<T>(IList<T> collection, Expression<Func<T, bool>> predicate)
/// </example>
public static T With<T>(CaptureMatch<T> match)
{
return Match.Create(match);
Match.Register(match);
return default(T);
}
}
}
12 changes: 12 additions & 0 deletions src/Moq/ExpressionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,18 @@ void Split(Expression e, out Expression r /* remainder */, out InvocationShape p
case ExpressionType.Call: // regular method call
{
var methodCallExpression = (MethodCallExpression)e;

if (methodCallExpression.Method.IsGenericMethod)
{
foreach (var typeArgument in methodCallExpression.Method.GetGenericArguments())
{
if (typeArgument.IsTypeMatcher(out var typeMatcherType))
{
Guard.ImplementsTypeMatcherProtocol(typeMatcherType);
}
}
}

if (!methodCallExpression.Method.IsStatic)
{
r = methodCallExpression.Object;
Expand Down
96 changes: 73 additions & 23 deletions src/Moq/Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,21 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.ExceptionServices;
using System.Text;

using Moq.Properties;

namespace Moq
{
internal static class Extensions
{
public static bool CanCreateInstance(this Type type)
{
return type.IsValueType || type.GetConstructor(Type.EmptyTypes) != null;
}

/// <summary>
/// Gets the default value for the specified type. This is the Reflection counterpart of C#'s <see langword="default"/> operator.
/// </summary>
Expand Down Expand Up @@ -144,6 +145,32 @@ public static bool IsMockable(this Type type)
return !type.IsSealed || type.IsDelegateType();
}

public static bool IsTypeMatcher(this Type type)
{
return Attribute.IsDefined(type, typeof(TypeMatcherAttribute));
}

public static bool IsTypeMatcher(this Type type, out Type typeMatcherType)
{
if (type.IsTypeMatcher())
{
var attr = (TypeMatcherAttribute)Attribute.GetCustomAttribute(type, typeof(TypeMatcherAttribute));
typeMatcherType = attr.Type ?? type;
Guard.ImplementsTypeMatcherProtocol(typeMatcherType);
return true;
}
else
{
typeMatcherType = null;
return false;
}
}

public static bool ImplementsTypeMatcherProtocol(this Type type)
{
return typeof(ITypeMatcher).IsAssignableFrom(type) && type.CanCreateInstance();
}

public static bool CanOverride(this MethodBase method)
{
return method.IsVirtual && !method.IsFinal && !method.IsPrivate;
Expand Down Expand Up @@ -176,7 +203,7 @@ public static IEnumerable<MethodInfo> GetMethods(this Type type, string name)
return type.GetMember(name).OfType<MethodInfo>();
}

public static bool CompareTo<TTypes, TOtherTypes>(this TTypes types, TOtherTypes otherTypes, bool exact)
public static bool CompareTo<TTypes, TOtherTypes>(this TTypes types, TOtherTypes otherTypes, TypeComparison comparisonType)
where TTypes : IReadOnlyList<Type>
where TOtherTypes : IReadOnlyList<Type>
{
Expand All @@ -187,28 +214,51 @@ public static bool CompareTo<TTypes, TOtherTypes>(this TTypes types, TOtherTypes
return false;
}

if (exact)
switch (comparisonType)
{
for (int i = 0; i < count; ++i)
{
if (types[i] != otherTypes[i])
case TypeComparison.Equality:
for (int i = 0; i < count; ++i)
{
return false;
if (types[i] != otherTypes[i])
{
return false;
}
}
}
}
else
{
for (int i = 0; i < count; ++i)
{
if (types[i].IsAssignableFrom(otherTypes[i]) == false)
return true;

case TypeComparison.AssignmentCompatibility:
for (int i = 0; i < count; ++i)
{
return false;
if (types[i].IsAssignableFrom(otherTypes[i]) == false)
{
return false;
}
}
}
}
return true;

return true;
case TypeComparison.TypeMatchersOrElseAssignmentCompatibility:
for (int i = 0; i < count; ++i)
{
if (types[i].IsTypeMatcher(out var typeMatcherType))
{
Debug.Assert(typeMatcherType.ImplementsTypeMatcherProtocol());

var typeMatcher = (ITypeMatcher)Activator.CreateInstance(typeMatcherType);
if (typeMatcher.Matches(otherTypes[i]) == false)
{
return false;
}
}
else if (types[i].IsAssignableFrom(otherTypes[i]) == false)
{
return false;
}
}
return true;

default:
throw new ArgumentOutOfRangeException(nameof(comparisonType));
}
}

public static string GetParameterTypeList(this MethodInfo method)
Expand All @@ -225,7 +275,7 @@ public static bool CompareParameterTypesTo<TOtherTypes>(this Delegate function,
where TOtherTypes : IReadOnlyList<Type>
{
var method = function.GetMethodInfo();
if (method.GetParameterTypes().CompareTo(otherTypes, exact: false))
if (method.GetParameterTypes().CompareTo(otherTypes, TypeComparison.AssignmentCompatibility))
{
// the backing method for the literal delegate is compatible, DynamicInvoke(...) will succeed
return true;
Expand All @@ -236,7 +286,7 @@ public static bool CompareParameterTypesTo<TOtherTypes>(this Delegate function,
// an instance delegate invocation is created for an extension method (bundled with a receiver)
// or at times for DLR code generation paths because the CLR is optimized for instance methods.
var invokeMethod = GetInvokeMethodFromUntypedDelegateCallback(function);
if (invokeMethod != null && invokeMethod.GetParameterTypes().CompareTo(otherTypes, exact: false))
if (invokeMethod != null && invokeMethod.GetParameterTypes().CompareTo(otherTypes, TypeComparison.AssignmentCompatibility))
{
// the Invoke(...) method is compatible instead. DynamicInvoke(...) will succeed.
return true;
Expand Down
29 changes: 29 additions & 0 deletions src/Moq/Guard.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,35 @@ namespace Moq
[DebuggerStepThrough]
internal static class Guard
{
public static void CanCreateInstance(Type type)
{
if (!type.CanCreateInstance())
{
throw new ArgumentException(
string.Format(
CultureInfo.CurrentCulture,
Resources.TypeHasNoDefaultConstructor,
type.GetFormattedName()));
}
}

public static void ImplementsTypeMatcherProtocol(Type type)
{
Debug.Assert(type != null);

if (typeof(ITypeMatcher).IsAssignableFrom(type) == false)
{
throw new ArgumentException(
string.Format(
CultureInfo.CurrentCulture,
Resources.TypeNotImplementInterface,
type.GetFormattedName(),
typeof(ITypeMatcher).GetFormattedName()));
}

Guard.CanCreateInstance(type);
}

public static void IsAssignmentToPropertyOrIndexer(LambdaExpression expression, string paramName)
{
Debug.Assert(expression != null);
Expand Down
6 changes: 4 additions & 2 deletions src/Moq/IMatcher.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
// Copyright (c) 2007, Clarius Consulting, Manas Technology Solutions, InSTEDD.
// All rights reserved. Licensed under the BSD 3-Clause License; see License.txt.

using System;

namespace Moq
{
internal interface IMatcher
{
bool Matches(object value);
bool Matches(object argument, Type parameterType);

void SetupEvaluatedSuccessfully(object value);
void SetupEvaluatedSuccessfully(object argument, Type parameterType);
}
}
25 changes: 25 additions & 0 deletions src/Moq/ITypeMatcher.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System;

namespace Moq
{
/// <summary>
/// Types that implement this interface represent a criterion against which generic type arguments are matched.
/// <para>
/// To be used in combination with <see cref="TypeMatcherAttribute"/>.
/// </para>
/// </summary>
public interface ITypeMatcher
{
/// <summary>
/// Matches the provided type argument against the criterion represented by this type matcher.
/// </summary>
/// <param name="typeArgument">
/// The generic type argument that should be matched.
/// </param>
/// <returns>
/// <see langword="true"/> if the provided type argument matched the criterion represented by this instance;
/// otherwise, <see langword="false"/>.
/// </returns>
bool Matches(Type typeArgument);
}
}
31 changes: 31 additions & 0 deletions src/Moq/InvocationAction.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright (c) 2007, Clarius Consulting, Manas Technology Solutions, InSTEDD.
// All rights reserved. Licensed under the BSD 3-Clause License; see License.txt.

using System;

namespace Moq
{
/// <summary>
/// A delegate-like type for use with `setup.Callback` which instructs the `Callback` verb
/// to provide the callback with the current <see cref="IInvocation"/>, instead of
/// with a list of arguments.
/// <para>
/// This type is useful in scenarios involving generic type argument matchers (such as
/// <see cref="It.IsAnyType" />) as <see cref="IInvocation"/> allows the discovery of both
/// arguments and type arguments.
/// </para>
/// </summary>
public readonly struct InvocationAction
{
internal readonly Action<IInvocation> Action;

/// <summary>
/// Initializes a new instance of the <see cref="InvocationAction"/> type.
/// </summary>
/// <param name="action">The delegate that should be wrapped by this instance.</param>
public InvocationAction(Action<IInvocation> action)
{
this.Action = action;
}
}
}
31 changes: 31 additions & 0 deletions src/Moq/InvocationFunc.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright (c) 2007, Clarius Consulting, Manas Technology Solutions, InSTEDD.
// All rights reserved. Licensed under the BSD 3-Clause License; see License.txt.

using System;

namespace Moq
{
/// <summary>
/// A delegate-like type for use with `setup.Returns` which instructs the `Returns` verb
/// to provide the callback with the current <see cref="IInvocation"/>, instead of
/// with a list of arguments.
/// <para>
/// This type is useful in scenarios involving generic type argument matchers (such as
/// <see cref="It.IsAnyType" />) as <see cref="IInvocation"/> allows the discovery of both
/// arguments and type arguments.
/// </para>
/// </summary>
public readonly struct InvocationFunc
{
internal readonly Func<IInvocation, object> Func;

/// <summary>
/// Initializes a new instance of the <see cref="InvocationFunc"/> type.
/// </summary>
/// <param name="func">The delegate that should be wrapped by this instance.</param>
public InvocationFunc(Func<IInvocation, object> func)
{
this.Func = func;
}
}
}
8 changes: 5 additions & 3 deletions src/Moq/InvocationShape.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,10 @@ public bool IsMatch(Invocation invocation)
}

var arguments = invocation.Arguments;
var parameterTypes = invocation.Method.GetParameterTypes();
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], parameterTypes[i]) == false)
{
return false;
}
Expand All @@ -84,9 +85,10 @@ public bool IsMatch(Invocation invocation)
public void SetupEvaluatedSuccessfully(Invocation invocation)
{
var arguments = invocation.Arguments;
var parameterTypes = invocation.Method.GetParameterTypes();
for (int i = 0, n = this.argumentMatchers.Length; i < n; ++i)
{
this.argumentMatchers[i].SetupEvaluatedSuccessfully(arguments[i]);
this.argumentMatchers[i].SetupEvaluatedSuccessfully(arguments[i], parameterTypes[i]);
}
}

Expand Down Expand Up @@ -127,7 +129,7 @@ private bool IsOverride(Invocation invocation)

if (method.IsGenericMethod || invocationMethod.IsGenericMethod)
{
if (!method.GetGenericArguments().CompareTo(invocationMethod.GetGenericArguments(), exact: false))
if (!method.GetGenericArguments().CompareTo(invocationMethod.GetGenericArguments(), TypeComparison.TypeMatchersOrElseAssignmentCompatibility))
{
return false;
}
Expand Down
Loading

0 comments on commit 61f420f

Please sign in to comment.