diff --git a/src/NSubstitute/Core/CallSpecification.cs b/src/NSubstitute/Core/CallSpecification.cs index e0d1ff15..c5984206 100644 --- a/src/NSubstitute/Core/CallSpecification.cs +++ b/src/NSubstitute/Core/CallSpecification.cs @@ -59,7 +59,7 @@ private static Type[] ParameterTypes(MethodInfo info) return info.GetParameters().Select(p => p.ParameterType).ToArray(); } - internal static bool TypesAreAllEquivalent(Type[] aArgs, Type[] bArgs) + internal static bool TypesAreAllEquivalent(Type[] aArgs, Type[] bArgs, AreAssignableInclusionType assignableInclusionType = AreAssignableInclusionType.Include) { if (aArgs.Length != bArgs.Length) return false; for (var i = 0; i < aArgs.Length; i++) @@ -79,21 +79,31 @@ internal static bool TypesAreAllEquivalent(Type[] aArgs, Type[] bArgs) if (first.IsGenericType && second.IsGenericType && first.GetGenericTypeDefinition() == second.GetGenericTypeDefinition()) { + var genericArgumentsInclusionType = assignableInclusionType switch + { + AreAssignableInclusionType.Exclude => AreAssignableInclusionType.Exclude, + AreAssignableInclusionType.ExcludeGenericArguments => AreAssignableInclusionType.Exclude, + _ => AreAssignableInclusionType.Include + }; + // both are the same generic type. If their GenericTypeArguments match then they are equivalent - if (!TypesAreAllEquivalent(first.GenericTypeArguments, second.GenericTypeArguments)) + if (!TypesAreAllEquivalent(first.GenericTypeArguments, second.GenericTypeArguments, genericArgumentsInclusionType)) { return false; } continue; } + var includeAreAssignable = assignableInclusionType == AreAssignableInclusionType.Include + || assignableInclusionType == AreAssignableInclusionType.ExcludeGenericArguments; + var areAssignable = first.IsAssignableFrom(second) || second.IsAssignableFrom(first); var areAnyTypeAssignable = typeof(Arg.AnyType).IsAssignableFrom(first) || typeof(Arg.AnyType).IsAssignableFrom(second); var areByRefAnyTypeAssignable = first.IsByRef && second.IsByRef && (typeof(Arg.AnyType).IsAssignableFrom(first.GetElementType()) || typeof(Arg.AnyType).IsAssignableFrom(second.GetElementType())); - var areEquivalent = areAssignable || areAnyTypeAssignable || areByRefAnyTypeAssignable; + var areEquivalent = (includeAreAssignable && areAssignable) || areAnyTypeAssignable || areByRefAnyTypeAssignable; if (!areEquivalent) return false; } return true; @@ -102,7 +112,12 @@ internal static bool TypesAreAllEquivalent(Type[] aArgs, Type[] bArgs) private static bool AreEquivalentDefinitions(MethodInfo a, MethodInfo b) { return a.IsGenericMethod == b.IsGenericMethod - && TypesAreAllEquivalent([a.ReturnType], [b.ReturnType]) + + // Exclude the assignable check for generic arguments, generic types generally are not equivalent when a generic argument is not + // exactly the same, even though the types themselves may be assignable. + // See https://github.com/nsubstitute/NSubstitute/issues/974. + && TypesAreAllEquivalent([a.ReturnType], [b.ReturnType], AreAssignableInclusionType.ExcludeGenericArguments) + && a.Name.Equals(b.Name, StringComparison.Ordinal); } @@ -175,4 +190,25 @@ private bool HasDifferentNumberOfArguments(ICall call) { return _argumentSpecifications.Length != call.GetOriginalArguments().Length; } + + /// + /// Specifies whether an assignability check between types should be included for type equivalence checks. + /// + internal enum AreAssignableInclusionType + { + /// + /// Include the check + /// + Include, + + /// + /// Exclude the check + /// + Exclude, + + /// + /// Exclude the check for generic arguments, but include it for the current type + /// + ExcludeGenericArguments, + } } \ No newline at end of file diff --git a/tests/NSubstitute.Acceptance.Specs/GenericArguments.cs b/tests/NSubstitute.Acceptance.Specs/GenericArguments.cs index aa173e13..e8f6ab9d 100644 --- a/tests/NSubstitute.Acceptance.Specs/GenericArguments.cs +++ b/tests/NSubstitute.Acceptance.Specs/GenericArguments.cs @@ -1,7 +1,7 @@ +using NUnit.Framework; using System.Collections; using System.Globalization; using System.Reflection; -using NUnit.Framework; namespace NSubstitute.Acceptance.Specs; @@ -180,6 +180,24 @@ public void Returns_works_with_AnyType_for_ref_parameter_with_AnyType_generic_ar Assert.That(result, Is.True); } + /// + /// See https://github.com/nsubstitute/NSubstitute/issues/974. + /// + [Test] + public void Returns_works_with_mismatching_generic_return_types() + { + ISomethingWithGenerics something = Substitute.For(); + + something + .SomeFunction(Arg.Any()) + .Returns(Substitute.For>()); + + something + .SomeFunction(Arg.Any()) + .Returns(Substitute.For>()); + } + + [Test] public void Callback_allows_access_to_method_call() { @@ -210,4 +228,4 @@ static ICollection CreateSubstitute(int count) Assert.That(result.Count, Is.EqualTo(7)); } -} \ No newline at end of file +}