diff --git a/TUnit.Assertions.SourceGenerator/Generators/AssertionExtensionGenerator.cs b/TUnit.Assertions.SourceGenerator/Generators/AssertionExtensionGenerator.cs index 7bd92aceff..df27b7c2c7 100644 --- a/TUnit.Assertions.SourceGenerator/Generators/AssertionExtensionGenerator.cs +++ b/TUnit.Assertions.SourceGenerator/Generators/AssertionExtensionGenerator.cs @@ -92,13 +92,23 @@ public void Initialize(IncrementalGeneratorInitializationContext context) return null; } + // Check for RequiresDynamicCode attribute + var requiresDynamicCodeAttr = classSymbol.GetAttributes() + .FirstOrDefault(attr => attr.AttributeClass?.Name == "RequiresDynamicCodeAttribute"); + string? requiresDynamicCodeMessage = null; + if (requiresDynamicCodeAttr != null && requiresDynamicCodeAttr.ConstructorArguments.Length > 0) + { + requiresDynamicCodeMessage = requiresDynamicCodeAttr.ConstructorArguments[0].Value?.ToString(); + } + return new AssertionExtensionData( classSymbol, methodName!, negatedMethodName, assertionBaseType, constructors, - overloadPriority + overloadPriority, + requiresDynamicCodeMessage ); } @@ -299,6 +309,13 @@ private static void GenerateExtensionMethod( sourceBuilder.AppendLine($" /// Extension method for {assertionType.Name}."); sourceBuilder.AppendLine(" /// "); + // Add RequiresDynamicCode attribute if present + if (!string.IsNullOrEmpty(data.RequiresDynamicCodeMessage)) + { + var escapedMessage = data.RequiresDynamicCodeMessage.Replace("\"", "\\\""); + sourceBuilder.AppendLine($" [global::System.Diagnostics.CodeAnalysis.RequiresDynamicCode(\"{escapedMessage}\")]"); + } + // Add OverloadResolutionPriority attribute only if priority > 0 if (data.OverloadResolutionPriority > 0) { @@ -436,6 +453,7 @@ private record AssertionExtensionData( string? NegatedMethodName, INamedTypeSymbol AssertionBaseType, ImmutableArray Constructors, - int OverloadResolutionPriority + int OverloadResolutionPriority, + string? RequiresDynamicCodeMessage ); } diff --git a/TUnit.Assertions.Tests/CollectionStructuralEquivalenceTests.cs b/TUnit.Assertions.Tests/CollectionStructuralEquivalenceTests.cs new file mode 100644 index 0000000000..7660b6fa19 --- /dev/null +++ b/TUnit.Assertions.Tests/CollectionStructuralEquivalenceTests.cs @@ -0,0 +1,235 @@ +using TUnit.Assertions.Conditions.Helpers; +using TUnit.Assertions.Enums; + +namespace TUnit.Assertions.Tests; + +/// +/// Tests for issue #3454: Collection IsEquivalentTo should use structural equality for complex objects +/// +public class CollectionStructuralEquivalenceTests +{ + [Test] + public async Task Collections_With_Structurally_Equal_Objects_Are_Equivalent() + { + var a = new Message { Content = "Hello" }; + var b = new Message { Content = "Hello" }; + var listA = new List { a, a }; + var listB = new List { b, b }; + + await TUnitAssert.That(listA).IsEquivalentTo(listB); + } + + [Test] + public async Task Collections_With_Structurally_Different_Objects_Are_Not_Equivalent() + { + var a = new Message { Content = "Hello" }; + var b = new Message { Content = "World" }; + var listA = new List { a }; + var listB = new List { b }; + + await TUnitAssert.That(listA).IsNotEquivalentTo(listB); + } + + [Test] + public async Task Collections_With_Nested_Objects_Are_Equivalent() + { + var listA = new List + { + new() { Content = "Hello", Nested = new Message { Content = "World" } } + }; + var listB = new List + { + new() { Content = "Hello", Nested = new Message { Content = "World" } } + }; + + await TUnitAssert.That(listA).IsEquivalentTo(listB); + } + + [Test] + public async Task Collections_With_Different_Nested_Objects_Are_Not_Equivalent() + { + var listA = new List + { + new() { Content = "Hello", Nested = new Message { Content = "World" } } + }; + var listB = new List + { + new() { Content = "Hello", Nested = new Message { Content = "Universe" } } + }; + + await TUnitAssert.That(listA).IsNotEquivalentTo(listB); + } + + [Test] + public async Task Collections_With_Nested_Collections_Are_Equivalent() + { + var listA = new List + { + new() { Content = "Hello", Messages = [new Message { Content = "A" }, new Message { Content = "B" }] } + }; + var listB = new List + { + new() { Content = "Hello", Messages = [new Message { Content = "A" }, new Message { Content = "B" }] } + }; + + await TUnitAssert.That(listA).IsEquivalentTo(listB); + } + + [Test] + public async Task Collections_With_Different_Nested_Collections_Are_Not_Equivalent() + { + var listA = new List + { + new() { Content = "Hello", Messages = [new Message { Content = "A" }] } + }; + var listB = new List + { + new() { Content = "Hello", Messages = [new Message { Content = "B" }] } + }; + + await TUnitAssert.That(listA).IsNotEquivalentTo(listB); + } + + [Test] + public async Task Collections_With_ReferenceEqualityComparer_Uses_Reference_Equality() + { + var a = new Message { Content = "Hello" }; + var b = new Message { Content = "Hello" }; + var listA = new List { a }; + var listB = new List { b }; + + var exception = await TUnitAssert.ThrowsAsync( + async () => await TUnitAssert.That(listA).IsEquivalentTo(listB).Using(ReferenceEqualityComparer.Instance) + ); + + await TUnitAssert.That(exception).IsNotNull(); + } + + [Test] + public async Task Collections_With_Same_Reference_And_ReferenceEqualityComparer_Are_Equivalent() + { + var a = new Message { Content = "Hello" }; + var listA = new List { a }; + var listB = new List { a }; + + await TUnitAssert.That(listA).IsEquivalentTo(listB).Using(ReferenceEqualityComparer.Instance); + } + + [Test] + public async Task Primitives_Still_Work_With_Structural_Comparer() + { + var listA = new List { 1, 2, 3 }; + var listB = new List { 1, 2, 3 }; + + await TUnitAssert.That(listA).IsEquivalentTo(listB); + } + + [Test] + public async Task Strings_Still_Work_With_Structural_Comparer() + { + var listA = new List { "a", "b", "c" }; + var listB = new List { "a", "b", "c" }; + + await TUnitAssert.That(listA).IsEquivalentTo(listB); + } + + [Test] + public async Task Collections_With_Equatable_Objects_Use_Equatable_Implementation() + { + var listA = new List { new("Hello"), new("World") }; + var listB = new List { new("Hello"), new("World") }; + + await TUnitAssert.That(listA).IsEquivalentTo(listB); + } + + [Test] + public async Task Collections_With_Null_Items_Are_Equivalent() + { + var listA = new List { new Message { Content = "Hello" }, null, new Message { Content = "World" } }; + var listB = new List { new Message { Content = "Hello" }, null, new Message { Content = "World" } }; + + await TUnitAssert.That(listA).IsEquivalentTo(listB); + } + + [Test] + public async Task Collections_With_Different_Null_Positions_Are_Equivalent_By_Default() + { + var listA = new List { new Message { Content = "Hello" }, null }; + var listB = new List { null, new Message { Content = "Hello" } }; + + await TUnitAssert.That(listA).IsEquivalentTo(listB); + } + + [Test] + public async Task Collections_With_Different_Null_Positions_Are_Not_Equivalent_When_Order_Matters() + { + var listA = new List { new Message { Content = "Hello" }, null }; + var listB = new List { null, new Message { Content = "Hello" } }; + + await TUnitAssert.That(listA).IsNotEquivalentTo(listB, CollectionOrdering.Matching); + } + + [Test] + public async Task Single_Object_IsEquivalentTo_Still_Works_As_Before() + { + var a = new Message { Content = "Hello" }; + var b = new Message { Content = "Hello" }; + + await TUnitAssert.That(a).IsEquivalentTo(b); + } + + [Test] + public async Task Collections_With_Custom_Comparer_Uses_Custom_Comparer() + { + var listA = new List { "hello", "world" }; + var listB = new List { "HELLO", "WORLD" }; + + await TUnitAssert.That(listA).IsEquivalentTo(listB).Using(StringComparer.OrdinalIgnoreCase); + } + public class Message + { + public string? Content { get; set; } + } + + public class MessageWithNested + { + public string? Content { get; set; } + public Message? Nested { get; set; } + } + + public class MessageWithCollection + { + public string? Content { get; set; } + public List? Messages { get; set; } + } + + public class EquatableMessage : IEquatable + { + public string Content { get; } + + public EquatableMessage(string content) + { + Content = content; + } + + public bool Equals(EquatableMessage? other) + { + if (other == null) + { + return false; + } + + return Content == other.Content; + } + + public override bool Equals(object? obj) + { + return Equals(obj as EquatableMessage); + } + + public override int GetHashCode() + { + return Content.GetHashCode(); + } + } +} diff --git a/TUnit.Assertions/Conditions/Helpers/ReferenceEqualityComparer.cs b/TUnit.Assertions/Conditions/Helpers/ReferenceEqualityComparer.cs new file mode 100644 index 0000000000..e9fe1ec31b --- /dev/null +++ b/TUnit.Assertions/Conditions/Helpers/ReferenceEqualityComparer.cs @@ -0,0 +1,29 @@ +namespace TUnit.Assertions.Conditions.Helpers; + +/// +/// An equality comparer that uses reference equality (ReferenceEquals) for comparison. +/// Useful when you want to assert that collections contain the exact same object instances, +/// not just structurally equivalent objects. +/// +/// The type of objects to compare +public sealed class ReferenceEqualityComparer : IEqualityComparer where T : class +{ + /// + /// Singleton instance of the reference equality comparer. + /// + public static readonly ReferenceEqualityComparer Instance = new(); + + private ReferenceEqualityComparer() + { + } + + public bool Equals(T? x, T? y) + { + return ReferenceEquals(x, y); + } + + public int GetHashCode(T obj) + { + return System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(obj); + } +} diff --git a/TUnit.Assertions/Conditions/Helpers/StructuralEqualityComparer.cs b/TUnit.Assertions/Conditions/Helpers/StructuralEqualityComparer.cs new file mode 100644 index 0000000000..9a723cd5de --- /dev/null +++ b/TUnit.Assertions/Conditions/Helpers/StructuralEqualityComparer.cs @@ -0,0 +1,168 @@ +using System.Collections; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; + +namespace TUnit.Assertions.Conditions.Helpers; + +/// +/// An equality comparer that performs structural equivalency comparison for complex objects. +/// For primitive types, strings, dates, enums, and IEquatable types, uses standard equality. +/// For complex objects, performs deep comparison of properties and fields. +/// +/// The type of objects to compare +[RequiresDynamicCode("Structural equality comparison uses reflection to access object members and is not compatible with AOT")] +public sealed class StructuralEqualityComparer : IEqualityComparer +{ + /// + /// Singleton instance of the structural equality comparer. + /// + public static readonly StructuralEqualityComparer Instance = new(); + + private StructuralEqualityComparer() + { + } + + public bool Equals(T? x, T? y) + { + if (x == null && y == null) + { + return true; + } + + if (x == null || y == null) + { + return false; + } + + var type = typeof(T); + + if (IsPrimitiveType(type)) + { + return EqualityComparer.Default.Equals(x, y); + } + + if (typeof(IEquatable).IsAssignableFrom(type)) + { + return ((IEquatable)x).Equals(y); + } + + return CompareStructurally(x, y, new HashSet(new ReferenceEqualityComparer())); + } + + public int GetHashCode(T obj) + { + if (obj == null) + { + return 0; + } + + return EqualityComparer.Default.GetHashCode(obj); + } + + private static bool IsPrimitiveType(Type type) + { + return type.IsPrimitive + || type.IsEnum + || type == typeof(string) + || type == typeof(decimal) + || type == typeof(DateTime) + || type == typeof(DateTimeOffset) + || type == typeof(TimeSpan) + || type == typeof(Guid) +#if NET6_0_OR_GREATER + || type == typeof(DateOnly) + || type == typeof(TimeOnly) +#endif + ; + } + + [UnconditionalSuppressMessage("Trimming", "IL2072", Justification = "GetType() is acceptable for runtime structural comparison")] + private bool CompareStructurally(object? x, object? y, HashSet visited) + { + if (x == null && y == null) + { + return true; + } + + if (x == null || y == null) + { + return false; + } + + var xType = x.GetType(); + var yType = y.GetType(); + + if (IsPrimitiveType(xType)) + { + return Equals(x, y); + } + + if (visited.Contains(x)) + { + return true; + } + + visited.Add(x); + + if (x is IEnumerable xEnumerable && y is IEnumerable yEnumerable + && x is not string && y is not string) + { + var xList = xEnumerable.Cast().ToList(); + var yList = yEnumerable.Cast().ToList(); + + if (xList.Count != yList.Count) + { + return false; + } + + for (int i = 0; i < xList.Count; i++) + { + if (!CompareStructurally(xList[i], yList[i], visited)) + { + return false; + } + } + + return true; + } + + var members = GetMembersToCompare(xType); + + foreach (var member in members) + { + var xValue = GetMemberValue(x, member); + var yValue = GetMemberValue(y, member); + + if (!CompareStructurally(xValue, yValue, visited)) + { + return false; + } + } + + return true; + } + + private static List GetMembersToCompare([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicFields)] Type type) + { + var members = new List(); + members.AddRange(type.GetProperties(BindingFlags.Public | BindingFlags.Instance)); + members.AddRange(type.GetFields(BindingFlags.Public | BindingFlags.Instance)); + return members; + } + + private static object? GetMemberValue(object obj, MemberInfo member) + { + return member switch + { + PropertyInfo prop => prop.GetValue(obj), + FieldInfo field => field.GetValue(obj), + _ => throw new InvalidOperationException($"Unknown member type: {member.GetType()}") + }; + } + + private sealed class ReferenceEqualityComparer : IEqualityComparer + { + public new bool Equals(object? x, object? y) => ReferenceEquals(x, y); + public int GetHashCode(object obj) => System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(obj); + } +} diff --git a/TUnit.Assertions/Conditions/IsEquivalentToAssertion.cs b/TUnit.Assertions/Conditions/IsEquivalentToAssertion.cs index d5ea980baf..1ea55434c3 100644 --- a/TUnit.Assertions/Conditions/IsEquivalentToAssertion.cs +++ b/TUnit.Assertions/Conditions/IsEquivalentToAssertion.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using System.Text; using TUnit.Assertions.Attributes; using TUnit.Assertions.Conditions.Helpers; @@ -13,6 +14,7 @@ namespace TUnit.Assertions.Conditions; /// Inherits from CollectionComparerBasedAssertion to preserve collection type awareness in And/Or chains. /// [AssertionExtension("IsEquivalentTo")] +[RequiresDynamicCode("Collection equivalency uses structural comparison for complex objects, which requires reflection and is not compatible with AOT")] public class IsEquivalentToAssertion : CollectionComparerBasedAssertion { private readonly IEnumerable _expected; @@ -46,6 +48,7 @@ public IsEquivalentToAssertion Using(IEqualityComparer comparer) return this; } + [UnconditionalSuppressMessage("AOT", "IL3050", Justification = "Collection equivalency uses structural comparison which requires reflection")] protected override Task CheckAsync(EvaluationMetadata> metadata) { var value = metadata.Value; @@ -56,11 +59,13 @@ protected override Task CheckAsync(EvaluationMetadata.Instance; + var result = CollectionEquivalencyChecker.AreEquivalent( value, _expected, _ordering, - GetComparer()); + comparer); return Task.FromResult(result.AreEquivalent ? AssertionResult.Passed diff --git a/TUnit.Assertions/Conditions/NotEquivalentToAssertion.cs b/TUnit.Assertions/Conditions/NotEquivalentToAssertion.cs index a6a1cfdbba..e10e1ca2af 100644 --- a/TUnit.Assertions/Conditions/NotEquivalentToAssertion.cs +++ b/TUnit.Assertions/Conditions/NotEquivalentToAssertion.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using System.Text; using TUnit.Assertions.Attributes; using TUnit.Assertions.Conditions.Helpers; @@ -12,6 +13,7 @@ namespace TUnit.Assertions.Conditions; /// Inherits from CollectionComparerBasedAssertion to preserve collection type awareness in And/Or chains. /// [AssertionExtension("IsNotEquivalentTo")] +[RequiresDynamicCode("Collection equivalency uses structural comparison for complex objects, which requires reflection and is not compatible with AOT")] public class NotEquivalentToAssertion : CollectionComparerBasedAssertion { private readonly IEnumerable _notExpected; @@ -33,6 +35,7 @@ public NotEquivalentToAssertion Using(IEqualityComparer comparer) return this; } + [UnconditionalSuppressMessage("AOT", "IL3050", Justification = "Collection equivalency uses structural comparison which requires reflection")] protected override Task CheckAsync(EvaluationMetadata> metadata) { var value = metadata.Value; @@ -43,11 +46,13 @@ protected override Task CheckAsync(EvaluationMetadata.Instance; + var result = CollectionEquivalencyChecker.AreEquivalent( value, _notExpected, _ordering, - GetComparer()); + comparer); // Invert the logic: we want them to NOT be equivalent return Task.FromResult(result.AreEquivalent diff --git a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet10_0.verified.txt b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet10_0.verified.txt index 0b12137d60..cf53b41e7f 100644 --- a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet10_0.verified.txt +++ b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet10_0.verified.txt @@ -977,11 +977,14 @@ namespace .Conditions protected override string GetExpectation() { } public . Using(. comparer) { } } + [.("Collection equivalency uses structural comparison for complex objects, which requ" + + "ires reflection and is not compatible with AOT")] [.("IsEquivalentTo")] public class IsEquivalentToAssertion : . { public IsEquivalentToAssertion(.<.> context, . expected, . ordering = 0) { } public IsEquivalentToAssertion(.<.> context, . expected, . comparer, . ordering = 0) { } + [.("AOT", "IL3050", Justification="Collection equivalency uses structural comparison which requires reflection")] protected override .<.> CheckAsync(.<.> metadata) { } protected override string GetExpectation() { } public . Using(. comparer) { } @@ -1079,10 +1082,13 @@ namespace .Conditions public . IgnoringType( type) { } public . IgnoringType() { } } + [.("Collection equivalency uses structural comparison for complex objects, which requ" + + "ires reflection and is not compatible with AOT")] [.("IsNotEquivalentTo")] public class NotEquivalentToAssertion : . { public NotEquivalentToAssertion(.<.> context, . notExpected, . ordering = 0) { } + [.("AOT", "IL3050", Justification="Collection equivalency uses structural comparison which requires reflection")] protected override .<.> CheckAsync(.<.> metadata) { } protected override string GetExpectation() { } public . Using(. comparer) { } @@ -1503,6 +1509,24 @@ namespace .Conditions public static class WeakReferenceAssertionExtensions { } } namespace . +{ + public sealed class ReferenceEqualityComparer : . + where T : class + { + public static readonly ..ReferenceEqualityComparer Instance; + public bool Equals(T? x, T? y) { } + public int GetHashCode(T obj) { } + } + [.("Structural equality comparison uses reflection to access object members and is no" + + "t compatible with AOT")] + public sealed class StructuralEqualityComparer : . + { + public static readonly ..StructuralEqualityComparer Instance; + public bool Equals(T? x, T? y) { } + public int GetHashCode(T obj) { } + } +} +namespace . { public class CountWrapper : ., .<.> { @@ -2898,7 +2922,11 @@ namespace .Extensions } public static class IsEquivalentToAssertionExtensions { + [.("Collection equivalency uses structural comparison for complex objects, which requ" + + "ires reflection and is not compatible with AOT")] public static . IsEquivalentTo(this .<.> source, . expected, . ordering = 0, [.("expected")] string? expectedExpression = null, [.("ordering")] string? orderingExpression = null) { } + [.("Collection equivalency uses structural comparison for complex objects, which requ" + + "ires reflection and is not compatible with AOT")] public static . IsEquivalentTo(this .<.> source, . expected, . comparer, . ordering = 0, [.("expected")] string? expectedExpression = null, [.("comparer")] string? comparerExpression = null, [.("ordering")] string? orderingExpression = null) { } } public static class IsInAssertionExtensions @@ -2960,6 +2988,8 @@ namespace .Extensions } public static class NotEquivalentToAssertionExtensions { + [.("Collection equivalency uses structural comparison for complex objects, which requ" + + "ires reflection and is not compatible with AOT")] public static . IsNotEquivalentTo(this .<.> source, . notExpected, . ordering = 0, [.("notExpected")] string? notExpectedExpression = null, [.("ordering")] string? orderingExpression = null) { } } public static class NotSameReferenceAssertionExtensions diff --git a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet8_0.verified.txt b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet8_0.verified.txt index e67cd617f7..81b4f7672e 100644 --- a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet8_0.verified.txt +++ b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet8_0.verified.txt @@ -974,11 +974,14 @@ namespace .Conditions protected override string GetExpectation() { } public . Using(. comparer) { } } + [.("Collection equivalency uses structural comparison for complex objects, which requ" + + "ires reflection and is not compatible with AOT")] [.("IsEquivalentTo")] public class IsEquivalentToAssertion : . { public IsEquivalentToAssertion(.<.> context, . expected, . ordering = 0) { } public IsEquivalentToAssertion(.<.> context, . expected, . comparer, . ordering = 0) { } + [.("AOT", "IL3050", Justification="Collection equivalency uses structural comparison which requires reflection")] protected override .<.> CheckAsync(.<.> metadata) { } protected override string GetExpectation() { } public . Using(. comparer) { } @@ -1076,10 +1079,13 @@ namespace .Conditions public . IgnoringType( type) { } public . IgnoringType() { } } + [.("Collection equivalency uses structural comparison for complex objects, which requ" + + "ires reflection and is not compatible with AOT")] [.("IsNotEquivalentTo")] public class NotEquivalentToAssertion : . { public NotEquivalentToAssertion(.<.> context, . notExpected, . ordering = 0) { } + [.("AOT", "IL3050", Justification="Collection equivalency uses structural comparison which requires reflection")] protected override .<.> CheckAsync(.<.> metadata) { } protected override string GetExpectation() { } public . Using(. comparer) { } @@ -1500,6 +1506,24 @@ namespace .Conditions public static class WeakReferenceAssertionExtensions { } } namespace . +{ + public sealed class ReferenceEqualityComparer : . + where T : class + { + public static readonly ..ReferenceEqualityComparer Instance; + public bool Equals(T? x, T? y) { } + public int GetHashCode(T obj) { } + } + [.("Structural equality comparison uses reflection to access object members and is no" + + "t compatible with AOT")] + public sealed class StructuralEqualityComparer : . + { + public static readonly ..StructuralEqualityComparer Instance; + public bool Equals(T? x, T? y) { } + public int GetHashCode(T obj) { } + } +} +namespace . { public class CountWrapper : ., .<.> { @@ -2889,7 +2913,11 @@ namespace .Extensions } public static class IsEquivalentToAssertionExtensions { + [.("Collection equivalency uses structural comparison for complex objects, which requ" + + "ires reflection and is not compatible with AOT")] public static . IsEquivalentTo(this .<.> source, . expected, . ordering = 0, [.("expected")] string? expectedExpression = null, [.("ordering")] string? orderingExpression = null) { } + [.("Collection equivalency uses structural comparison for complex objects, which requ" + + "ires reflection and is not compatible with AOT")] public static . IsEquivalentTo(this .<.> source, . expected, . comparer, . ordering = 0, [.("expected")] string? expectedExpression = null, [.("comparer")] string? comparerExpression = null, [.("ordering")] string? orderingExpression = null) { } } public static class IsInAssertionExtensions @@ -2950,6 +2978,8 @@ namespace .Extensions } public static class NotEquivalentToAssertionExtensions { + [.("Collection equivalency uses structural comparison for complex objects, which requ" + + "ires reflection and is not compatible with AOT")] public static . IsNotEquivalentTo(this .<.> source, . notExpected, . ordering = 0, [.("notExpected")] string? notExpectedExpression = null, [.("ordering")] string? orderingExpression = null) { } } public static class NotSameReferenceAssertionExtensions diff --git a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet9_0.verified.txt b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet9_0.verified.txt index 83a4bbae0f..98f26c15ce 100644 --- a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet9_0.verified.txt +++ b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet9_0.verified.txt @@ -977,11 +977,14 @@ namespace .Conditions protected override string GetExpectation() { } public . Using(. comparer) { } } + [.("Collection equivalency uses structural comparison for complex objects, which requ" + + "ires reflection and is not compatible with AOT")] [.("IsEquivalentTo")] public class IsEquivalentToAssertion : . { public IsEquivalentToAssertion(.<.> context, . expected, . ordering = 0) { } public IsEquivalentToAssertion(.<.> context, . expected, . comparer, . ordering = 0) { } + [.("AOT", "IL3050", Justification="Collection equivalency uses structural comparison which requires reflection")] protected override .<.> CheckAsync(.<.> metadata) { } protected override string GetExpectation() { } public . Using(. comparer) { } @@ -1079,10 +1082,13 @@ namespace .Conditions public . IgnoringType( type) { } public . IgnoringType() { } } + [.("Collection equivalency uses structural comparison for complex objects, which requ" + + "ires reflection and is not compatible with AOT")] [.("IsNotEquivalentTo")] public class NotEquivalentToAssertion : . { public NotEquivalentToAssertion(.<.> context, . notExpected, . ordering = 0) { } + [.("AOT", "IL3050", Justification="Collection equivalency uses structural comparison which requires reflection")] protected override .<.> CheckAsync(.<.> metadata) { } protected override string GetExpectation() { } public . Using(. comparer) { } @@ -1503,6 +1509,24 @@ namespace .Conditions public static class WeakReferenceAssertionExtensions { } } namespace . +{ + public sealed class ReferenceEqualityComparer : . + where T : class + { + public static readonly ..ReferenceEqualityComparer Instance; + public bool Equals(T? x, T? y) { } + public int GetHashCode(T obj) { } + } + [.("Structural equality comparison uses reflection to access object members and is no" + + "t compatible with AOT")] + public sealed class StructuralEqualityComparer : . + { + public static readonly ..StructuralEqualityComparer Instance; + public bool Equals(T? x, T? y) { } + public int GetHashCode(T obj) { } + } +} +namespace . { public class CountWrapper : ., .<.> { @@ -2898,7 +2922,11 @@ namespace .Extensions } public static class IsEquivalentToAssertionExtensions { + [.("Collection equivalency uses structural comparison for complex objects, which requ" + + "ires reflection and is not compatible with AOT")] public static . IsEquivalentTo(this .<.> source, . expected, . ordering = 0, [.("expected")] string? expectedExpression = null, [.("ordering")] string? orderingExpression = null) { } + [.("Collection equivalency uses structural comparison for complex objects, which requ" + + "ires reflection and is not compatible with AOT")] public static . IsEquivalentTo(this .<.> source, . expected, . comparer, . ordering = 0, [.("expected")] string? expectedExpression = null, [.("comparer")] string? comparerExpression = null, [.("ordering")] string? orderingExpression = null) { } } public static class IsInAssertionExtensions @@ -2960,6 +2988,8 @@ namespace .Extensions } public static class NotEquivalentToAssertionExtensions { + [.("Collection equivalency uses structural comparison for complex objects, which requ" + + "ires reflection and is not compatible with AOT")] public static . IsNotEquivalentTo(this .<.> source, . notExpected, . ordering = 0, [.("notExpected")] string? notExpectedExpression = null, [.("ordering")] string? orderingExpression = null) { } } public static class NotSameReferenceAssertionExtensions diff --git a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.Net4_7.verified.txt b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.Net4_7.verified.txt index 1ac99701fd..96fa125ba7 100644 --- a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.Net4_7.verified.txt +++ b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.Net4_7.verified.txt @@ -1412,6 +1412,22 @@ namespace .Conditions public static class WeakReferenceAssertionExtensions { } } namespace . +{ + public sealed class ReferenceEqualityComparer : . + where T : class + { + public static readonly ..ReferenceEqualityComparer Instance; + public bool Equals(T? x, T? y) { } + public int GetHashCode(T obj) { } + } + public sealed class StructuralEqualityComparer : . + { + public static readonly ..StructuralEqualityComparer Instance; + public bool Equals(T? x, T? y) { } + public int GetHashCode(T obj) { } + } +} +namespace . { public class CountWrapper : ., .<.> {