diff --git a/TUnit.Assertions.Tests/Bugs/Tests5841.cs b/TUnit.Assertions.Tests/Bugs/Tests5841.cs new file mode 100644 index 0000000000..ca0549b6df --- /dev/null +++ b/TUnit.Assertions.Tests/Bugs/Tests5841.cs @@ -0,0 +1,39 @@ +namespace TUnit.Assertions.Tests.Bugs; + +/// +/// Regression tests for issue #5841. +/// IsEquivalentTo must not invoke property/field getters whose return type is a ref struct +/// (e.g. ReadOnlySpan<T>, reachable via ReadOnlyMemory<T>.Span). Ref structs cannot +/// be boxed, so RuntimeMethodInfo.Invoke throws NotSupportedException. +/// +public class Tests5841 +{ + public class HasMemoryProperty + { + public ReadOnlyMemory Memory { get; init; } + } + + public class HasSpanProperty + { + public ReadOnlySpan Span => default; + public int Value { get; init; } + } + + [Test] + public async Task IsEquivalentTo_with_ReadOnlyMemory_property_does_not_invoke_Span_getter() + { + var a = new HasMemoryProperty { Memory = new byte[] { 1, 2, 3 } }; + var b = new HasMemoryProperty { Memory = new byte[] { 1, 2, 3 } }; + + await Assert.That(a).IsEquivalentTo(b); + } + + [Test] + public async Task IsEquivalentTo_skips_ref_struct_returning_property() + { + var a = new HasSpanProperty { Value = 7 }; + var b = new HasSpanProperty { Value = 7 }; + + await Assert.That(a).IsEquivalentTo(b); + } +} diff --git a/TUnit.Assertions/Conditions/Helpers/ReflectionHelper.cs b/TUnit.Assertions/Conditions/Helpers/ReflectionHelper.cs index 501523b5be..c9e932848d 100644 --- a/TUnit.Assertions/Conditions/Helpers/ReflectionHelper.cs +++ b/TUnit.Assertions/Conditions/Helpers/ReflectionHelper.cs @@ -35,19 +35,54 @@ private static MemberInfo[] BuildMembersToCompare(Type type) var members = new List(); // Filter out indexed properties (properties with parameters like this[int index]) + // and members whose type is a ref struct (IsByRefLike). Ref structs cannot be boxed, + // so PropertyInfo.GetValue / FieldInfo.GetValue throws NotSupportedException on them. + // Common offender: ReadOnlyMemory.Span returns ReadOnlySpan. var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance); foreach (var prop in properties) { - if (prop.GetIndexParameters().Length == 0 && prop.CanRead && prop.GetMethod?.IsPublic == true) + if (prop.GetIndexParameters().Length == 0 + && prop.CanRead + && prop.GetMethod?.IsPublic == true + && !IsByRefLike(prop.PropertyType)) { members.Add(prop); } } - members.AddRange(type.GetFields(BindingFlags.Public | BindingFlags.Instance)); + foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.Instance)) + { + if (!IsByRefLike(field.FieldType)) + { + members.Add(field); + } + } + return members.ToArray(); } + private static bool IsByRefLike(Type type) + { +#if NET5_0_OR_GREATER + return type.IsByRefLike; +#else + if (!type.IsValueType) + { + return false; + } + + foreach (var attr in type.GetCustomAttributesData()) + { + if (attr.AttributeType.FullName == "System.Runtime.CompilerServices.IsByRefLikeAttribute") + { + return true; + } + } + + return false; +#endif + } + /// /// Gets the value of a member (property or field) from an object. ///