Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for nested type matchers #1092

Merged
merged 6 commits into from Nov 1, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -11,6 +11,7 @@ The format is loosely based on [Keep a Changelog](http://keepachangelog.com/en/1

* New method overloads for `It.Is`, `It.IsIn`, and `It.IsNotIn` that compare values using a custom `IEqualityComparer<T>` (@weitzhandler, #1064)
* New properties `ReturnValue` and `Exception` on `IInvocation` to query recorded invocations return values or exceptions (@MaStr11, #921, #1077)
* Support for "nested" type matchers, i.e. type matchers that appear as part of a composite type (such as `It.IsAnyType[]` or `Func<It.IsAnyType, bool>`). Argument match expressions like `It.IsAny<Func<It.IsAnyType, bool>>()` should now work as expected, whereas they previously didn't. In this particular example, you should no longer need a workaround like `(Func<It.IsAnyType, bool>)It.IsAny<object>()` as originally suggested in #918. (@stakx, #1092)

#### Changed

Expand Down
8 changes: 6 additions & 2 deletions src/Moq/ExpressionExtensions.cs
Expand Up @@ -198,9 +198,13 @@ void Split(Expression e, out Expression r /* remainder */, out InvocationShape p
{
foreach (var typeArgument in methodCallExpression.Method.GetGenericArguments())
{
if (typeArgument.IsTypeMatcher(out var typeMatcherType))
if (typeArgument.IsOrContainsTypeMatcher())
{
Guard.ImplementsTypeMatcherProtocol(typeMatcherType);
// This is a (somewhat roundabout) way of ensuring that the type matchers used
// will be usable. They will not be usable if they don't implement the type
// matcher protocol correctly; and `SubstituteTypeMatchers` tests for that, so
// we'll reuse its recursive logic instead of having to reimplement our own.
_ = typeArgument.SubstituteTypeMatchers(typeArgument);
}
}
}
Expand Down
129 changes: 118 additions & 11 deletions src/Moq/Extensions.cs
Expand Up @@ -252,6 +252,26 @@ public static bool IsTypeMatcher(this Type type, out Type typeMatcherType)
}
}

public static bool IsOrContainsTypeMatcher(this Type type)
{
if (type.IsTypeMatcher())
{
return true;
}
else if (type.HasElementType)
{
return type.GetElementType().IsOrContainsTypeMatcher();
}
else if (type.IsGenericType)
{
return type.GetGenericArguments().Any(IsOrContainsTypeMatcher);
}
else
{
return false;
}
}

public static bool ImplementsTypeMatcherProtocol(this Type type)
{
return typeof(ITypeMatcher).IsAssignableFrom(type) && type.CanCreateInstance();
Expand Down Expand Up @@ -290,26 +310,23 @@ public static IEnumerable<MethodInfo> GetMethods(this Type type, string name)

for (int i = 0; i < count; ++i)
{
if (considerTypeMatchers && types[i].IsTypeMatcher(out var typeMatcherType))
{
Debug.Assert(typeMatcherType.ImplementsTypeMatcherProtocol());
var t = types[i];

var typeMatcher = (ITypeMatcher)Activator.CreateInstance(typeMatcherType);
if (typeMatcher.Matches(otherTypes[i]) == false)
{
return false;
}
if (considerTypeMatchers && t.IsOrContainsTypeMatcher())
{
t = t.SubstituteTypeMatchers(otherTypes[i]);
}
else if (exact)

if (exact)
{
if (types[i] != otherTypes[i])
if (t.Equals(otherTypes[i]) == false)
{
return false;
}
}
else
{
if (types[i].IsAssignableFrom(otherTypes[i]) == false)
if (t.IsAssignableFrom(otherTypes[i]) == false)
{
return false;
}
Expand Down Expand Up @@ -369,6 +386,96 @@ private static MethodInfo GetInvokeMethodFromUntypedDelegateCallback(Delegate ca
}
}

/// <summary>
/// Visits all constituent parts of <paramref name="type"/>, replacing all type matchers
/// that match the type argument at the corresponding position in <paramref name="other"/>.
/// </summary>
/// <param name="type">The type to be matched. May be, or contain, type matchers.</param>
/// <param name="other">The type argument to match against <paramref name="type"/>.</param>
public static Type SubstituteTypeMatchers(this Type type, Type other)
{
// If a type matcher `T` successfully matches its corresponding type `O` from `other`, `T` in `type`
// gets replaced by `O`. If all type matchers successfully matched, and they have been replaced by
// their arguments in `other`, callers can then perform a final `IsAssignableFrom` check to match
// everything else (fixed types). Being able to defer to `IsAssignableFrom` saves us from having to
// re-implement all of its type equivalence rules (polymorphism, co-/contravariance, etc.).
//
// We still need to do some checks ourselves, however: In order to traverse both `type` and `other`
// in lockstep, we need to ensure that they have the same basic structure.

if (type.IsTypeMatcher(out var typeMatcherType))
{
var typeMatcher = (ITypeMatcher)Activator.CreateInstance(typeMatcherType);

if (typeMatcher.Matches(other))
{
return other;
}
}
else if (type.HasElementType && other.HasElementType)
{
var te = type.GetElementType();
var oe = other.GetElementType();

if (type.IsArray && other.IsArray)
{
var tr = type.GetArrayRank();
var or = other.GetArrayRank();

if (tr == or)
{
var se = te.SubstituteTypeMatchers(oe);
if (se.Equals(te))
{
return type;
}
else
{
return tr == 1 ? se.MakeArrayType() : se.MakeArrayType(tr);
}
}
}
else if (type.IsByRef && other.IsByRef)
{
var se = te.SubstituteTypeMatchers(oe);
return se == te ? type : se.MakeByRefType();
}
else if (type.IsPointer && other.IsPointer)
{
var se = te.SubstituteTypeMatchers(oe);
return se == te ? type : se.MakePointerType();
}
}
else if (type.IsGenericType && other.IsGenericType)
{
var td = type.GetGenericTypeDefinition();
var od = other.GetGenericTypeDefinition();

if (td.Equals(od))
{
var ta = type.GetGenericArguments();
var oa = other.GetGenericArguments();
var changed = false;

Debug.Assert(oa.Length == ta.Length);

for (int i = 0; i < ta.Length; ++i)
stakx marked this conversation as resolved.
Show resolved Hide resolved
{
var sa = ta[i].SubstituteTypeMatchers(oa[i]);
if (sa.Equals(ta[i]) == false)
{
changed = true;
ta[i] = sa;
}
}

return changed ? td.MakeGenericType(ta) : type;
}
}

return type;
}

public static Setup TryFind(this IEnumerable<Setup> setups, InvocationShape expectation)
{
return setups.FirstOrDefault(setup => setup.Expectation.Equals(expectation));
Expand Down
6 changes: 3 additions & 3 deletions src/Moq/It.cs
Expand Up @@ -51,7 +51,7 @@ public static class Ref<TValue>
/// </example>
public static TValue IsAny<TValue>()
{
if (typeof(TValue).IsTypeMatcher())
if (typeof(TValue).IsOrContainsTypeMatcher())
{
return Match.Create<TValue>(
(argument, parameterType) => argument == null || parameterType.IsAssignableFrom(argument.GetType()),
Expand Down Expand Up @@ -99,7 +99,7 @@ bool ITypeMatcher.Matches(Type type)
/// <typeparam name="TValue">Type of the value.</typeparam>
public static TValue IsNotNull<TValue>()
{
if (typeof(TValue).IsTypeMatcher())
if (typeof(TValue).IsOrContainsTypeMatcher())
{
return Match.Create<TValue>(
(argument, parameterType) => argument != null && parameterType.IsAssignableFrom(argument.GetType()),
Expand Down Expand Up @@ -140,7 +140,7 @@ public static TValue IsNotNull<TValue>()
[SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures")]
public static TValue Is<TValue>(Expression<Func<TValue, bool>> match)
{
if (typeof(TValue).IsTypeMatcher())
if (typeof(TValue).IsOrContainsTypeMatcher())
{
throw new ArgumentException(Resources.UseItIsOtherOverload, nameof(match));
}
Expand Down
22 changes: 22 additions & 0 deletions tests/Moq.Tests/CustomTypeMatchersFixture.cs
Expand Up @@ -53,6 +53,17 @@ public void Cannot_use_type_matcher_with_parameterized_constructor_directly_in_S
Assert.Contains("Picky does not have a default (public parameterless) constructor", ex.Message);
}

[Fact]
public void Cannot_use_nested_type_matcher_with_parameterized_constructor_directly_in_Setup()
{
var mock = new Mock<IX>();

Action setup = () => mock.Setup(x => x.Method<Picky[]>());

var ex = Assert.Throws<ArgumentException>(setup);
Assert.Contains("Picky does not have a default (public parameterless) constructor", ex.Message);
}

[Fact]
public void Cannot_use_type_matcher_with_parameterized_constructor_directly_in_Verify()
{
Expand All @@ -64,6 +75,17 @@ public void Cannot_use_type_matcher_with_parameterized_constructor_directly_in_V
Assert.Contains("Picky does not have a default (public parameterless) constructor", ex.Message);
}

[Fact]
public void Cannot_use_nested_type_matcher_with_parameterized_constructor_directly_in_Verify()
{
var mock = new Mock<IX>();

Action verify = () => mock.Verify(x => x.Method<Picky[]>(), Times.Never);

var ex = Assert.Throws<ArgumentException>(verify);
Assert.Contains("Picky does not have a default (public parameterless) constructor", ex.Message);
}

[Fact]
public void Can_use_type_matcher_derived_from_one_having_a_parameterized_constructor()
{
Expand Down
1 change: 1 addition & 0 deletions tests/Moq.Tests/Moq.Tests.csproj
Expand Up @@ -23,6 +23,7 @@

<ItemGroup>
<PackageReference Include="Castle.Core" Version="4.4.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="3.1.9" />
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.0" PrivateAssets="All" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
<ProjectReference Include="..\..\src\Moq\Moq.csproj" />
Expand Down
79 changes: 79 additions & 0 deletions tests/Moq.Tests/NestedTypeMatchersFixture.cs
@@ -0,0 +1,79 @@
// Copyright (c) 2007, Clarius Consulting, Manas Technology Solutions, InSTEDD, and Contributors.
// All rights reserved. Licensed under the BSD 3-Clause License; see License.txt.

using System;
using System.Collections.Generic;

using Xunit;

namespace Moq.Tests
{
public class NestedTypeMatchersFixture
{
#region SubstituteTypeMatchers helper extension method

[Theory]
[InlineData(typeof(It.IsSubtype<Enum>), typeof(AttributeTargets), typeof(AttributeTargets))]
[InlineData(typeof(It.IsSubtype<Enum>[]), typeof(AttributeTargets[]), typeof(AttributeTargets[]))]
[InlineData(typeof(IEnumerable<It.IsSubtype<Enum>>), typeof(IEnumerable<AttributeTargets>), typeof(IEnumerable<AttributeTargets>))]
[InlineData(typeof(IEnumerable<It.IsSubtype<Enum>[]>), typeof(IEnumerable<AttributeTargets[]>), typeof(IEnumerable<AttributeTargets[]>))]
[InlineData(typeof(IEnumerable<It.IsSubtype<Enum>>[]), typeof(IEnumerable<AttributeTargets>[]), typeof(IEnumerable<AttributeTargets>[]))]
public void SubstituteTypeMatchers_substitutes_matches(Type type, Type other, Type expected)
{
Assert.Equal(expected, actual: type.SubstituteTypeMatchers(other));
}

[Theory]
[InlineData(typeof(It.IsSubtype<Enum>), typeof(object), typeof(It.IsSubtype<Enum>))]
[InlineData(typeof(It.IsSubtype<Enum>[]), typeof(object[]), typeof(It.IsSubtype<Enum>[]))]
[InlineData(typeof(IEnumerable<It.IsSubtype<Enum>>), typeof(IEnumerable<object>), typeof(IEnumerable<It.IsSubtype<Enum>>))]
[InlineData(typeof(IEnumerable<It.IsSubtype<Enum>[]>), typeof(IEnumerable<object[]>), typeof(IEnumerable<It.IsSubtype<Enum>[]>))]
public void SubstituteTypeMatchers_does_not_substitute_mismatches(Type type, Type other, Type expected)
{
Assert.Equal(expected, actual: type.SubstituteTypeMatchers(other));
}

[Theory]
[InlineData(typeof(It.IsAnyType[]), typeof(IEnumerable<object>), typeof(It.IsAnyType[]))]
[InlineData(typeof(IEnumerable<It.IsAnyType>), typeof(object[]), typeof(IEnumerable<It.IsAnyType>))]
public void SubstituteTypeMatchers_does_not_substitute_mismatched_composite_kind(Type type, Type other, Type expected)
{
Assert.Equal(expected, actual: type.SubstituteTypeMatchers(other));
}

[Theory]
[InlineData(typeof(It.IsAnyType), typeof(IEnumerable<object>), typeof(IEnumerable<object>))]
[InlineData(typeof(It.IsAnyType), typeof(object[]), typeof(object[]))]
public void SubstituteTypeMatchers_gives_precedence_to_type_matchers_over_composite_kind(Type type, Type other, Type expected)
{
Assert.Equal(expected, actual: type.SubstituteTypeMatchers(other));
}

[Theory]
[InlineData(typeof(Tuple<It.IsSubtype<Enum>, object, It.IsAnyType, It.IsSubtype<Enum>>), typeof(Tuple<AttributeTargets, object, object, object>), typeof(Tuple<AttributeTargets, object, object, It.IsSubtype<Enum>>))]
public void SubstituteTypeMatchers_may_substitute_only_some_parts(Type type, Type other, Type expected)
{
Assert.Equal(expected, actual: type.SubstituteTypeMatchers(other));
}

#endregion

#region Test cases

[Fact]
public void It_IsAnyType_used_as_generic_type_argument_of_method()
{
var mock = new Mock<IX>();
mock.Setup(m => m.Method<It.IsAnyType>(It.IsAny<IEnumerable<It.IsAnyType>>())).Verifiable();
mock.Object.Method(new string[] { "a", "b" });
Comment on lines +67 to +68
Copy link
Contributor Author

@stakx stakx Nov 1, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not actually sure I understand why this should work, given the previous tests for SubstituteTypeMatchers. Would this successfully match a string[] (runtime type) or a IEnumerable<string> (static compile-time type) against IEnumerable<It.IsAnyType>? Need to investigate.

mock.Verify();
}

public interface IX
{
void Method<T>(IEnumerable<T> args);
}

#endregion
}
}