Skip to content

Commit 1bd8cdb

Browse files
authored
fix(assertions): skip ref-struct members in IsEquivalentTo (#5841) (#5842)
PropertyInfo.GetValue / FieldInfo.GetValue throw NotSupportedException for ref-struct return types because they cannot be boxed. Filter members whose type is ByRefLike in BuildMembersToCompare so the structural walk skips e.g. ReadOnlyMemory<T>.Span. netstandard2.0 falls back to scanning for IsByRefLikeAttribute since Type.IsByRefLike is .NET 5+.
1 parent caf48fe commit 1bd8cdb

2 files changed

Lines changed: 76 additions & 2 deletions

File tree

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
namespace TUnit.Assertions.Tests.Bugs;
2+
3+
/// <summary>
4+
/// Regression tests for issue #5841.
5+
/// IsEquivalentTo must not invoke property/field getters whose return type is a ref struct
6+
/// (e.g. ReadOnlySpan&lt;T&gt;, reachable via ReadOnlyMemory&lt;T&gt;.Span). Ref structs cannot
7+
/// be boxed, so RuntimeMethodInfo.Invoke throws NotSupportedException.
8+
/// </summary>
9+
public class Tests5841
10+
{
11+
public class HasMemoryProperty
12+
{
13+
public ReadOnlyMemory<byte> Memory { get; init; }
14+
}
15+
16+
public class HasSpanProperty
17+
{
18+
public ReadOnlySpan<byte> Span => default;
19+
public int Value { get; init; }
20+
}
21+
22+
[Test]
23+
public async Task IsEquivalentTo_with_ReadOnlyMemory_property_does_not_invoke_Span_getter()
24+
{
25+
var a = new HasMemoryProperty { Memory = new byte[] { 1, 2, 3 } };
26+
var b = new HasMemoryProperty { Memory = new byte[] { 1, 2, 3 } };
27+
28+
await Assert.That(a).IsEquivalentTo(b);
29+
}
30+
31+
[Test]
32+
public async Task IsEquivalentTo_skips_ref_struct_returning_property()
33+
{
34+
var a = new HasSpanProperty { Value = 7 };
35+
var b = new HasSpanProperty { Value = 7 };
36+
37+
await Assert.That(a).IsEquivalentTo(b);
38+
}
39+
}

TUnit.Assertions/Conditions/Helpers/ReflectionHelper.cs

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,19 +35,54 @@ private static MemberInfo[] BuildMembersToCompare(Type type)
3535
var members = new List<MemberInfo>();
3636

3737
// Filter out indexed properties (properties with parameters like this[int index])
38+
// and members whose type is a ref struct (IsByRefLike). Ref structs cannot be boxed,
39+
// so PropertyInfo.GetValue / FieldInfo.GetValue throws NotSupportedException on them.
40+
// Common offender: ReadOnlyMemory<T>.Span returns ReadOnlySpan<T>.
3841
var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);
3942
foreach (var prop in properties)
4043
{
41-
if (prop.GetIndexParameters().Length == 0 && prop.CanRead && prop.GetMethod?.IsPublic == true)
44+
if (prop.GetIndexParameters().Length == 0
45+
&& prop.CanRead
46+
&& prop.GetMethod?.IsPublic == true
47+
&& !IsByRefLike(prop.PropertyType))
4248
{
4349
members.Add(prop);
4450
}
4551
}
4652

47-
members.AddRange(type.GetFields(BindingFlags.Public | BindingFlags.Instance));
53+
foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.Instance))
54+
{
55+
if (!IsByRefLike(field.FieldType))
56+
{
57+
members.Add(field);
58+
}
59+
}
60+
4861
return members.ToArray();
4962
}
5063

64+
private static bool IsByRefLike(Type type)
65+
{
66+
#if NET5_0_OR_GREATER
67+
return type.IsByRefLike;
68+
#else
69+
if (!type.IsValueType)
70+
{
71+
return false;
72+
}
73+
74+
foreach (var attr in type.GetCustomAttributesData())
75+
{
76+
if (attr.AttributeType.FullName == "System.Runtime.CompilerServices.IsByRefLikeAttribute")
77+
{
78+
return true;
79+
}
80+
}
81+
82+
return false;
83+
#endif
84+
}
85+
5186
/// <summary>
5287
/// Gets the value of a member (property or field) from an object.
5388
/// </summary>

0 commit comments

Comments
 (0)