diff --git a/Directory.Packages.props b/Directory.Packages.props index af96d34789..c8364fa4fc 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -68,10 +68,10 @@ - - - - + + + + diff --git a/README.md b/README.md index 7c66388d4a..5c8d78618d 100644 --- a/README.md +++ b/README.md @@ -289,12 +289,12 @@ Apple M1 (Virtual), 1 CPU, 3 logical and 3 physical cores Job=.NET 9.0 Runtime=.NET 9.0 ``` -| Method | Mean | Error | StdDev | -|------------- |---------:|---------:|---------:| -| Build_TUnit | 981.9 ms | 19.54 ms | 37.64 ms | -| Build_NUnit | 794.8 ms | 12.52 ms | 10.45 ms | -| Build_xUnit | 785.2 ms | 8.75 ms | 7.30 ms | -| Build_MSTest | 838.8 ms | 11.85 ms | 9.90 ms | +| Method | Mean | Error | StdDev | Median | +|------------- |-----------:|---------:|----------:|-----------:| +| Build_TUnit | 1,160.1 ms | 63.92 ms | 183.40 ms | 1,089.8 ms | +| Build_NUnit | 789.3 ms | 10.36 ms | 8.65 ms | 789.9 ms | +| Build_xUnit | 829.0 ms | 16.51 ms | 28.91 ms | 824.6 ms | +| Build_MSTest | 842.7 ms | 16.83 ms | 42.83 ms | 831.1 ms | @@ -313,10 +313,10 @@ Job=.NET 9.0 Runtime=.NET 9.0 ``` | Method | Mean | Error | StdDev | |------------- |--------:|---------:|---------:| -| Build_TUnit | 1.899 s | 0.0209 s | 0.0174 s | -| Build_NUnit | 1.486 s | 0.0216 s | 0.0192 s | -| Build_xUnit | 1.492 s | 0.0121 s | 0.0113 s | -| Build_MSTest | 1.511 s | 0.0107 s | 0.0100 s | +| Build_TUnit | 1.911 s | 0.0349 s | 0.0343 s | +| Build_NUnit | 1.481 s | 0.0173 s | 0.0153 s | +| Build_xUnit | 1.514 s | 0.0215 s | 0.0201 s | +| Build_MSTest | 1.514 s | 0.0151 s | 0.0141 s | @@ -335,10 +335,10 @@ Job=.NET 9.0 Runtime=.NET 9.0 ``` | Method | Mean | Error | StdDev | |------------- |--------:|---------:|---------:| -| Build_TUnit | 1.887 s | 0.0374 s | 0.0350 s | -| Build_NUnit | 1.476 s | 0.0238 s | 0.0223 s | -| Build_xUnit | 1.472 s | 0.0237 s | 0.0210 s | -| Build_MSTest | 1.521 s | 0.0152 s | 0.0143 s | +| Build_TUnit | 1.914 s | 0.0380 s | 0.0568 s | +| Build_NUnit | 1.485 s | 0.0254 s | 0.0226 s | +| Build_xUnit | 1.486 s | 0.0121 s | 0.0101 s | +| Build_MSTest | 1.541 s | 0.0131 s | 0.0116 s | ### Scenario: A single test that completes instantly (including spawning a new process and initialising the test framework) @@ -356,13 +356,13 @@ Apple M1 (Virtual), 1 CPU, 3 logical and 3 physical cores Job=.NET 9.0 Runtime=.NET 9.0 ``` -| Method | Mean | Error | StdDev | -|---------- |----------:|---------:|---------:| -| TUnit_AOT | 75.67 ms | 1.481 ms | 1.455 ms | -| TUnit | 479.63 ms | 5.521 ms | 4.894 ms | -| NUnit | 699.32 ms | 8.003 ms | 7.095 ms | -| xUnit | 723.38 ms | 6.808 ms | 6.035 ms | -| MSTest | 628.77 ms | 7.284 ms | 6.457 ms | +| Method | Mean | Error | StdDev | +|---------- |----------:|----------:|----------:| +| TUnit_AOT | 69.20 ms | 0.754 ms | 0.589 ms | +| TUnit | 491.47 ms | 9.787 ms | 18.620 ms | +| NUnit | 703.36 ms | 9.870 ms | 7.706 ms | +| xUnit | 725.22 ms | 7.797 ms | 6.912 ms | +| MSTest | 623.46 ms | 12.439 ms | 11.026 ms | @@ -381,11 +381,11 @@ Job=.NET 9.0 Runtime=.NET 9.0 ``` | Method | Mean | Error | StdDev | |---------- |------------:|----------:|----------:| -| TUnit_AOT | 24.69 ms | 0.491 ms | 1.233 ms | -| TUnit | 829.43 ms | 16.315 ms | 26.346 ms | -| NUnit | 1,286.61 ms | 10.981 ms | 10.272 ms | -| xUnit | 1,341.42 ms | 16.429 ms | 15.367 ms | -| MSTest | 1,144.21 ms | 8.782 ms | 8.215 ms | +| TUnit_AOT | 24.11 ms | 0.479 ms | 1.319 ms | +| TUnit | 826.41 ms | 16.113 ms | 18.555 ms | +| NUnit | 1,290.46 ms | 9.301 ms | 8.700 ms | +| xUnit | 1,347.28 ms | 9.441 ms | 7.883 ms | +| MSTest | 1,148.47 ms | 12.201 ms | 11.413 ms | @@ -402,13 +402,13 @@ AMD EPYC 7763, 1 CPU, 4 logical and 2 physical cores Job=.NET 9.0 Runtime=.NET 9.0 ``` -| Method | Mean | Error | StdDev | -|---------- |------------:|----------:|----------:| -| TUnit_AOT | 57.57 ms | 2.075 ms | 6.119 ms | -| TUnit | 869.51 ms | 16.896 ms | 25.289 ms | -| NUnit | 1,311.68 ms | 8.231 ms | 7.296 ms | -| xUnit | 1,366.04 ms | 20.997 ms | 19.641 ms | -| MSTest | 1,169.46 ms | 8.864 ms | 7.402 ms | +| Method | Mean | Error | StdDev | Median | +|---------- |------------:|----------:|----------:|------------:| +| TUnit_AOT | 55.51 ms | 1.773 ms | 5.173 ms | 53.87 ms | +| TUnit | 868.51 ms | 16.877 ms | 26.768 ms | 870.72 ms | +| NUnit | 1,337.78 ms | 19.672 ms | 18.401 ms | 1,334.68 ms | +| xUnit | 1,376.66 ms | 14.994 ms | 13.292 ms | 1,374.15 ms | +| MSTest | 1,181.27 ms | 12.312 ms | 11.516 ms | 1,177.15 ms | ### Scenario: A test that takes 50ms to execute, repeated 100 times (including spawning a new process and initialising the test framework) @@ -428,11 +428,11 @@ Job=.NET 9.0 Runtime=.NET 9.0 ``` | Method | Mean | Error | StdDev | |---------- |------------:|----------:|----------:| -| TUnit_AOT | 246.7 ms | 14.07 ms | 41.49 ms | -| TUnit | 738.7 ms | 31.08 ms | 91.64 ms | -| NUnit | 14,125.2 ms | 276.86 ms | 484.90 ms | -| xUnit | 14,574.1 ms | 290.60 ms | 573.61 ms | -| MSTest | 14,539.4 ms | 285.42 ms | 543.04 ms | +| TUnit_AOT | 228.0 ms | 11.88 ms | 35.04 ms | +| TUnit | 642.0 ms | 19.77 ms | 58.28 ms | +| NUnit | 14,360.7 ms | 283.06 ms | 584.57 ms | +| xUnit | 14,502.6 ms | 287.08 ms | 532.12 ms | +| MSTest | 14,499.4 ms | 285.50 ms | 570.18 ms | @@ -451,11 +451,11 @@ Job=.NET 9.0 Runtime=.NET 9.0 ``` | Method | Mean | Error | StdDev | |---------- |------------:|----------:|----------:| -| TUnit_AOT | 73.59 ms | 0.377 ms | 0.294 ms | -| TUnit | 895.14 ms | 17.563 ms | 17.250 ms | -| NUnit | 6,287.98 ms | 12.844 ms | 12.014 ms | -| xUnit | 6,427.61 ms | 14.939 ms | 13.974 ms | -| MSTest | 6,260.22 ms | 9.162 ms | 8.122 ms | +| TUnit_AOT | 74.01 ms | 0.711 ms | 0.594 ms | +| TUnit | 891.52 ms | 17.564 ms | 18.793 ms | +| NUnit | 6,287.51 ms | 12.436 ms | 11.025 ms | +| xUnit | 6,454.07 ms | 19.839 ms | 17.586 ms | +| MSTest | 6,272.87 ms | 25.795 ms | 24.129 ms | @@ -474,11 +474,11 @@ Job=.NET 9.0 Runtime=.NET 9.0 ``` | Method | Mean | Error | StdDev | |---------- |-----------:|---------:|---------:| -| TUnit_AOT | 110.4 ms | 2.06 ms | 1.93 ms | -| TUnit | 930.8 ms | 18.31 ms | 19.59 ms | -| NUnit | 7,499.7 ms | 22.01 ms | 20.59 ms | -| xUnit | 7,557.0 ms | 17.44 ms | 16.31 ms | -| MSTest | 7,437.2 ms | 23.13 ms | 21.64 ms | +| TUnit_AOT | 109.0 ms | 0.49 ms | 0.41 ms | +| TUnit | 956.1 ms | 18.97 ms | 24.67 ms | +| NUnit | 7,509.9 ms | 10.56 ms | 9.36 ms | +| xUnit | 7,568.1 ms | 14.17 ms | 12.57 ms | +| MSTest | 7,468.6 ms | 24.46 ms | 22.88 ms | diff --git a/TUnit.Assertions.Tests/Bugs/Tests2117.cs b/TUnit.Assertions.Tests/Bugs/Tests2117.cs index c5a4ee77c1..347a1e9947 100644 --- a/TUnit.Assertions.Tests/Bugs/Tests2117.cs +++ b/TUnit.Assertions.Tests/Bugs/Tests2117.cs @@ -26,17 +26,21 @@ at Assert.That(a).IsEquivalentTo(b, collectionOrdering.Value) [Arguments(new[] { 1, 2, 3 }, new[] { 1, 2, 3, 4 }, null, """ Expected a to be equivalent to [1, 2, 3, 4] - - but it is [1, 2, 3] - + + but [3] did not match + Expected: 4 + Received: null + at Assert.That(a).IsEquivalentTo(b) """)] [Arguments(new[] { 1, 2, 3 }, new[] { 3, 2, 1 }, null, """ Expected a to be equivalent to [3, 2, 1] - - but it is [1, 2, 3] - + + but [0] did not match + Expected: 3 + Received: 1 + at Assert.That(a).IsEquivalentTo(b) """)] public async Task IsEquivalent_Fail(int[] a, int[] b, CollectionOrdering? collectionOrdering, string expectedError) @@ -69,9 +73,9 @@ at Assert.That(a).IsNotEquivalentTo(b, collectionOrdering.Value) [Arguments(new[] { 1, 2, 3 }, new[] { 1, 2, 3 }, null, """ Expected a to not be equivalent to [1, 2, 3] - - but the two Enumerables were equivalent - + + but it is + at Assert.That(a).IsNotEquivalentTo(b) """)] public async Task IsNotEquivalent_Fail(int[] a, int[] b, CollectionOrdering? collectionOrdering, string expectedError) diff --git a/TUnit.Assertions.UnitTests/EquivalentAssertionTests.cs b/TUnit.Assertions.UnitTests/EquivalentAssertionTests.cs index e04e822fbe..761eb7aedf 100644 --- a/TUnit.Assertions.UnitTests/EquivalentAssertionTests.cs +++ b/TUnit.Assertions.UnitTests/EquivalentAssertionTests.cs @@ -81,6 +81,24 @@ public async Task Different_Enumerables_Are_Equivalent2() await TUnitAssert.That(array).IsEquivalentTo(list); } + [Test] + public async Task Different_Dictionaries_Are_Equivalent_With_Different_Ordered_Keys() + { + var dict1 = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "A", "A" }, + { "B", "B" }, + }; + + var dict2 = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "B", "B" }, + { "A", "A" }, + }; + + await TUnitAssert.That(dict1).IsEquivalentTo(dict2); + } + [Test] public async Task Different_Enumerables_Are_Equivalent_Any_Order() { @@ -356,7 +374,7 @@ public void Objects_With_Nested_Enumerable_Mismatch_Are_Not_Equivalent() """ Expected object1 to be equivalent to object2 - but EnumerableItem MyClass.Inner.Inner.Collection.[3] did not match + but MyClass.Inner.Inner.Collection.[3] did not match Expected: "4" Received: null diff --git a/TUnit.Assertions/Assertions/Generics/Conditions/EquivalentToExpectedValueAssertCondition.cs b/TUnit.Assertions/Assertions/Generics/Conditions/EquivalentToExpectedValueAssertCondition.cs index da6fea5622..cb4100126f 100644 --- a/TUnit.Assertions/Assertions/Generics/Conditions/EquivalentToExpectedValueAssertCondition.cs +++ b/TUnit.Assertions/Assertions/Generics/Conditions/EquivalentToExpectedValueAssertCondition.cs @@ -43,22 +43,23 @@ protected override ValueTask GetResult(TActual? actualValue, TE "it is not null"); } - if (actualValue is IEnumerable actualEnumerable && ExpectedValue is IEnumerable expectedEnumerable) - { - var collectionEquivalentToEqualityComparer = new CollectionEquivalentToEqualityComparer( - new CompareOptions - { - MembersToIgnore = [.._ignoredMembers], - EquivalencyKind = EquivalencyKind, - }); - - var castedActual = actualEnumerable.Cast().ToArray(); - - return AssertionResult - .FailIf(!castedActual.SequenceEqual(expectedEnumerable.Cast(), - collectionEquivalentToEqualityComparer), - $"{GetFailureMessage(castedActual, collectionEquivalentToEqualityComparer)}"); - } + // if (actualValue is IEnumerable actualEnumerable && ExpectedValue is IEnumerable expectedEnumerable + // && actualValue is not IDictionary && ExpectedValue is not IDictionary) + // { + // var collectionEquivalentToEqualityComparer = new CollectionEquivalentToEqualityComparer( + // new CompareOptions + // { + // MembersToIgnore = [.._ignoredMembers], + // EquivalencyKind = EquivalencyKind, + // }); + // + // var castedActual = actualEnumerable.Cast().ToArray(); + // + // return AssertionResult + // .FailIf(!castedActual.SequenceEqual(expectedEnumerable.Cast(), + // collectionEquivalentToEqualityComparer), + // $"{GetFailureMessage(castedActual, collectionEquivalentToEqualityComparer)}"); + // } bool? isEqual = null; @@ -70,13 +71,6 @@ protected override ValueTask GetResult(TActual? actualValue, TE { isEqual = expectedBasicEqualityComparer.Equals(actualValue, ExpectedValue); } - else if (actualValue is IEnumerable enumerable && ExpectedValue is IEnumerable enumerable2) - { - IEnumerable castedEnumerable = [..enumerable]; - IEnumerable castedEnumerable2 = [..enumerable2]; - - isEqual = castedEnumerable.SequenceEqual(castedEnumerable2); - } if (isEqual != null) { return AssertionResult @@ -96,6 +90,15 @@ protected override ValueTask GetResult(TActual? actualValue, TE { return FailWithMessage(Formatter.Format(firstFailure.Actual)); } + + if (firstFailure.Type == MemberType.EnumerableItem) + { + return FailWithMessage($""" + {string.Join(".", firstFailure.NestedMemberNames)} did not match + Expected: {Formatter.Format(firstFailure.Expected)} + Received: {Formatter.Format(firstFailure.Actual)} + """); + } return FailWithMessage($""" {firstFailure.Type} {string.Join(".", firstFailure.NestedMemberNames)} did not match diff --git a/TUnit.Assertions/Assertions/Throws/ThrowsException.cs b/TUnit.Assertions/Assertions/Throws/ThrowsException.cs index f2f6b16b87..dc96749253 100644 --- a/TUnit.Assertions/Assertions/Throws/ThrowsException.cs +++ b/TUnit.Assertions/Assertions/Throws/ThrowsException.cs @@ -38,7 +38,7 @@ public ThrowsException WithMessage(string expected, [Caller public ThrowsException WithInnerException() { _source.AppendExpression($"{nameof(WithInnerException)}()"); - return new(_delegateAssertionBuilder, _source, e => _selector(e)?.InnerException); + return new ThrowsException(_delegateAssertionBuilder, _source, e => _selector(e)?.InnerException); } public TaskAwaiter GetAwaiter() @@ -57,18 +57,5 @@ public ThrowsException WithInnerException() public DelegateOr Or => _delegateAssertionBuilder.Or; - internal void RegisterAssertion(BaseAssertCondition condition, string?[] argumentExpressions, [CallerMemberName] string? caller = null) => _source.RegisterAssertion(condition, argumentExpressions, caller); - internal void RegisterAssertion(Func, BaseAssertCondition> conditionFunc, string?[] argumentExpressions, [CallerMemberName] string? caller = null) => _source.RegisterAssertion(conditionFunc(_selector), argumentExpressions, caller); - - private async ValueTask AssertionDataTask() - { - var assertionData = await _delegateAssertionBuilder.ProcessAssertionsAsync(); - - return assertionData with - { - Result = assertionData.Exception as TException, - Exception = null - }; - } } \ No newline at end of file diff --git a/TUnit.Assertions/Compare.cs b/TUnit.Assertions/Compare.cs index 278cfab54b..4396b0fe43 100644 --- a/TUnit.Assertions/Compare.cs +++ b/TUnit.Assertions/Compare.cs @@ -26,7 +26,11 @@ public static IEnumerable CheckEquivalent< TExpected expected, CompareOptions options, int? index) { - return CheckEquivalent(actual, expected, options, [InitialMemberName(actual, index)], MemberType.Value, []); + var initialMemberName = InitialMemberName(actual, index); + + string[] memberNames = string.IsNullOrEmpty(initialMemberName) ? [] : [initialMemberName]; + + return CheckEquivalent(actual, expected, options, memberNames, MemberType.Value, []); } private static IEnumerable CheckEquivalent< @@ -77,6 +81,27 @@ private static IEnumerable CheckEquivalent< { yield break; } + + if(actual is IDictionary actualDictionary && expected is IDictionary expectedDictionary) + { + var keys = actualDictionary.Keys.Cast() + .Concat(expectedDictionary.Keys.Cast()) + .Distinct() + .ToArray(); + + foreach (var key in keys) + { + var actualObject = actualDictionary[key]; + var expectedObject = expectedDictionary[key]; + + foreach (var comparisonFailure in CheckEquivalent(actualObject, expectedObject, options, + [..memberNames, $"[{key}]"], MemberType.EnumerableItem, visited)) + { + yield return comparisonFailure; + } + } + yield break; + } if (actual is IEnumerable actualEnumerable && expected is IEnumerable expectedEnumerable) { @@ -206,6 +231,11 @@ private static IEnumerable CheckEquivalent< private static string InitialMemberName(object? actual, int? index) { + if (actual is IEnumerable) + { + return string.Empty; + } + var type = actual?.GetType().Name ?? typeof(TActual).Name; if (index is null) diff --git a/TUnit.Assertions/MemberType.cs b/TUnit.Assertions/MemberType.cs index 845b5f9ab7..5148336059 100644 --- a/TUnit.Assertions/MemberType.cs +++ b/TUnit.Assertions/MemberType.cs @@ -5,5 +5,6 @@ public enum MemberType Property, Field, Value, - EnumerableItem + EnumerableItem, + DictionaryItem } \ No newline at end of file diff --git a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet2_0.verified.txt b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet2_0.verified.txt index 57e3cdf3fa..11aab3b441 100644 --- a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet2_0.verified.txt +++ b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet2_0.verified.txt @@ -98,6 +98,7 @@ namespace TUnit.Assertions Field = 1, Value = 2, EnumerableItem = 3, + DictionaryItem = 4, } } namespace TUnit.Assertions.AssertConditions 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 7754daa620..f12e66fb7e 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 @@ -106,6 +106,7 @@ namespace TUnit.Assertions Field = 1, Value = 2, EnumerableItem = 3, + DictionaryItem = 4, } } namespace TUnit.Assertions.AssertConditions 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 fb242b5c03..89c6f0ed9e 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 @@ -106,6 +106,7 @@ namespace TUnit.Assertions Field = 1, Value = 2, EnumerableItem = 3, + DictionaryItem = 4, } } namespace TUnit.Assertions.AssertConditions diff --git a/TUnit.Templates/content/TUnit.AspNet/TestProject/TestProject.csproj b/TUnit.Templates/content/TUnit.AspNet/TestProject/TestProject.csproj index ac67112245..d6fcd0bbea 100644 --- a/TUnit.Templates/content/TUnit.AspNet/TestProject/TestProject.csproj +++ b/TUnit.Templates/content/TUnit.AspNet/TestProject/TestProject.csproj @@ -9,7 +9,7 @@ - + diff --git a/TUnit.Templates/content/TUnit.Aspire.Starter/ExampleNamespace.TestProject/ExampleNamespace.TestProject.csproj b/TUnit.Templates/content/TUnit.Aspire.Starter/ExampleNamespace.TestProject/ExampleNamespace.TestProject.csproj index b9f1d8b7ae..191ffceef2 100644 --- a/TUnit.Templates/content/TUnit.Aspire.Starter/ExampleNamespace.TestProject/ExampleNamespace.TestProject.csproj +++ b/TUnit.Templates/content/TUnit.Aspire.Starter/ExampleNamespace.TestProject/ExampleNamespace.TestProject.csproj @@ -11,7 +11,7 @@ - + diff --git a/TUnit.Templates/content/TUnit.Aspire.Test/ExampleNamespace.csproj b/TUnit.Templates/content/TUnit.Aspire.Test/ExampleNamespace.csproj index f533c0bb59..0988b7ea8f 100644 --- a/TUnit.Templates/content/TUnit.Aspire.Test/ExampleNamespace.csproj +++ b/TUnit.Templates/content/TUnit.Aspire.Test/ExampleNamespace.csproj @@ -10,7 +10,7 @@ - + diff --git a/TUnit.Templates/content/TUnit.Playwright/TestProject.csproj b/TUnit.Templates/content/TUnit.Playwright/TestProject.csproj index e6476bdc24..0582d5e043 100644 --- a/TUnit.Templates/content/TUnit.Playwright/TestProject.csproj +++ b/TUnit.Templates/content/TUnit.Playwright/TestProject.csproj @@ -8,7 +8,7 @@ - + diff --git a/TUnit.Templates/content/TUnit/TestProject.csproj b/TUnit.Templates/content/TUnit/TestProject.csproj index 16a4dd99ec..8062657d08 100644 --- a/TUnit.Templates/content/TUnit/TestProject.csproj +++ b/TUnit.Templates/content/TUnit/TestProject.csproj @@ -8,7 +8,7 @@ - + \ No newline at end of file