From d44759e3b919c55985440cb66c9dce6eff30902b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mihnea=20R=C4=83dulescu?= <> Date: Wed, 15 May 2024 22:58:35 +0300 Subject: [PATCH 1/4] Unable to match arguments whose type is generic, when their concrete type is not known (#786) --- src/NSubstitute/Core/Extensions.cs | 35 ++++++++++++++++--- .../ArgumentMatching.cs | 19 ++++++++++ 2 files changed, 50 insertions(+), 4 deletions(-) diff --git a/src/NSubstitute/Core/Extensions.cs b/src/NSubstitute/Core/Extensions.cs index bb3a4880..ff7d0abe 100644 --- a/src/NSubstitute/Core/Extensions.cs +++ b/src/NSubstitute/Core/Extensions.cs @@ -24,17 +24,44 @@ public static bool IsCompatibleWith(this object? instance, Type type) } var instanceType = instance.GetType(); + var compatibleInstanceTypes = GetCompatibleTypes(instanceType); - if (instanceType.IsGenericType && type.IsGenericType - && instanceType.GetGenericTypeDefinition() == type.GetGenericTypeDefinition()) + foreach (var aCompatibleInstanceType in compatibleInstanceTypes) { - // both are the same generic type. If their GenericTypeArguments match then they are equivalent - return CallSpecification.TypesAreAllEquivalent(instanceType.GenericTypeArguments, type.GenericTypeArguments); + if (aCompatibleInstanceType.IsGenericType && + type.IsGenericType && + aCompatibleInstanceType.GetGenericTypeDefinition() == type.GetGenericTypeDefinition()) + { + // both are the same generic type. If their GenericTypeArguments match then they are equivalent + return CallSpecification.TypesAreAllEquivalent( + aCompatibleInstanceType.GenericTypeArguments, type.GenericTypeArguments); + } } return requiredType.IsInstanceOfType(instance); } + private static IReadOnlyList GetCompatibleTypes(Type type) + { + var baseType = type.BaseType; + var interfacesOfType = type.GetInterfaces(); + + List compatibleTypes = [type, ..interfacesOfType]; + + if (baseType is not null) + { + compatibleTypes.AddRange(GetCompatibleTypes(baseType)); + } + + foreach (var anInterfaceOfType in interfacesOfType) + { + compatibleTypes.AddRange(GetCompatibleTypes(anInterfaceOfType)); + } + + var distinctCompatibleTypes = compatibleTypes.Distinct().ToList(); + return distinctCompatibleTypes; + } + /// /// Join the using . /// diff --git a/tests/NSubstitute.Acceptance.Specs/ArgumentMatching.cs b/tests/NSubstitute.Acceptance.Specs/ArgumentMatching.cs index 517ed831..84df7838 100644 --- a/tests/NSubstitute.Acceptance.Specs/ArgumentMatching.cs +++ b/tests/NSubstitute.Acceptance.Specs/ArgumentMatching.cs @@ -740,6 +740,25 @@ public void Supports_custom_argument_matcher_descriptions() Assert.That(ex.Message, Contains.Substring("24 is not forty two")); } + public interface IMyService + { + void MyMethod(IMyArgument argument); + } + public interface IMyArgument { } + // Suppose I don't have access to this type at compile time, so I could not have written Arg.Any() + public class MyStringArgument : IMyArgument { } + + [Test] + public void Supports_matching_covariant_argument() + { + IMyService service = Substitute.For(); + var argument = new MyStringArgument(); + + service.MyMethod(argument); + + service.Received().MyMethod(Arg.Any>()); + } + [SetUp] public void SetUp() { From 2fcec33b2525104f52d2801a96be7f7ede8a477f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mihnea=20R=C4=83dulescu?= <> Date: Wed, 15 May 2024 23:38:54 +0300 Subject: [PATCH 2/4] Fixed whitespace characters --- src/NSubstitute/Core/Extensions.cs | 2 +- tests/NSubstitute.Acceptance.Specs/ArgumentMatching.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/NSubstitute/Core/Extensions.cs b/src/NSubstitute/Core/Extensions.cs index ff7d0abe..695a12fc 100644 --- a/src/NSubstitute/Core/Extensions.cs +++ b/src/NSubstitute/Core/Extensions.cs @@ -46,7 +46,7 @@ private static IReadOnlyList GetCompatibleTypes(Type type) var baseType = type.BaseType; var interfacesOfType = type.GetInterfaces(); - List compatibleTypes = [type, ..interfacesOfType]; + List compatibleTypes = [type, .. interfacesOfType]; if (baseType is not null) { diff --git a/tests/NSubstitute.Acceptance.Specs/ArgumentMatching.cs b/tests/NSubstitute.Acceptance.Specs/ArgumentMatching.cs index 84df7838..9c0a3b43 100644 --- a/tests/NSubstitute.Acceptance.Specs/ArgumentMatching.cs +++ b/tests/NSubstitute.Acceptance.Specs/ArgumentMatching.cs @@ -755,7 +755,7 @@ public void Supports_matching_covariant_argument() var argument = new MyStringArgument(); service.MyMethod(argument); - + service.Received().MyMethod(Arg.Any>()); } From d9797313c09959b5bd34f794bf572f639c4cd876 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mihnea=20R=C4=83dulescu?= <> Date: Tue, 28 May 2024 23:10:01 +0300 Subject: [PATCH 3/4] Simplified argument matching when using Arg.AnyType --- src/NSubstitute/Core/Extensions.cs | 43 ++++------ .../ArgumentMatching.cs | 81 ++++++++++++++++--- 2 files changed, 83 insertions(+), 41 deletions(-) diff --git a/src/NSubstitute/Core/Extensions.cs b/src/NSubstitute/Core/Extensions.cs index 695a12fc..608d98fa 100644 --- a/src/NSubstitute/Core/Extensions.cs +++ b/src/NSubstitute/Core/Extensions.cs @@ -23,44 +23,29 @@ public static bool IsCompatibleWith(this object? instance, Type type) return TypeCanBeNull(requiredType); } - var instanceType = instance.GetType(); - var compatibleInstanceTypes = GetCompatibleTypes(instanceType); + var genericTypeDefinition = type.IsGenericType ? type.GetGenericTypeDefinition() : null; - foreach (var aCompatibleInstanceType in compatibleInstanceTypes) + if (genericTypeDefinition is not null) { - if (aCompatibleInstanceType.IsGenericType && - type.IsGenericType && - aCompatibleInstanceType.GetGenericTypeDefinition() == type.GetGenericTypeDefinition()) + var instanceType = instance.GetType(); + var compatibleInstanceTypes = GetCompatibleTypes(instanceType); + + foreach (var aCompatibleInstanceType in compatibleInstanceTypes) { - // both are the same generic type. If their GenericTypeArguments match then they are equivalent - return CallSpecification.TypesAreAllEquivalent( - aCompatibleInstanceType.GenericTypeArguments, type.GenericTypeArguments); + if (aCompatibleInstanceType.IsGenericType && + aCompatibleInstanceType.GetGenericTypeDefinition() == genericTypeDefinition) + { + // both are the same generic type. If their GenericTypeArguments match then they are equivalent + return CallSpecification.TypesAreAllEquivalent( + aCompatibleInstanceType.GenericTypeArguments, type.GenericTypeArguments); + } } } return requiredType.IsInstanceOfType(instance); } - private static IReadOnlyList GetCompatibleTypes(Type type) - { - var baseType = type.BaseType; - var interfacesOfType = type.GetInterfaces(); - - List compatibleTypes = [type, .. interfacesOfType]; - - if (baseType is not null) - { - compatibleTypes.AddRange(GetCompatibleTypes(baseType)); - } - - foreach (var anInterfaceOfType in interfacesOfType) - { - compatibleTypes.AddRange(GetCompatibleTypes(anInterfaceOfType)); - } - - var distinctCompatibleTypes = compatibleTypes.Distinct().ToList(); - return distinctCompatibleTypes; - } + private static IReadOnlyList GetCompatibleTypes(Type type) => [type, .. type.GetInterfaces()]; /// /// Join the using . diff --git a/tests/NSubstitute.Acceptance.Specs/ArgumentMatching.cs b/tests/NSubstitute.Acceptance.Specs/ArgumentMatching.cs index 44f24e04..49b4238a 100644 --- a/tests/NSubstitute.Acceptance.Specs/ArgumentMatching.cs +++ b/tests/NSubstitute.Acceptance.Specs/ArgumentMatching.cs @@ -300,11 +300,11 @@ public void Should_allow_to_check_received_using_properties_from_other_substitut public void Throw_with_ambiguous_arguments_when_given_an_arg_matcher_and_a_default_arg_value_v1() { Assert.Throws(() => - { - _something.Add(0, Arg.Any()).Returns(1); - Assert.Fail("Should not make it here, as it can't work out which arg the matcher refers to." + - "If it does this will throw an AssertionException rather than AmbiguousArgumentsException."); - }); + { + _something.Add(0, Arg.Any()).Returns(1); + Assert.Fail("Should not make it here, as it can't work out which arg the matcher refers to." + + "If it does this will throw an AssertionException rather than AmbiguousArgumentsException."); + }); } [Test] @@ -740,18 +740,54 @@ public void Supports_custom_argument_matcher_descriptions() Assert.That(ex.Message, Contains.Substring("24 is not forty two")); } - public interface IMyService + [Test] + public void Supports_matching_generic_interface_bound_type_string_with_class_argument() { - void MyMethod(IMyArgument argument); + var service = Substitute.For(); + var argument = new MyStringArgument(); + + service.MyMethod(argument); + + service.Received().MyMethod(Arg.Any>()); } - public interface IMyArgument { } - // Suppose I don't have access to this type at compile time, so I could not have written Arg.Any() - public class MyStringArgument : IMyArgument { } [Test] - public void Supports_matching_covariant_argument() + public void Supports_matching_generic_interface_bound_type_custom_class_with_class_argument() { - IMyService service = Substitute.For(); + var service = Substitute.For(); + var argument = new MySampleClassArgument(); + + service.MyMethod(argument); + + service.Received().MyMethod(Arg.Any>()); + } + + [Test] + public void Supports_matching_generic_interface_bound_type_custom_class_with_derived_class_argument() + { + var service = Substitute.For(); + var argument = new MySampleDerivedClassArgument(); + + service.MyMethod(argument); + + service.Received().MyMethod(Arg.Any>()); + } + + [Test] + public void Supports_matching_custom_class_with_derived_class_argument() + { + var service = Substitute.For(); + var argument = new MySampleDerivedClassArgument(); + + service.MyMethod(argument); + + service.Received().MyMethod(Arg.Any()); + } + + [Test] + public void Supports_matching_generic_interface_bound_type_ArgAnyType_with_class_argument() + { + var service = Substitute.For(); var argument = new MyStringArgument(); service.MyMethod(argument); @@ -759,9 +795,30 @@ public void Supports_matching_covariant_argument() service.Received().MyMethod(Arg.Any>()); } + [Test] + public void Supports_matching_generic_interface_bound_type_ArgAnyType_with_derived_class_argument() + { + var service = Substitute.For(); + var argument = new MySampleDerivedClassArgument(); + + service.MyMethod(argument); + + service.Received().MyMethod(Arg.Any>()); + } + [SetUp] public void SetUp() { _something = Substitute.For(); } + + public interface IMyService + { + void MyMethod(IMyArgument argument); + } + public interface IMyArgument { } + public class SampleClass { } + public class MyStringArgument : IMyArgument { } + public class MySampleClassArgument : IMyArgument { } + public class MySampleDerivedClassArgument : MySampleClassArgument { } } \ No newline at end of file From 92e2fb56d26782c076f803a70ef833cc6710cf88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mihnea=20R=C4=83dulescu?= <> Date: Sun, 2 Jun 2024 14:57:18 +0300 Subject: [PATCH 4/4] Added DidNotReceive() tests covering the Arg.Any constellation --- .../ArgumentMatching.cs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/NSubstitute.Acceptance.Specs/ArgumentMatching.cs b/tests/NSubstitute.Acceptance.Specs/ArgumentMatching.cs index 49b4238a..8b131553 100644 --- a/tests/NSubstitute.Acceptance.Specs/ArgumentMatching.cs +++ b/tests/NSubstitute.Acceptance.Specs/ArgumentMatching.cs @@ -806,6 +806,28 @@ public void Supports_matching_generic_interface_bound_type_ArgAnyType_with_deriv service.Received().MyMethod(Arg.Any>()); } + [Test] + public void Does_not_support_matching_ArgAny_of_type_derived_from_base_type_with_string_type_param_to_other_type_derived_from_base_type() + { + var service = Substitute.For(); + var argument = new MyOtherStringArgument(); + + service.MyMethod(argument); + + service.DidNotReceive().MyMethod(Arg.Any()); + } + + [Test] + public void Does_not_support_matching_ArgAny_of_type_derived_from_base_type_with_custom_type_param_to_other_type_derived_from_base_type() + { + var service = Substitute.For(); + var argument = new MyOtherSampleClassArgument(); + + service.MyMethod(argument); + + service.DidNotReceive().MyMethod(Arg.Any()); + } + [SetUp] public void SetUp() { @@ -819,6 +841,8 @@ public interface IMyService public interface IMyArgument { } public class SampleClass { } public class MyStringArgument : IMyArgument { } + public class MyOtherStringArgument : IMyArgument { } public class MySampleClassArgument : IMyArgument { } + public class MyOtherSampleClassArgument : IMyArgument { } public class MySampleDerivedClassArgument : MySampleClassArgument { } } \ No newline at end of file