diff --git a/src/xunit.execution/Sdk/TypeUtility.cs b/src/xunit.execution/Sdk/TypeUtility.cs index 152cc3bec..d65d71479 100644 --- a/src/xunit.execution/Sdk/TypeUtility.cs +++ b/src/xunit.execution/Sdk/TypeUtility.cs @@ -130,48 +130,53 @@ public static object[] ResolveMethodArguments(this MethodBase testMethod, object private static object TryConvertObject(object argumentValue, Type parameterType) { - var argumentValueType = argumentValue?.GetType(); - - // We don't need to check if we're passing null to a value type here, as MethodInfo.Invoke does this - if (argumentValueType == null) - return argumentValue; + if (argumentValue == null) + return null; // No need to perform conversion - if (parameterType.IsAssignableFrom(argumentValueType)) + if (parameterType.IsAssignableFrom(argumentValue.GetType())) return argumentValue; - // Implicit & explicit conversions to/from a type can be declared on either side of the relationship - // We need to check both possibilities. - return PerformDefinedConversions(argumentValue, parameterType) - ?? PerformDefinedConversions(argumentValue, argumentValueType) - ?? argumentValue; + return PerformDefinedConversions(argumentValue, parameterType) ?? argumentValue; } private static object PerformDefinedConversions(object argumentValue, - Type conversionDeclaringType) + Type parameterType) { // argumentValue is known to not be null when we're called from TryConvertObject var argumentValueType = argumentValue.GetType(); - - var methodTypes = new Type[] { argumentValueType }; var methodArguments = new object[] { argumentValue }; - // Check if we can implicitly convert the argument type to the parameter type - var implicitMethod = conversionDeclaringType.GetRuntimeMethod("op_Implicit", methodTypes); - if (implicitMethod != null && implicitMethod.IsStatic && !IsByRefLikeType(implicitMethod.ReturnType)) - return implicitMethod.Invoke(null, methodArguments); + bool isMatchingOperator(MethodInfo m, string name) => + m.Name.Equals(name) && + m.IsSpecialName && // Filter out non-operator methods that might bear this reserved name + m.IsStatic && + !IsByRefLikeType(m.ReturnType) && + m.GetParameters().Length == 1 && + m.GetParameters()[0].ParameterType == argumentValueType && + parameterType.IsAssignableFrom(m.ReturnType); - // Check if we can explicitly convert the argument type to the parameter type - var explicitMethod = conversionDeclaringType.GetRuntimeMethod("op_Explicit", methodTypes); - if (explicitMethod != null && explicitMethod.IsStatic && !IsByRefLikeType(explicitMethod.ReturnType)) - return explicitMethod.Invoke(null, methodArguments); + // Implicit & explicit conversions to/from a type can be declared on either side of the relationship. + // We need to check both possibilities. + foreach (var conversionDeclaringType in new[] { parameterType, argumentValueType }) + { + var runtimeMethods = conversionDeclaringType.GetRuntimeMethods(); + + var implicitMethod = runtimeMethods.FirstOrDefault(m => isMatchingOperator(m, "op_Implicit")); + if (implicitMethod != null) + return implicitMethod.Invoke(null, methodArguments); + + var explicitMethod = runtimeMethods.FirstOrDefault(m => isMatchingOperator(m, "op_Explicit")); + if (explicitMethod != null) + return explicitMethod.Invoke(null, methodArguments); + } return null; } private static bool IsByRefLikeType(Type type) { - object val = type.GetType().GetRuntimeProperty("IsByRefLike")?.GetValue(type); + var val = type.GetType().GetRuntimeProperty("IsByRefLike")?.GetValue(type); if (val is bool isByRefLike) return isByRefLike; diff --git a/test/test.xunit.execution/Acceptance/Xunit2TheoryAcceptanceTests.cs b/test/test.xunit.execution/Acceptance/Xunit2TheoryAcceptanceTests.cs index 16a07333a..156ed42ca 100644 --- a/test/test.xunit.execution/Acceptance/Xunit2TheoryAcceptanceTests.cs +++ b/test/test.xunit.execution/Acceptance/Xunit2TheoryAcceptanceTests.cs @@ -264,6 +264,8 @@ public void ImplicitExplicitConversions() Assert.Collection(results.Cast().OrderBy(r => r.Test.DisplayName), result => Assert.Equal(@"Xunit2TheoryAcceptanceTests+TheoryTests+ClassWithOperatorConversions.ArgumentDeclaredExplicitConversion(value: ""abc"")", result.Test.DisplayName), result => Assert.Equal(@"Xunit2TheoryAcceptanceTests+TheoryTests+ClassWithOperatorConversions.ArgumentDeclaredImplicitConversion(value: ""abc"")", result.Test.DisplayName), + result => Assert.Equal(@"Xunit2TheoryAcceptanceTests+TheoryTests+ClassWithOperatorConversions.DecimalToInt(value: 43)", result.Test.DisplayName), + result => Assert.Equal(@"Xunit2TheoryAcceptanceTests+TheoryTests+ClassWithOperatorConversions.IntToDecimal(value: 43)", result.Test.DisplayName), result => Assert.Equal(@"Xunit2TheoryAcceptanceTests+TheoryTests+ClassWithOperatorConversions.IntToLong(i: 1)", result.Test.DisplayName), result => Assert.Equal(@"Xunit2TheoryAcceptanceTests+TheoryTests+ClassWithOperatorConversions.ParameterDeclaredExplicitConversion(e: Explicit { Value = ""abc"" })", result.Test.DisplayName), result => Assert.Equal(@"Xunit2TheoryAcceptanceTests+TheoryTests+ClassWithOperatorConversions.ParameterDeclaredImplicitConversion(i: Implicit { Value = ""abc"" })", result.Test.DisplayName), @@ -325,6 +327,27 @@ public void UIntToULong(ulong i) Assert.Equal(1UL, i); } + public static IEnumerable DecimalArgument() + { + yield return new object[] { 43M }; + } + + // Decimal type offers multiple explicit conversions + [Theory] + [MemberData(nameof(DecimalArgument))] + public void DecimalToInt(int value) + { + Assert.Equal(43, value); + } + + // Decimal type offers multiple implicit conversions + [Theory] + [InlineData(43)] + public void IntToDecimal(decimal value) + { + Assert.Equal(43M, value); + } + public class Explicit { public string Value { get; set; }