diff --git a/TUnit.Assertions.Tests/Bugs/Tests3367.cs b/TUnit.Assertions.Tests/Bugs/Tests3367.cs new file mode 100644 index 0000000000..6b22e3e2c8 --- /dev/null +++ b/TUnit.Assertions.Tests/Bugs/Tests3367.cs @@ -0,0 +1,84 @@ +using TUnit.Assertions.Enums; + +namespace TUnit.Assertions.Tests.Bugs; + +public class Tests3367 +{ + /// + /// Custom comparer for doubles with tolerance. + /// Note: This comparer intentionally does NOT implement GetHashCode correctly + /// for tolerance-based equality, which is extremely difficult to do correctly. + /// TUnit should handle this gracefully. + /// + public class DoubleComparer(double tolerance) : IEqualityComparer + { + private readonly double _tolerance = tolerance; + + public bool Equals(double x, double y) => Math.Abs(x - y) <= _tolerance; + + public int GetHashCode(double obj) => obj.GetHashCode(); + } + + [Test] + public async Task IsEquivalentTo_WithCustomComparer_SingleElement_ShouldSucceed() + { + // Arrange + var comparer = new DoubleComparer(0.0001); + double value1 = 0.29999999999999999; + double value2 = 0.30000000000000004; + + // Act & Assert - single element comparison works + await TUnitAssert.That(comparer.Equals(value1, value2)).IsTrue(); + } + + [Test] + public async Task IsEquivalentTo_WithCustomComparer_Array_ShouldSucceed() + { + // Arrange + var comparer = new DoubleComparer(0.0001); + double[] array1 = [0.1, 0.2, 0.29999999999999999]; + double[] array2 = [0.1, 0.2, 0.30000000000000004]; + + // Act & Assert - array comparison should work with custom comparer + await TUnitAssert.That(array1).IsEquivalentTo(array2).Using(comparer); + } + + [Test] + public async Task IsEquivalentTo_WithCustomComparer_Array_DifferentOrder_ShouldSucceed() + { + // Arrange + var comparer = new DoubleComparer(0.0001); + double[] array1 = [0.1, 0.29999999999999999, 0.2]; + double[] array2 = [0.2, 0.1, 0.30000000000000004]; + + // Act & Assert - should work regardless of order + await TUnitAssert.That(array1).IsEquivalentTo(array2, CollectionOrdering.Any).Using(comparer); + } + + [Test] + public async Task IsEquivalentTo_WithCustomComparer_Array_NotEquivalent_ShouldFail() + { + // Arrange + var comparer = new DoubleComparer(0.0001); + double[] array1 = [0.1, 0.2, 0.3]; + double[] array2 = [0.1, 0.2, 0.5]; // 0.5 is not within tolerance of 0.3 + + // Act & Assert - should fail when values are not within tolerance + var exception = await TUnitAssert.ThrowsAsync( + async () => await TUnitAssert.That(array1).IsEquivalentTo(array2).Using(comparer)); + + await TUnitAssert.That(exception).IsNotNull(); + } + + [Test] + public async Task IsEquivalentTo_WithCustomComparer_DuplicateValues_ShouldSucceed() + { + // Arrange + var comparer = new DoubleComparer(0.0001); + double[] array1 = [0.1, 0.2, 0.2, 0.3]; + double[] array2 = [0.1, 0.20000000000000001, 0.19999999999999999, 0.30000000000000004]; + + // Act & Assert - should handle duplicates correctly + await TUnitAssert.That(array1).IsEquivalentTo(array2, CollectionOrdering.Any).Using(comparer); + } +} diff --git a/TUnit.Assertions.Tests/Old/EquivalentAssertionTests.cs b/TUnit.Assertions.Tests/Old/EquivalentAssertionTests.cs index c6ec270139..2fedf9fedd 100644 --- a/TUnit.Assertions.Tests/Old/EquivalentAssertionTests.cs +++ b/TUnit.Assertions.Tests/Old/EquivalentAssertionTests.cs @@ -95,7 +95,7 @@ public async Task Different_Dictionaries_Are_Equivalent_With_Different_Ordered_K { "A", "A" }, }; - await TUnitAssert.That(dict1).IsEquivalentTo(dict2); + await TUnitAssert.That(dict1).IsEquivalentTo(dict2, CollectionOrdering.Any); } [Test] diff --git a/TUnit.Assertions/Conditions/IsEquivalentToAssertion.cs b/TUnit.Assertions/Conditions/IsEquivalentToAssertion.cs index f3873dc662..c2597c0209 100644 --- a/TUnit.Assertions/Conditions/IsEquivalentToAssertion.cs +++ b/TUnit.Assertions/Conditions/IsEquivalentToAssertion.cs @@ -19,7 +19,7 @@ public class IsEquivalentToAssertion : Assertion context, IEnumerable expected, - CollectionOrdering ordering = CollectionOrdering.Any) + CollectionOrdering ordering = CollectionOrdering.Matching) : base(context) { _expected = expected ?? throw new ArgumentNullException(nameof(expected)); @@ -82,7 +82,50 @@ protected override Task CheckAsync(EvaluationMetadata(actualList); + + foreach (var expectedItem in expectedList) + { + var foundIndex = -1; + for (int i = 0; i < remainingActual.Count; i++) + { + var actualItem = remainingActual[i]; + + bool areEqual = expectedItem == null && actualItem == null || + expectedItem != null && actualItem != null && comparer.Equals(expectedItem, actualItem); + + if (areEqual) + { + foundIndex = i; + break; + } + } + + if (foundIndex == -1) + { + return Task.FromResult(AssertionResult.Failed( + $"collection does not contain expected item: {expectedItem}")); + } + + remainingActual.RemoveAt(foundIndex); + } + + return Task.FromResult(AssertionResult.Passed); + } + + // Use efficient Dictionary-based frequency map for default comparer - O(n) + // Build a frequency map of actual items // Track null count separately to avoid Dictionary notnull constraint int nullCount = 0; #pragma warning disable CS8714 // Nullability of type argument doesn't match 'notnull' constraint - we handle nulls separately 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 1d47957be5..c01ff4e910 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 @@ -861,7 +861,7 @@ namespace .Conditions public class IsEquivalentToAssertion : . where TCollection : . { - public IsEquivalentToAssertion(. context, . expected, . ordering = 0) { } + public IsEquivalentToAssertion(. context, . expected, . ordering = 1) { } protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } public . Using(. comparer) { } 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 f14350f52a..5435185072 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 @@ -861,7 +861,7 @@ namespace .Conditions public class IsEquivalentToAssertion : . where TCollection : . { - public IsEquivalentToAssertion(. context, . expected, . ordering = 0) { } + public IsEquivalentToAssertion(. context, . expected, . ordering = 1) { } protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } public . Using(. comparer) { } 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 8d7aa05390..ae6311651f 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 @@ -861,7 +861,7 @@ namespace .Conditions public class IsEquivalentToAssertion : . where TCollection : . { - public IsEquivalentToAssertion(. context, . expected, . ordering = 0) { } + public IsEquivalentToAssertion(. context, . expected, . ordering = 1) { } protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } public . Using(. comparer) { } 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 9023e92f6f..4ed2e80a9e 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 @@ -821,7 +821,7 @@ namespace .Conditions public class IsEquivalentToAssertion : . where TCollection : . { - public IsEquivalentToAssertion(. context, . expected, . ordering = 0) { } + public IsEquivalentToAssertion(. context, . expected, . ordering = 1) { } protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } public . Using(. comparer) { }