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
+}