From 53cf794f8a08f0047f3d814424bf6db2ff9d27ed Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Sat, 31 May 2025 01:04:59 +0100 Subject: [PATCH 1/4] Allow multiple property values with the same key --- TUnit.Core/DiscoveredTestContext.cs | 4 +- TUnit.Core/TestDetails.cs | 10 +++-- TUnit.Engine.Tests/Bugs/Bug2481.cs | 49 ++++++++++++++++++++++ TUnit.Engine/Extensions/TestExtensions.cs | 15 ++++++- TUnit.Engine/Json/TestJson.cs | 2 +- TUnit.Engine/Services/TestFilterService.cs | 9 +++- TUnit.TestProject/Bugs/2481/Tests.cs | 22 ++++++++++ TUnit.TestProject/CustomPropertyTests.cs | 6 +-- 8 files changed, 104 insertions(+), 13 deletions(-) create mode 100644 TUnit.Engine.Tests/Bugs/Bug2481.cs create mode 100644 TUnit.TestProject/Bugs/2481/Tests.cs diff --git a/TUnit.Core/DiscoveredTestContext.cs b/TUnit.Core/DiscoveredTestContext.cs index 43a1b4cc93..6f5bec62ac 100644 --- a/TUnit.Core/DiscoveredTestContext.cs +++ b/TUnit.Core/DiscoveredTestContext.cs @@ -19,7 +19,9 @@ internal DiscoveredTestContext(TestContext testContext) public void AddProperty(string key, string value) { - TestContext.TestDetails.InternalCustomProperties.Add(key, value); + TestContext.TestDetails.InternalCustomProperties + .GetOrAdd(key, []) + .Add(value); } public void AddCategory(string category) diff --git a/TUnit.Core/TestDetails.cs b/TUnit.Core/TestDetails.cs index 24440b0f34..5cf71f01eb 100644 --- a/TUnit.Core/TestDetails.cs +++ b/TUnit.Core/TestDetails.cs @@ -1,5 +1,6 @@ -using System.ComponentModel; +using System.Collections.Concurrent; using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using System.Text.Json.Serialization; using TUnit.Core.Interfaces; @@ -114,9 +115,12 @@ public abstract record TestDetails /// /// Gets the custom properties for the test. /// - public IReadOnlyDictionary CustomProperties => InternalCustomProperties; + [field: AllowNull, MaybeNull] + public IReadOnlyDictionary> CustomProperties => field ??= InternalCustomProperties.ToDictionary( + kvp => kvp.Key, IReadOnlyList (kvp) => kvp.Value.AsReadOnly() + ); - internal Dictionary InternalCustomProperties { get; } = []; + internal ConcurrentDictionary> InternalCustomProperties { get; } = []; /// /// Gets the attributes for the test assembly. diff --git a/TUnit.Engine.Tests/Bugs/Bug2481.cs b/TUnit.Engine.Tests/Bugs/Bug2481.cs new file mode 100644 index 0000000000..72eecea3ef --- /dev/null +++ b/TUnit.Engine.Tests/Bugs/Bug2481.cs @@ -0,0 +1,49 @@ +using Shouldly; +using TUnit.Engine.Tests.Enums; + +namespace TUnit.Engine.Tests.Bugs; + +public class Bug2481(TestMode testMode) : InvokableTestBase(testMode) +{ + [Test] + public async Task Test() + { + await RunTestsWithFilter( + "/*/TUnit.TestProject.Bugs._2481/*/*[Group=Bugs]", + [ + result => result.ResultSummary.Outcome.ShouldBe("Completed"), + result => result.ResultSummary.Counters.Total.ShouldBe(1), + result => result.ResultSummary.Counters.Passed.ShouldBe(1), + result => result.ResultSummary.Counters.Failed.ShouldBe(0), + result => result.ResultSummary.Counters.NotExecuted.ShouldBe(0) + ]); + } + + [Test] + public async Task Test2() + { + await RunTestsWithFilter( + "/*/TUnit.TestProject.Bugs._2481/*/*[Group=2481]", + [ + result => result.ResultSummary.Outcome.ShouldBe("Completed"), + result => result.ResultSummary.Counters.Total.ShouldBe(1), + result => result.ResultSummary.Counters.Passed.ShouldBe(1), + result => result.ResultSummary.Counters.Failed.ShouldBe(0), + result => result.ResultSummary.Counters.NotExecuted.ShouldBe(0) + ]); + } + + [Test] + public async Task Test3() + { + await RunTestsWithFilter( + "/*/TUnit.TestProject.Bugs._2481/*/*[Group=TUnit]", + [ + result => result.ResultSummary.Outcome.ShouldBe("Completed"), + result => result.ResultSummary.Counters.Total.ShouldBe(1), + result => result.ResultSummary.Counters.Passed.ShouldBe(1), + result => result.ResultSummary.Counters.Failed.ShouldBe(0), + result => result.ResultSummary.Counters.NotExecuted.ShouldBe(0) + ]); + } +} \ No newline at end of file diff --git a/TUnit.Engine/Extensions/TestExtensions.cs b/TUnit.Engine/Extensions/TestExtensions.cs index 0cbed41477..0acace0263 100644 --- a/TUnit.Engine/Extensions/TestExtensions.cs +++ b/TUnit.Engine/Extensions/TestExtensions.cs @@ -33,8 +33,8 @@ internal static TestNode ToTestNode(this TestContext testContext) ), // Custom TUnit Properties - ..testDetails.Categories.Select(category => new TestMetadataProperty(category, string.Empty)), - ..testDetails.CustomProperties.Select(x => new TestMetadataProperty(x.Key, x.Value)), + ..testDetails.Categories.Select(category => new TestMetadataProperty(category)), + ..ExtractProperties(testDetails), // Artifacts ..testContext.Artifacts.Select(x => new FileArtifactProperty(x.File, x.DisplayName, x.Description)), @@ -48,6 +48,17 @@ internal static TestNode ToTestNode(this TestContext testContext) return testNode; } + public static IEnumerable ExtractProperties(this TestDetails testDetails) + { + foreach (var propertyGroup in testDetails.CustomProperties) + { + foreach (var propertyValue in propertyGroup.Value) + { + yield return new KeyValuePairStringProperty(propertyGroup.Key, propertyValue); + } + } + } + internal static TestNode WithProperty(this TestNode testNode, IProperty property) { testNode.Properties.Add(property); diff --git a/TUnit.Engine/Json/TestJson.cs b/TUnit.Engine/Json/TestJson.cs index 97a5d660c3..a5719be619 100644 --- a/TUnit.Engine/Json/TestJson.cs +++ b/TUnit.Engine/Json/TestJson.cs @@ -21,7 +21,7 @@ public record TestJson public required TimeSpan? Timeout { get; init; } - public required IReadOnlyDictionary CustomProperties { get; init; } + public required IReadOnlyDictionary> CustomProperties { get; init; } public required string? ReturnType { get; init; } diff --git a/TUnit.Engine/Services/TestFilterService.cs b/TUnit.Engine/Services/TestFilterService.cs index 04e6234152..0d9ea5a4a6 100644 --- a/TUnit.Engine/Services/TestFilterService.cs +++ b/TUnit.Engine/Services/TestFilterService.cs @@ -4,6 +4,7 @@ using Microsoft.Testing.Platform.Logging; using Microsoft.Testing.Platform.Requests; using TUnit.Core; +using TUnit.Engine.Extensions; namespace TUnit.Engine.Services; @@ -73,10 +74,14 @@ private string BuildPath(TestDetails testDetails) private PropertyBag BuildPropertyBag(TestDetails testDetails) { + var properties = testDetails.ExtractProperties(); + + var categories = testDetails.Categories.Select(x => new TestMetadataProperty(x)); + return new PropertyBag( [ - ..testDetails.CustomProperties.Select(x => new KeyValuePairStringProperty(x.Key, x.Value)), - ..testDetails.Categories.Select(x => new KeyValuePairStringProperty("Category", x)) + ..properties, + ..categories ] ); } diff --git a/TUnit.TestProject/Bugs/2481/Tests.cs b/TUnit.TestProject/Bugs/2481/Tests.cs new file mode 100644 index 0000000000..025cb12a15 --- /dev/null +++ b/TUnit.TestProject/Bugs/2481/Tests.cs @@ -0,0 +1,22 @@ +namespace TUnit.TestProject.Bugs._2481; + +public class Tests +{ + [Test] + [Property("Group", "Bugs")] + [Property("Group", "2481")] + [Property("Group", "TUnit")] + public async Task Test() + { + var properties = TestContext.Current!.TestDetails.CustomProperties; + + await Assert.That(properties).HasCount().EqualTo(1); + + var array = properties["Group"].ToArray(); + + await Assert.That(array).HasCount().EqualTo(3) + .And.Contains(x => x is "Bugs") + .And.Contains(x => x is "2481") + .And.Contains(x => x is "TUnit"); + } +} \ No newline at end of file diff --git a/TUnit.TestProject/CustomPropertyTests.cs b/TUnit.TestProject/CustomPropertyTests.cs index ddee79ca92..817e629d85 100644 --- a/TUnit.TestProject/CustomPropertyTests.cs +++ b/TUnit.TestProject/CustomPropertyTests.cs @@ -1,6 +1,4 @@ using System.Collections.Immutable; -using TUnit.Assertions; -using TUnit.Assertions.Extensions; namespace TUnit.TestProject; @@ -26,10 +24,10 @@ public async Task Test() await Assert.That(GetDictionary()).ContainsValue("MethodPropertyValue2"); } - private static ImmutableDictionary GetDictionary() + private static ImmutableDictionary> GetDictionary() { return TestContext.Current?.TestDetails.CustomProperties.ToImmutableDictionary(x => x.Key, x => x.Value) - ?? ImmutableDictionary.Empty; + ?? ImmutableDictionary>.Empty; } public class ClassPropertyAttribute() : PropertyAttribute("ClassProperty2", "ClassPropertyValue2"); From 4a4e65c84e04709e06434416653ee7aa7817b5fe Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Sat, 31 May 2025 01:06:25 +0100 Subject: [PATCH 2/4] Update Public API --- ...Tests.Core_Library_Has_No_API_Changes.DotNet2_0.verified.txt | 2 +- ...Tests.Core_Library_Has_No_API_Changes.DotNet8_0.verified.txt | 2 +- ...Tests.Core_Library_Has_No_API_Changes.DotNet9_0.verified.txt | 2 +- ...sts.Engine_Library_Has_No_API_Changes.DotNet2_0.verified.txt | 2 +- ...sts.Engine_Library_Has_No_API_Changes.DotNet8_0.verified.txt | 2 +- ...sts.Engine_Library_Has_No_API_Changes.DotNet9_0.verified.txt | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet2_0.verified.txt b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet2_0.verified.txt index a54a2aee34..f8bf5daeef 100644 --- a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet2_0.verified.txt +++ b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet2_0.verified.txt @@ -843,7 +843,7 @@ namespace TUnit.Core public System.Attribute[] ClassAttributes { get; } public abstract object ClassInstance { get; } public required int CurrentRepeatAttempt { get; init; } - public System.Collections.Generic.IReadOnlyDictionary CustomProperties { get; } + public System.Collections.Generic.IReadOnlyDictionary> CustomProperties { get; } [System.Text.Json.Serialization.JsonIgnore] public required System.Attribute[] DataAttributes { get; init; } [System.Text.Json.Serialization.JsonIgnore] diff --git a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet8_0.verified.txt b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet8_0.verified.txt index 7cb1611c55..17812e9c46 100644 --- a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet8_0.verified.txt +++ b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet8_0.verified.txt @@ -890,7 +890,7 @@ namespace TUnit.Core public System.Attribute[] ClassAttributes { get; } public abstract object ClassInstance { get; } public required int CurrentRepeatAttempt { get; init; } - public System.Collections.Generic.IReadOnlyDictionary CustomProperties { get; } + public System.Collections.Generic.IReadOnlyDictionary> CustomProperties { get; } [System.Text.Json.Serialization.JsonIgnore] public required System.Attribute[] DataAttributes { get; init; } [System.Text.Json.Serialization.JsonIgnore] diff --git a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet9_0.verified.txt b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet9_0.verified.txt index 0333595dc7..4c3f01d603 100644 --- a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet9_0.verified.txt +++ b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet9_0.verified.txt @@ -890,7 +890,7 @@ namespace TUnit.Core public System.Attribute[] ClassAttributes { get; } public abstract object ClassInstance { get; } public required int CurrentRepeatAttempt { get; init; } - public System.Collections.Generic.IReadOnlyDictionary CustomProperties { get; } + public System.Collections.Generic.IReadOnlyDictionary> CustomProperties { get; } [System.Text.Json.Serialization.JsonIgnore] public required System.Attribute[] DataAttributes { get; init; } [System.Text.Json.Serialization.JsonIgnore] diff --git a/TUnit.PublicAPI/Tests.Engine_Library_Has_No_API_Changes.DotNet2_0.verified.txt b/TUnit.PublicAPI/Tests.Engine_Library_Has_No_API_Changes.DotNet2_0.verified.txt index afd2ae2457..dce685fd88 100644 --- a/TUnit.PublicAPI/Tests.Engine_Library_Has_No_API_Changes.DotNet2_0.verified.txt +++ b/TUnit.PublicAPI/Tests.Engine_Library_Has_No_API_Changes.DotNet2_0.verified.txt @@ -96,7 +96,7 @@ namespace TUnit.Engine.Json public TestJson() { } public required System.Collections.Generic.IReadOnlyList Categories { get; init; } public required string? ClassType { get; init; } - public required System.Collections.Generic.IReadOnlyDictionary CustomProperties { get; init; } + public required System.Collections.Generic.IReadOnlyDictionary> CustomProperties { get; init; } public required string DisplayName { get; set; } public required System.Collections.Generic.Dictionary ObjectBag { get; init; } public required TUnit.Engine.Json.TestResultJson? Result { get; set; } diff --git a/TUnit.PublicAPI/Tests.Engine_Library_Has_No_API_Changes.DotNet8_0.verified.txt b/TUnit.PublicAPI/Tests.Engine_Library_Has_No_API_Changes.DotNet8_0.verified.txt index b2fc1880ea..a40b1bb1a0 100644 --- a/TUnit.PublicAPI/Tests.Engine_Library_Has_No_API_Changes.DotNet8_0.verified.txt +++ b/TUnit.PublicAPI/Tests.Engine_Library_Has_No_API_Changes.DotNet8_0.verified.txt @@ -99,7 +99,7 @@ namespace TUnit.Engine.Json public TestJson() { } public required System.Collections.Generic.IReadOnlyList Categories { get; init; } public required string? ClassType { get; init; } - public required System.Collections.Generic.IReadOnlyDictionary CustomProperties { get; init; } + public required System.Collections.Generic.IReadOnlyDictionary> CustomProperties { get; init; } public required string DisplayName { get; set; } public required System.Collections.Generic.Dictionary ObjectBag { get; init; } public required TUnit.Engine.Json.TestResultJson? Result { get; set; } diff --git a/TUnit.PublicAPI/Tests.Engine_Library_Has_No_API_Changes.DotNet9_0.verified.txt b/TUnit.PublicAPI/Tests.Engine_Library_Has_No_API_Changes.DotNet9_0.verified.txt index 89cd0bcbd0..4b0c236bf1 100644 --- a/TUnit.PublicAPI/Tests.Engine_Library_Has_No_API_Changes.DotNet9_0.verified.txt +++ b/TUnit.PublicAPI/Tests.Engine_Library_Has_No_API_Changes.DotNet9_0.verified.txt @@ -99,7 +99,7 @@ namespace TUnit.Engine.Json public TestJson() { } public required System.Collections.Generic.IReadOnlyList Categories { get; init; } public required string? ClassType { get; init; } - public required System.Collections.Generic.IReadOnlyDictionary CustomProperties { get; init; } + public required System.Collections.Generic.IReadOnlyDictionary> CustomProperties { get; init; } public required string DisplayName { get; set; } public required System.Collections.Generic.Dictionary ObjectBag { get; init; } public required TUnit.Engine.Json.TestResultJson? Result { get; set; } From 4e5eb89d667d5fbaba8241561a349d76c8c8174f Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Sat, 31 May 2025 01:27:24 +0100 Subject: [PATCH 3/4] Update TestFilterService.cs --- TUnit.Engine/Services/TestFilterService.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/TUnit.Engine/Services/TestFilterService.cs b/TUnit.Engine/Services/TestFilterService.cs index 0d9ea5a4a6..9a76691286 100644 --- a/TUnit.Engine/Services/TestFilterService.cs +++ b/TUnit.Engine/Services/TestFilterService.cs @@ -81,7 +81,8 @@ private PropertyBag BuildPropertyBag(TestDetails testDetails) return new PropertyBag( [ ..properties, - ..categories + ..categories, + ..testDetails.Categories.Select(x => new KeyValuePairStringProperty("Category", x)) ] ); } @@ -91,4 +92,4 @@ private bool UnhandledFilter(ITestExecutionFilter testExecutionFilter) _logger.LogWarning($"Filter is Unhandled Type: {testExecutionFilter.GetType().FullName}"); return true; } -} \ No newline at end of file +} From 93ce3dde46d5ab485875f92c61b9b1eb7bd54c0f Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Sat, 31 May 2025 07:41:00 +0100 Subject: [PATCH 4/4] Update CustomPropertyTests.cs --- TUnit.TestProject/CustomPropertyTests.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/TUnit.TestProject/CustomPropertyTests.cs b/TUnit.TestProject/CustomPropertyTests.cs index 817e629d85..6da44dbb33 100644 --- a/TUnit.TestProject/CustomPropertyTests.cs +++ b/TUnit.TestProject/CustomPropertyTests.cs @@ -12,16 +12,16 @@ public class CustomPropertyTests public async Task Test() { await Assert.That(GetDictionary()).ContainsKey("ClassProperty"); - await Assert.That(GetDictionary()).ContainsValue("ClassPropertyValue"); + await Assert.That(GetDictionary()["ClassProperty"]).Contains("ClassPropertyValue"); await Assert.That(GetDictionary()).ContainsKey("ClassProperty2"); - await Assert.That(GetDictionary()).ContainsValue("ClassPropertyValue2"); + await Assert.That(GetDictionary()["ClassProperty2"]).Contains("ClassPropertyValue2"); await Assert.That(GetDictionary()).ContainsKey("MethodProperty"); - await Assert.That(GetDictionary()).ContainsValue("MethodPropertyValue"); + await Assert.That(GetDictionary()["MethodProperty"]).Contains("MethodPropertyValue"); await Assert.That(GetDictionary()).ContainsKey("MethodProperty2"); - await Assert.That(GetDictionary()).ContainsValue("MethodPropertyValue2"); + await Assert.That(GetDictionary()["MethodProperty2"]).Contains("MethodPropertyValue2"); } private static ImmutableDictionary> GetDictionary() @@ -33,4 +33,4 @@ private static ImmutableDictionary> GetDictionary( public class ClassPropertyAttribute() : PropertyAttribute("ClassProperty2", "ClassPropertyValue2"); public class MethodPropertyAttribute() : PropertyAttribute("MethodProperty2", "MethodPropertyValue2"); -} \ No newline at end of file +}