Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions TUnit.Assertions.Tests/Bugs/Tests5841.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
namespace TUnit.Assertions.Tests.Bugs;

/// <summary>
/// Regression tests for issue #5841.
/// IsEquivalentTo must not invoke property/field getters whose return type is a ref struct
/// (e.g. ReadOnlySpan&lt;T&gt;, reachable via ReadOnlyMemory&lt;T&gt;.Span). Ref structs cannot
/// be boxed, so RuntimeMethodInfo.Invoke throws NotSupportedException.
/// </summary>
public class Tests5841
{
public class HasMemoryProperty
{
public ReadOnlyMemory<byte> Memory { get; init; }
}

public class HasSpanProperty
{
public ReadOnlySpan<byte> 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);
}
}
39 changes: 37 additions & 2 deletions TUnit.Assertions/Conditions/Helpers/ReflectionHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,19 +35,54 @@ private static MemberInfo[] BuildMembersToCompare(Type type)
var members = new List<MemberInfo>();

// 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<T>.Span returns ReadOnlySpan<T>.
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
}

/// <summary>
/// Gets the value of a member (property or field) from an object.
/// </summary>
Expand Down
Loading