diff --git a/TUnit.Assertions.Tests/MemoryAssertionTests.cs b/TUnit.Assertions.Tests/MemoryAssertionTests.cs index 944b2645c0..de703c1341 100644 --- a/TUnit.Assertions.Tests/MemoryAssertionTests.cs +++ b/TUnit.Assertions.Tests/MemoryAssertionTests.cs @@ -1,4 +1,6 @@ #if NET5_0_OR_GREATER +using TUnit.Assertions.Enums; + namespace TUnit.Assertions.Tests; public class MemoryAssertionTests @@ -131,6 +133,68 @@ public async Task Test_ReadOnlyMemory_Chaining_With_And() await Assert.That(memory).IsNotEmpty().And.Contains(2); } + // IsEquivalentTo tests + [Test] + public async Task Test_Memory_IsEquivalentTo() + { + Memory memory = new[] { 3, 1, 2 }; + await Assert.That(memory).IsEquivalentTo(new[] { 1, 2, 3 }); + } + + [Test] + public async Task Test_Memory_IsEquivalentTo_Ordered() + { + Memory memory = new[] { 1, 2, 3 }; + await Assert.That(memory).IsEquivalentTo(new[] { 1, 2, 3 }, CollectionOrdering.Matching); + } + + [Test] + public async Task Test_Memory_IsEquivalentTo_Fails() + { + Memory memory = new[] { 1, 2, 3 }; + var action = async () => await Assert.That(memory).IsEquivalentTo(new[] { 1, 2, 4 }); + + var exception = await Assert.That(action).Throws(); + + await Assert.That(exception.Message).Contains("does not contain expected item: 4"); + } + + [Test] + public async Task Test_Memory_IsEquivalentTo_DifferentCount_Fails() + { + Memory memory = new[] { 1, 2, 3 }; + var action = async () => await Assert.That(memory).IsEquivalentTo(new[] { 1, 2 }); + + var exception = await Assert.That(action).Throws(); + + await Assert.That(exception.Message).Contains("3 items but expected 2"); + } + + [Test] + public async Task Test_ReadOnlyMemory_IsEquivalentTo() + { + ReadOnlyMemory memory = new byte[] { 0x01, 0x02, 0x03 }; + await Assert.That(memory).IsEquivalentTo(new byte[] { 0x01, 0x02, 0x03 }); + } + + [Test] + public async Task Test_ReadOnlyMemory_IsEquivalentTo_Unordered() + { + ReadOnlyMemory memory = new byte[] { 0x03, 0x01, 0x02 }; + await Assert.That(memory).IsEquivalentTo(new byte[] { 0x01, 0x02, 0x03 }); + } + + [Test] + public async Task Test_ReadOnlyMemory_IsEquivalentTo_Fails() + { + ReadOnlyMemory memory = new byte[] { 0x01, 0x02, 0x03 }; + var action = async () => await Assert.That(memory).IsEquivalentTo(new byte[] { 0x01, 0x02, 0x04 }); + + var exception = await Assert.That(action).Throws(); + + await Assert.That(exception.Message).Contains("does not contain expected item"); + } + // Failure tests [Test] public async Task Test_Memory_IsEmpty_Fails() diff --git a/TUnit.Assertions/Collections/MemoryAssertions.cs b/TUnit.Assertions/Collections/MemoryAssertions.cs index 2748581bfa..0cbaf3b12a 100644 --- a/TUnit.Assertions/Collections/MemoryAssertions.cs +++ b/TUnit.Assertions/Collections/MemoryAssertions.cs @@ -1,6 +1,9 @@ #if NET5_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; using TUnit.Assertions.Abstractions; +using TUnit.Assertions.Conditions.Helpers; using TUnit.Assertions.Core; +using TUnit.Assertions.Enums; using TUnit.Assertions.Sources; namespace TUnit.Assertions.Collections; @@ -161,6 +164,71 @@ protected override Task CheckAsync(EvaluationMetadata protected override string GetExpectation() => $"to not contain {_expected}"; } +/// +/// Asserts that a memory is equivalent to an expected collection. +/// +public class MemoryIsEquivalentToAssertion : MemoryAssertionBase +{ + private readonly Func> _adapterFactory; + private readonly IEnumerable _expected; + private readonly IEqualityComparer _comparer; + private readonly CollectionOrdering _ordering; + + [RequiresUnreferencedCode("Collection equivalency uses structural comparison for complex objects, which requires reflection and is not compatible with AOT")] + public MemoryIsEquivalentToAssertion( + AssertionContext context, + Func> adapterFactory, + IEnumerable expected, + CollectionOrdering ordering = CollectionOrdering.Any) + : this(context, adapterFactory, expected, StructuralEqualityComparer.Instance, ordering) + { + } + + public MemoryIsEquivalentToAssertion( + AssertionContext context, + Func> adapterFactory, + IEnumerable expected, + IEqualityComparer comparer, + CollectionOrdering ordering = CollectionOrdering.Any) + : base(context) + { + _adapterFactory = adapterFactory; + _expected = expected ?? throw new ArgumentNullException(nameof(expected)); + _comparer = comparer; + _ordering = ordering; + } + + protected override ICollectionAdapter CreateAdapter(TMemory value) => _adapterFactory(value); + + protected override Task CheckAsync(EvaluationMetadata metadata) + { + if (metadata.Exception != null) + { + return Task.FromResult(AssertionResult.Failed($"threw {metadata.Exception.GetType().Name}")); + } + + if (metadata.Value == null) + { + return Task.FromResult(AssertionResult.Failed("value was null")); + } + + var adapter = _adapterFactory(metadata.Value); + + var result = CollectionEquivalencyChecker.AreEquivalent( + adapter.AsEnumerable(), + _expected, + _ordering, + _comparer); + + return Task.FromResult(result.AreEquivalent + ? AssertionResult.Passed + : AssertionResult.Failed(result.ErrorMessage!)); + } + + protected override string GetExpectation() => + $"to be equivalent to [{string.Join(", ", _expected)}]"; +} + /// /// Asserts that a memory has exactly one item. /// diff --git a/TUnit.Assertions/Sources/MemoryAssertionBase.cs b/TUnit.Assertions/Sources/MemoryAssertionBase.cs index deadf3bc32..821c27e907 100644 --- a/TUnit.Assertions/Sources/MemoryAssertionBase.cs +++ b/TUnit.Assertions/Sources/MemoryAssertionBase.cs @@ -1,4 +1,5 @@ #if NET5_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Text; using TUnit.Assertions.Abstractions; @@ -6,6 +7,7 @@ using TUnit.Assertions.Collections; using TUnit.Assertions.Conditions; using TUnit.Assertions.Core; +using TUnit.Assertions.Enums; namespace TUnit.Assertions.Sources; @@ -211,6 +213,66 @@ public MemoryIsInDescendingOrderAssertion IsInDescendingOrder() return new MemoryIsInDescendingOrderAssertion(Context, CreateAdapter); } + /// + /// Asserts that the memory is equivalent to the expected collection. + /// Two collections are equivalent if they contain the same elements, regardless of order (default). + /// + [RequiresUnreferencedCode("Collection equivalency uses structural comparison for complex objects, which requires reflection and is not compatible with AOT")] + public MemoryIsEquivalentToAssertion IsEquivalentTo( + IEnumerable expected, + CollectionOrdering ordering = CollectionOrdering.Any, + [CallerArgumentExpression(nameof(expected))] string? expectedExpression = null, + [CallerArgumentExpression(nameof(ordering))] string? orderingExpression = null) + { + Context.ExpressionBuilder.Append(".IsEquivalentTo("); + var added = false; + if (expectedExpression != null) + { + Context.ExpressionBuilder.Append(expectedExpression); + added = true; + } + if (orderingExpression != null) + { + Context.ExpressionBuilder.Append(added ? ", " : ""); + Context.ExpressionBuilder.Append(orderingExpression); + } + Context.ExpressionBuilder.Append(')'); + return new MemoryIsEquivalentToAssertion(Context, CreateAdapter, expected, ordering); + } + + /// + /// Asserts that the memory is equivalent to the expected collection using a custom equality comparer. + /// + public MemoryIsEquivalentToAssertion IsEquivalentTo( + IEnumerable expected, + IEqualityComparer comparer, + CollectionOrdering ordering = CollectionOrdering.Any, + [CallerArgumentExpression(nameof(expected))] string? expectedExpression = null, + [CallerArgumentExpression(nameof(comparer))] string? comparerExpression = null, + [CallerArgumentExpression(nameof(ordering))] string? orderingExpression = null) + { + Context.ExpressionBuilder.Append(".IsEquivalentTo("); + var added = false; + if (expectedExpression != null) + { + Context.ExpressionBuilder.Append(expectedExpression); + added = true; + } + if (comparerExpression != null) + { + Context.ExpressionBuilder.Append(added ? ", " : ""); + Context.ExpressionBuilder.Append(comparerExpression); + added = true; + } + if (orderingExpression != null) + { + Context.ExpressionBuilder.Append(added ? ", " : ""); + Context.ExpressionBuilder.Append(orderingExpression); + } + Context.ExpressionBuilder.Append(')'); + return new MemoryIsEquivalentToAssertion(Context, CreateAdapter, expected, comparer, ordering); + } + /// /// Asserts that all items in the memory are distinct. /// 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 2504fe78e4..adff69eaad 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 @@ -567,6 +567,16 @@ namespace .Collections protected override . CreateAdapter(TMemory value) { } protected override string GetExpectation() { } } + public class MemoryIsEquivalentToAssertion : . + { + [.("Collection equivalency uses structural comparison for complex objects, which requ" + + "ires reflection and is not compatible with AOT")] + public MemoryIsEquivalentToAssertion(. context, > adapterFactory, . expected, . ordering = 0) { } + public MemoryIsEquivalentToAssertion(. context, > adapterFactory, . expected, . comparer, . ordering = 0) { } + protected override .<.> CheckAsync(. metadata) { } + protected override . CreateAdapter(TMemory value) { } + protected override string GetExpectation() { } + } public class MemoryIsInDescendingOrderAssertion : . { public MemoryIsInDescendingOrderAssertion(. context, > adapterFactory, .? comparer = null) { } @@ -5973,6 +5983,10 @@ namespace .Sources public . IsAssignableFrom() { } public . IsAssignableTo() { } public . IsEmpty() { } + [.("Collection equivalency uses structural comparison for complex objects, which requ" + + "ires reflection and is not compatible with AOT")] + public . IsEquivalentTo(. expected, . ordering = 0, [.("expected")] string? expectedExpression = null, [.("ordering")] string? orderingExpression = null) { } + public . IsEquivalentTo(. expected, . comparer, . ordering = 0, [.("expected")] string? expectedExpression = null, [.("comparer")] string? comparerExpression = null, [.("ordering")] string? orderingExpression = null) { } public . IsInDescendingOrder() { } public . IsInOrder() { } public . IsNotAssignableFrom() { } 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 aabee6e0f3..f02b43e3d4 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 @@ -550,6 +550,16 @@ namespace .Collections protected override . CreateAdapter(TMemory value) { } protected override string GetExpectation() { } } + public class MemoryIsEquivalentToAssertion : . + { + [.("Collection equivalency uses structural comparison for complex objects, which requ" + + "ires reflection and is not compatible with AOT")] + public MemoryIsEquivalentToAssertion(. context, > adapterFactory, . expected, . ordering = 0) { } + public MemoryIsEquivalentToAssertion(. context, > adapterFactory, . expected, . comparer, . ordering = 0) { } + protected override .<.> CheckAsync(. metadata) { } + protected override . CreateAdapter(TMemory value) { } + protected override string GetExpectation() { } + } public class MemoryIsInDescendingOrderAssertion : . { public MemoryIsInDescendingOrderAssertion(. context, > adapterFactory, .? comparer = null) { } @@ -5920,6 +5930,10 @@ namespace .Sources public . IsAssignableFrom() { } public . IsAssignableTo() { } public . IsEmpty() { } + [.("Collection equivalency uses structural comparison for complex objects, which requ" + + "ires reflection and is not compatible with AOT")] + public . IsEquivalentTo(. expected, . ordering = 0, [.("expected")] string? expectedExpression = null, [.("ordering")] string? orderingExpression = null) { } + public . IsEquivalentTo(. expected, . comparer, . ordering = 0, [.("expected")] string? expectedExpression = null, [.("comparer")] string? comparerExpression = null, [.("ordering")] string? orderingExpression = null) { } public . IsInDescendingOrder() { } public . IsInOrder() { } public . IsNotAssignableFrom() { } 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 e609932ca1..3cb7308e6f 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 @@ -567,6 +567,16 @@ namespace .Collections protected override . CreateAdapter(TMemory value) { } protected override string GetExpectation() { } } + public class MemoryIsEquivalentToAssertion : . + { + [.("Collection equivalency uses structural comparison for complex objects, which requ" + + "ires reflection and is not compatible with AOT")] + public MemoryIsEquivalentToAssertion(. context, > adapterFactory, . expected, . ordering = 0) { } + public MemoryIsEquivalentToAssertion(. context, > adapterFactory, . expected, . comparer, . ordering = 0) { } + protected override .<.> CheckAsync(. metadata) { } + protected override . CreateAdapter(TMemory value) { } + protected override string GetExpectation() { } + } public class MemoryIsInDescendingOrderAssertion : . { public MemoryIsInDescendingOrderAssertion(. context, > adapterFactory, .? comparer = null) { } @@ -5973,6 +5983,10 @@ namespace .Sources public . IsAssignableFrom() { } public . IsAssignableTo() { } public . IsEmpty() { } + [.("Collection equivalency uses structural comparison for complex objects, which requ" + + "ires reflection and is not compatible with AOT")] + public . IsEquivalentTo(. expected, . ordering = 0, [.("expected")] string? expectedExpression = null, [.("ordering")] string? orderingExpression = null) { } + public . IsEquivalentTo(. expected, . comparer, . ordering = 0, [.("expected")] string? expectedExpression = null, [.("comparer")] string? comparerExpression = null, [.("ordering")] string? orderingExpression = null) { } public . IsInDescendingOrder() { } public . IsInOrder() { } public . IsNotAssignableFrom() { }