diff --git a/TUnit.Core/AbstractDynamicTest.cs b/TUnit.Core/AbstractDynamicTest.cs index e086176bc6..e0d3464b6c 100644 --- a/TUnit.Core/AbstractDynamicTest.cs +++ b/TUnit.Core/AbstractDynamicTest.cs @@ -47,11 +47,23 @@ public class DynamicDiscoveryResult : DiscoveryResult public Dictionary? Properties { get; set; } public string? DisplayName { get; set; } + + /// + /// Unique index for this dynamic test within its builder context. + /// Used to generate unique test IDs when multiple dynamic tests target the same method. + /// + public int DynamicTestIndex { get; set; } } public abstract class AbstractDynamicTest { public abstract IEnumerable GetTests(); + + /// + /// Unique index for this dynamic test within its builder context. + /// Used to generate unique test IDs when multiple dynamic tests target the same method. + /// + public int DynamicTestIndex { get; set; } } public abstract class AbstractDynamicTest<[DynamicallyAccessedMembers( @@ -99,7 +111,8 @@ public override IEnumerable GetTests() Attributes = Attributes, TestClassType = typeof(T), CreatorFilePath = CreatorFilePath, - CreatorLineNumber = CreatorLineNumber + CreatorLineNumber = CreatorLineNumber, + DynamicTestIndex = DynamicTestIndex }; yield return result; diff --git a/TUnit.Core/DynamicTestBuilderContext.cs b/TUnit.Core/DynamicTestBuilderContext.cs index f73e84f4c9..90867d914c 100644 --- a/TUnit.Core/DynamicTestBuilderContext.cs +++ b/TUnit.Core/DynamicTestBuilderContext.cs @@ -11,6 +11,8 @@ public class DynamicTestBuilderContext [ ]; + private int _nextIndex = 1; // Start at 1 to match loop index convention used by regular data sources + public DynamicTestBuilderContext(string filePath, int lineNumber) { FilePath = filePath; @@ -34,6 +36,9 @@ public void AddTest(AbstractDynamicTest test) testWithLocation.CreatorLineNumber = LineNumber; } + // Assign unique index for test ID generation + test.DynamicTestIndex = _nextIndex++; + _tests.Add(test); } } diff --git a/TUnit.Core/IDynamicTestMetadata.cs b/TUnit.Core/IDynamicTestMetadata.cs index e28c56dc8d..23b71dc093 100644 --- a/TUnit.Core/IDynamicTestMetadata.cs +++ b/TUnit.Core/IDynamicTestMetadata.cs @@ -1,8 +1,13 @@ namespace TUnit.Core; /// -/// Marker interface for dynamic test metadata that should bypass normal data source processing +/// Interface for dynamic test metadata that should bypass normal data source processing /// public interface IDynamicTestMetadata { + /// + /// Unique index for this dynamic test within its builder context. + /// Used to generate unique test IDs when multiple dynamic tests target the same method. + /// + int DynamicTestIndex { get; } } \ No newline at end of file diff --git a/TUnit.Engine.Tests/DynamicTestIndexTests.cs b/TUnit.Engine.Tests/DynamicTestIndexTests.cs new file mode 100644 index 0000000000..d0513ff80f --- /dev/null +++ b/TUnit.Engine.Tests/DynamicTestIndexTests.cs @@ -0,0 +1,24 @@ +using Shouldly; +using TUnit.Engine.Tests.Enums; + +namespace TUnit.Engine.Tests; + +/// +/// Tests that validate DynamicTestIndex generates unique test IDs +/// when multiple dynamic tests target the same method. +/// +public class DynamicTestIndexTests(TestMode testMode) : InvokableTestBase(testMode) +{ + [Test] + public async Task Test() + { + await RunTestsWithFilter( + "/*/*DynamicTests/DynamicTestIndexTests/*", + [ + result => result.ResultSummary.Outcome.ShouldBe("Completed"), + result => result.ResultSummary.Counters.Total.ShouldBe(5), + result => result.ResultSummary.Counters.Passed.ShouldBe(5), + result => result.ResultSummary.Counters.Failed.ShouldBe(0) + ]); + } +} diff --git a/TUnit.Engine/Building/Collectors/AotTestDataCollector.cs b/TUnit.Engine/Building/Collectors/AotTestDataCollector.cs index a538088821..18a314bd48 100644 --- a/TUnit.Engine/Building/Collectors/AotTestDataCollector.cs +++ b/TUnit.Engine/Building/Collectors/AotTestDataCollector.cs @@ -298,6 +298,8 @@ private static MethodMetadata CreateDummyMethodMetadata(Type type, string method private sealed class AotDynamicTestMetadata(DynamicDiscoveryResult dynamicResult) : TestMetadata, IDynamicTestMetadata { + public int DynamicTestIndex => dynamicResult.DynamicTestIndex; + public override Func CreateExecutableTestFactory { get => (context, metadata) => diff --git a/TUnit.Engine/Building/TestBuilderPipeline.cs b/TUnit.Engine/Building/TestBuilderPipeline.cs index cc3fc7c815..2e33d9baab 100644 --- a/TUnit.Engine/Building/TestBuilderPipeline.cs +++ b/TUnit.Engine/Building/TestBuilderPipeline.cs @@ -152,6 +152,8 @@ private async Task GenerateDynamicTests(TestMetadata m .SelectAsync(async repeatIndex => { // Create a simple TestData for ID generation + // Use DynamicTestIndex from the metadata to ensure unique test IDs for multiple dynamic tests + var dynamicTestIndex = metadata is IDynamicTestMetadata dynMeta ? dynMeta.DynamicTestIndex : 0; var testData = new TestBuilder.TestData { TestClassInstanceFactory = () => Task.FromResult(metadata.InstanceFactory(Type.EmptyTypes, [])), @@ -159,7 +161,7 @@ private async Task GenerateDynamicTests(TestMetadata m ClassDataLoopIndex = 0, ClassData = [], MethodDataSourceAttributeIndex = 0, - MethodDataLoopIndex = 0, + MethodDataLoopIndex = dynamicTestIndex, MethodData = [], RepeatIndex = repeatIndex, InheritanceDepth = metadata.InheritanceDepth, @@ -271,6 +273,8 @@ private async IAsyncEnumerable BuildTestsFromSingleMetad // Dynamic tests need to honor attributes like RepeatCount, RetryCount, etc. // We'll create multiple test instances based on RepeatCount + // Use DynamicTestIndex from the metadata to ensure unique test IDs for multiple dynamic tests + var dynamicTestIndex = ((IDynamicTestMetadata)resolvedMetadata).DynamicTestIndex; for (var repeatIndex = 0; repeatIndex < repeatCount + 1; repeatIndex++) { // Create a simple TestData for ID generation @@ -281,7 +285,7 @@ private async IAsyncEnumerable BuildTestsFromSingleMetad ClassDataLoopIndex = 0, ClassData = [], MethodDataSourceAttributeIndex = 0, - MethodDataLoopIndex = 0, + MethodDataLoopIndex = dynamicTestIndex, MethodData = [], RepeatIndex = repeatIndex, InheritanceDepth = resolvedMetadata.InheritanceDepth, diff --git a/TUnit.Engine/Discovery/ReflectionTestDataCollector.cs b/TUnit.Engine/Discovery/ReflectionTestDataCollector.cs index 86b942be33..e087d0229e 100644 --- a/TUnit.Engine/Discovery/ReflectionTestDataCollector.cs +++ b/TUnit.Engine/Discovery/ReflectionTestDataCollector.cs @@ -2108,6 +2108,8 @@ public DynamicReflectionTestMetadata( _dynamicResult = dynamicResult; } + public int DynamicTestIndex => _dynamicResult.DynamicTestIndex; + public override Func CreateExecutableTestFactory { get => (context, metadata) => diff --git a/TUnit.Engine/Services/TestRegistry.cs b/TUnit.Engine/Services/TestRegistry.cs index a1538e5e98..3b4013f0fa 100644 --- a/TUnit.Engine/Services/TestRegistry.cs +++ b/TUnit.Engine/Services/TestRegistry.cs @@ -353,6 +353,8 @@ public RuntimeDynamicTestMetadata(Type testClass, MethodInfo testMethod, Dynamic _dynamicResult = dynamicResult; } + public int DynamicTestIndex => _dynamicResult.DynamicTestIndex; + public override Func CreateExecutableTestFactory { get => (context, metadata) => diff --git a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet10_0.verified.txt b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet10_0.verified.txt index a01bfb6b84..afda7f52fe 100644 --- a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet10_0.verified.txt +++ b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet10_0.verified.txt @@ -6,6 +6,7 @@ namespace public abstract class AbstractDynamicTest { protected AbstractDynamicTest() { } + public int DynamicTestIndex { get; set; } public abstract .<.DiscoveryResult> GetTests(); } public abstract class AbstractDynamicTest<[.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..NonPublicConstructors | ..PublicMethods | ..NonPublicMethods | ..PublicFields | ..NonPublicFields | ..PublicProperties)] T> : .AbstractDynamicTest @@ -617,6 +618,7 @@ namespace public string? CreatorFilePath { get; set; } public int? CreatorLineNumber { get; set; } public string? DisplayName { get; set; } + public int DynamicTestIndex { get; set; } public string? ParentTestId { get; set; } public .? Properties { get; set; } public .? Relationship { get; set; } @@ -853,7 +855,10 @@ namespace string? CreatorFilePath { get; set; } int? CreatorLineNumber { get; set; } } - public interface IDynamicTestMetadata { } + public interface IDynamicTestMetadata + { + int DynamicTestIndex { get; } + } public interface IDynamicTestSource { .<.AbstractDynamicTest> CollectDynamicTests(string sessionId); 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 6473654dd9..5c77ae9d16 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 @@ -6,6 +6,7 @@ namespace public abstract class AbstractDynamicTest { protected AbstractDynamicTest() { } + public int DynamicTestIndex { get; set; } public abstract .<.DiscoveryResult> GetTests(); } public abstract class AbstractDynamicTest<[.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..NonPublicConstructors | ..PublicMethods | ..NonPublicMethods | ..PublicFields | ..NonPublicFields | ..PublicProperties)] T> : .AbstractDynamicTest @@ -617,6 +618,7 @@ namespace public string? CreatorFilePath { get; set; } public int? CreatorLineNumber { get; set; } public string? DisplayName { get; set; } + public int DynamicTestIndex { get; set; } public string? ParentTestId { get; set; } public .? Properties { get; set; } public .? Relationship { get; set; } @@ -853,7 +855,10 @@ namespace string? CreatorFilePath { get; set; } int? CreatorLineNumber { get; set; } } - public interface IDynamicTestMetadata { } + public interface IDynamicTestMetadata + { + int DynamicTestIndex { get; } + } public interface IDynamicTestSource { .<.AbstractDynamicTest> CollectDynamicTests(string sessionId); 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 f5e15d8132..7d0b7502f7 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 @@ -6,6 +6,7 @@ namespace public abstract class AbstractDynamicTest { protected AbstractDynamicTest() { } + public int DynamicTestIndex { get; set; } public abstract .<.DiscoveryResult> GetTests(); } public abstract class AbstractDynamicTest<[.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..NonPublicConstructors | ..PublicMethods | ..NonPublicMethods | ..PublicFields | ..NonPublicFields | ..PublicProperties)] T> : .AbstractDynamicTest @@ -617,6 +618,7 @@ namespace public string? CreatorFilePath { get; set; } public int? CreatorLineNumber { get; set; } public string? DisplayName { get; set; } + public int DynamicTestIndex { get; set; } public string? ParentTestId { get; set; } public .? Properties { get; set; } public .? Relationship { get; set; } @@ -853,7 +855,10 @@ namespace string? CreatorFilePath { get; set; } int? CreatorLineNumber { get; set; } } - public interface IDynamicTestMetadata { } + public interface IDynamicTestMetadata + { + int DynamicTestIndex { get; } + } public interface IDynamicTestSource { .<.AbstractDynamicTest> CollectDynamicTests(string sessionId); diff --git a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.Net4_7.verified.txt b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.Net4_7.verified.txt index 143ce52b60..232141cff3 100644 --- a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.Net4_7.verified.txt +++ b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.Net4_7.verified.txt @@ -6,6 +6,7 @@ namespace public abstract class AbstractDynamicTest { protected AbstractDynamicTest() { } + public int DynamicTestIndex { get; set; } public abstract .<.DiscoveryResult> GetTests(); } public abstract class AbstractDynamicTest : .AbstractDynamicTest @@ -597,6 +598,7 @@ namespace public string? CreatorFilePath { get; set; } public int? CreatorLineNumber { get; set; } public string? DisplayName { get; set; } + public int DynamicTestIndex { get; set; } public string? ParentTestId { get; set; } public .? Properties { get; set; } public .? Relationship { get; set; } @@ -830,7 +832,10 @@ namespace string? CreatorFilePath { get; set; } int? CreatorLineNumber { get; set; } } - public interface IDynamicTestMetadata { } + public interface IDynamicTestMetadata + { + int DynamicTestIndex { get; } + } public interface IDynamicTestSource { .<.AbstractDynamicTest> CollectDynamicTests(string sessionId); diff --git a/TUnit.TestProject/DynamicTests/DynamicTestIndexTests.cs b/TUnit.TestProject/DynamicTests/DynamicTestIndexTests.cs new file mode 100644 index 0000000000..32999d816c --- /dev/null +++ b/TUnit.TestProject/DynamicTests/DynamicTestIndexTests.cs @@ -0,0 +1,36 @@ +using TUnit.TestProject.Attributes; + +namespace TUnit.TestProject.DynamicTests; + +/// +/// Tests that validate DynamicTestIndex generates unique test IDs +/// when multiple dynamic tests target the same method. +/// +[EngineTest(ExpectedResult.Pass)] +public class DynamicTestIndexTests +{ + public void TestMethod(int value) + { + Console.WriteLine($"TestMethod called with value: {value}"); + } + +#pragma warning disable TUnitWIP0001 + [DynamicTestBuilder] +#pragma warning restore TUnitWIP0001 + public void BuildTests(DynamicTestBuilderContext context) + { + // Add 5 dynamic tests all targeting the SAME method with different arguments. + // Before the DynamicTestIndex fix, these would generate duplicate test IDs + // and only one would execute. + for (var i = 1; i <= 5; i++) + { + var value = i; + context.AddTest(new DynamicTest + { + TestMethod = c => c.TestMethod(value), + TestMethodArguments = [value], + Attributes = [] + }); + } + } +}