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..9a76691286 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,9 +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)), + ..properties, + ..categories, ..testDetails.Categories.Select(x => new KeyValuePairStringProperty("Category", x)) ] ); @@ -86,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 +} 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; } 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..6da44dbb33 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; @@ -14,25 +12,25 @@ 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() + 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"); public class MethodPropertyAttribute() : PropertyAttribute("MethodProperty2", "MethodPropertyValue2"); -} \ No newline at end of file +}