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) { }