diff --git a/TUnit.Core/Attributes/TestData/ClassDataSources.cs b/TUnit.Core/Attributes/TestData/ClassDataSources.cs index a933e860b4..efad3790cd 100644 --- a/TUnit.Core/Attributes/TestData/ClassDataSources.cs +++ b/TUnit.Core/Attributes/TestData/ClassDataSources.cs @@ -73,24 +73,21 @@ private string GetKey(int index, SharedType[] sharedTypes, string[] keys) private static object Create([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)] Type type, DataGeneratorMetadata dataGeneratorMetadata) { - return CreateWithNestedDependencies(type, dataGeneratorMetadata, recursionDepth: 0); + return Create(type, dataGeneratorMetadata, recursionDepth: 0); } private const int MaxRecursionDepth = 10; - [UnconditionalSuppressMessage("Trimming", "IL2072:Target parameter argument does not satisfy 'DynamicallyAccessedMembersAttribute' requirements", - Justification = "PropertyType from PropertyInjectionMetadata has the required DynamicallyAccessedMembers annotations")] - private static object CreateWithNestedDependencies([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)] Type type, DataGeneratorMetadata dataGeneratorMetadata, int recursionDepth) + private static object Create([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)] Type type, DataGeneratorMetadata dataGeneratorMetadata, int recursionDepth) { if (recursionDepth >= MaxRecursionDepth) { throw new InvalidOperationException($"Maximum recursion depth ({MaxRecursionDepth}) exceeded when creating nested ClassDataSource dependencies. This may indicate a circular dependency."); } - object instance; try { - instance = Activator.CreateInstance(type)!; + return Activator.CreateInstance(type)!; } catch (TargetInvocationException targetInvocationException) { @@ -101,21 +98,5 @@ private static object CreateWithNestedDependencies([DynamicallyAccessedMembers(D throw; } - - // Populate nested ClassDataSource properties recursively - var propertySource = PropertySourceRegistry.GetSource(type); - if (propertySource?.ShouldInitialize == true) - { - var propertyMetadata = propertySource.GetPropertyMetadata(); - foreach (var metadata in propertyMetadata) - { - // Recursively create the property value using CreateWithNestedDependencies - // This will handle nested ClassDataSource properties - var propertyValue = CreateWithNestedDependencies(metadata.PropertyType, dataGeneratorMetadata, recursionDepth + 1); - metadata.SetProperty(instance, propertyValue); - } - } - - return instance; } } diff --git a/TUnit.TestProject/Bugs/3803/TestRabbitContainer.cs b/TUnit.TestProject/Bugs/3803/TestRabbitContainer.cs new file mode 100644 index 0000000000..58c1a14622 --- /dev/null +++ b/TUnit.TestProject/Bugs/3803/TestRabbitContainer.cs @@ -0,0 +1,51 @@ +using TUnit.Core.Interfaces; + +namespace TUnit.TestProject.Bugs._3803; + +/// +/// Simulates a RabbitMQ test container. +/// This class should be instantiated only once per test session when marked as SharedType.PerTestSession. +/// +public class TestRabbitContainer : IAsyncInitializer, IAsyncDisposable +{ + private static int _instanceCount = 0; + private static int _initializeCount = 0; + private static int _disposeCount = 0; + + public static int InstanceCount => _instanceCount; + public static int InitializeCount => _initializeCount; + public static int DisposeCount => _disposeCount; + + public int InstanceId { get; } + public bool IsInitialized { get; private set; } + public bool IsDisposed { get; private set; } + + public TestRabbitContainer() + { + InstanceId = Interlocked.Increment(ref _instanceCount); + Console.WriteLine($"[TestRabbitContainer] Constructor called - Instance #{InstanceId}"); + } + + public Task InitializeAsync() + { + Interlocked.Increment(ref _initializeCount); + IsInitialized = true; + Console.WriteLine($"[TestRabbitContainer] InitializeAsync called - Instance #{InstanceId}"); + return Task.CompletedTask; + } + + public ValueTask DisposeAsync() + { + Interlocked.Increment(ref _disposeCount); + IsDisposed = true; + Console.WriteLine($"[TestRabbitContainer] DisposeAsync called - Instance #{InstanceId}"); + return default; + } + + public static void ResetCounters() + { + _instanceCount = 0; + _initializeCount = 0; + _disposeCount = 0; + } +} diff --git a/TUnit.TestProject/Bugs/3803/TestSqlContainer.cs b/TUnit.TestProject/Bugs/3803/TestSqlContainer.cs new file mode 100644 index 0000000000..57c6f317db --- /dev/null +++ b/TUnit.TestProject/Bugs/3803/TestSqlContainer.cs @@ -0,0 +1,51 @@ +using TUnit.Core.Interfaces; + +namespace TUnit.TestProject.Bugs._3803; + +/// +/// Simulates a SQL test container. +/// This class should be instantiated only once per test session when marked as SharedType.PerTestSession. +/// +public class TestSqlContainer : IAsyncInitializer, IAsyncDisposable +{ + private static int _instanceCount = 0; + private static int _initializeCount = 0; + private static int _disposeCount = 0; + + public static int InstanceCount => _instanceCount; + public static int InitializeCount => _initializeCount; + public static int DisposeCount => _disposeCount; + + public int InstanceId { get; } + public bool IsInitialized { get; private set; } + public bool IsDisposed { get; private set; } + + public TestSqlContainer() + { + InstanceId = Interlocked.Increment(ref _instanceCount); + Console.WriteLine($"[TestSqlContainer] Constructor called - Instance #{InstanceId}"); + } + + public Task InitializeAsync() + { + Interlocked.Increment(ref _initializeCount); + IsInitialized = true; + Console.WriteLine($"[TestSqlContainer] InitializeAsync called - Instance #{InstanceId}"); + return Task.CompletedTask; + } + + public ValueTask DisposeAsync() + { + Interlocked.Increment(ref _disposeCount); + IsDisposed = true; + Console.WriteLine($"[TestSqlContainer] DisposeAsync called - Instance #{InstanceId}"); + return default; + } + + public static void ResetCounters() + { + _instanceCount = 0; + _initializeCount = 0; + _disposeCount = 0; + } +} diff --git a/TUnit.TestProject/Bugs/3803/Tests.cs b/TUnit.TestProject/Bugs/3803/Tests.cs new file mode 100644 index 0000000000..556c173d52 --- /dev/null +++ b/TUnit.TestProject/Bugs/3803/Tests.cs @@ -0,0 +1,135 @@ +using TUnit.TestProject.Attributes; + +namespace TUnit.TestProject.Bugs._3803; + +/// +/// Bug #3803: Nested dependencies with SharedType.PerTestSession are instantiated multiple times +/// +/// Expected behavior: +/// - TestRabbitContainer should be instantiated ONCE per test session (InstanceCount == 1) +/// - TestSqlContainer should be instantiated ONCE per test session (InstanceCount == 1) +/// - All WebApplicationFactory instances should receive the SAME container instances +/// +/// Actual behavior (BUG): +/// - Containers are instantiated multiple times (once per test or once per factory) +/// - Each WebApplicationFactory receives DIFFERENT container instances +/// + +[NotInParallel] +[EngineTest(ExpectedResult.Pass)] +public class Bug3803_TestClass1 +{ + [ClassDataSource(Shared = SharedType.PerTestSession)] + public required WebApplicationFactory Factory { get; init; } + + [Test] + public async Task Test1_VerifyContainersAreShared() + { + Console.WriteLine($"[Bug3803_TestClass1.Test1] Factory Instance: #{Factory.InstanceId}"); + Console.WriteLine($"[Bug3803_TestClass1.Test1] RabbitContainer Instance: #{Factory.RabbitContainer.InstanceId}"); + Console.WriteLine($"[Bug3803_TestClass1.Test1] SqlContainer Instance: #{Factory.SqlContainer.InstanceId}"); + + // Verify containers are initialized + await Assert.That(Factory.RabbitContainer.IsInitialized).IsTrue(); + await Assert.That(Factory.SqlContainer.IsInitialized).IsTrue(); + + // BUG VERIFICATION: These should ALWAYS be 1 if SharedType.PerTestSession works correctly + await Assert.That(TestRabbitContainer.InstanceCount).IsEqualTo(1); + await Assert.That(TestSqlContainer.InstanceCount).IsEqualTo(1); + + // All instances should have ID = 1 (first and only instance) + await Assert.That(Factory.RabbitContainer.InstanceId).IsEqualTo(1); + await Assert.That(Factory.SqlContainer.InstanceId).IsEqualTo(1); + } + + [Test] + public async Task Test2_VerifyContainersAreStillShared() + { + Console.WriteLine($"[Bug3803_TestClass1.Test2] Factory Instance: #{Factory.InstanceId}"); + Console.WriteLine($"[Bug3803_TestClass1.Test2] RabbitContainer Instance: #{Factory.RabbitContainer.InstanceId}"); + Console.WriteLine($"[Bug3803_TestClass1.Test2] SqlContainer Instance: #{Factory.SqlContainer.InstanceId}"); + + // Same assertions as Test1 - containers should still be the same instances + await Assert.That(TestRabbitContainer.InstanceCount).IsEqualTo(1); + await Assert.That(TestSqlContainer.InstanceCount).IsEqualTo(1); + await Assert.That(Factory.RabbitContainer.InstanceId).IsEqualTo(1); + await Assert.That(Factory.SqlContainer.InstanceId).IsEqualTo(1); + } + + [Test] + public async Task Test3_VerifyInitializationCalledOnce() + { + Console.WriteLine($"[Bug3803_TestClass1.Test3] RabbitContainer InitializeCount: {TestRabbitContainer.InitializeCount}"); + Console.WriteLine($"[Bug3803_TestClass1.Test3] SqlContainer InitializeCount: {TestSqlContainer.InitializeCount}"); + + // Initialize should be called only once per container + await Assert.That(TestRabbitContainer.InitializeCount).IsEqualTo(1); + await Assert.That(TestSqlContainer.InitializeCount).IsEqualTo(1); + } +} + +[NotInParallel] +[EngineTest(ExpectedResult.Pass)] +public class Bug3803_TestClass2 +{ + [ClassDataSource(Shared = SharedType.PerTestSession)] + public required WebApplicationFactory Factory { get; init; } + + [Test] + public async Task Test1_DifferentClassShouldGetSameContainers() + { + Console.WriteLine($"[Bug3803_TestClass2.Test1] Factory Instance: #{Factory.InstanceId}"); + Console.WriteLine($"[Bug3803_TestClass2.Test1] RabbitContainer Instance: #{Factory.RabbitContainer.InstanceId}"); + Console.WriteLine($"[Bug3803_TestClass2.Test1] SqlContainer Instance: #{Factory.SqlContainer.InstanceId}"); + + // Even in a different test class, we should get the SAME container instances + await Assert.That(TestRabbitContainer.InstanceCount).IsEqualTo(1); + await Assert.That(TestSqlContainer.InstanceCount).IsEqualTo(1); + + // Should be the same instance (ID = 1) + await Assert.That(Factory.RabbitContainer.InstanceId).IsEqualTo(1); + await Assert.That(Factory.SqlContainer.InstanceId).IsEqualTo(1); + } + + [Test] + public async Task Test2_VerifyContainersAreInitialized() + { + await Assert.That(Factory.RabbitContainer.IsInitialized).IsTrue(); + await Assert.That(Factory.SqlContainer.IsInitialized).IsTrue(); + } +} + +[NotInParallel] +[EngineTest(ExpectedResult.Pass)] +public class Bug3803_TestClass3 +{ + [ClassDataSource(Shared = SharedType.PerTestSession)] + public required WebApplicationFactory Factory { get; init; } + + [Test] + public async Task Test1_ThirdClassAlsoGetsSameContainers() + { + Console.WriteLine($"[Bug3803_TestClass3.Test1] Factory Instance: #{Factory.InstanceId}"); + Console.WriteLine($"[Bug3803_TestClass3.Test1] RabbitContainer Instance: #{Factory.RabbitContainer.InstanceId}"); + Console.WriteLine($"[Bug3803_TestClass3.Test1] SqlContainer Instance: #{Factory.SqlContainer.InstanceId}"); + + // Still the same instances + await Assert.That(TestRabbitContainer.InstanceCount).IsEqualTo(1); + await Assert.That(TestSqlContainer.InstanceCount).IsEqualTo(1); + await Assert.That(Factory.RabbitContainer.InstanceId).IsEqualTo(1); + await Assert.That(Factory.SqlContainer.InstanceId).IsEqualTo(1); + } + + [Test] + public async Task Test2_FinalVerification() + { + Console.WriteLine($"[Bug3803_TestClass3.Test2] Final verification"); + Console.WriteLine($" Total RabbitContainer instances: {TestRabbitContainer.InstanceCount}"); + Console.WriteLine($" Total SqlContainer instances: {TestSqlContainer.InstanceCount}"); + Console.WriteLine($" Total WebApplicationFactory instances: {WebApplicationFactory.InstanceCount}"); + + // Final assertion: containers should have been instantiated exactly once + await Assert.That(TestRabbitContainer.InstanceCount).IsEqualTo(1); + await Assert.That(TestSqlContainer.InstanceCount).IsEqualTo(1); + } +} diff --git a/TUnit.TestProject/Bugs/3803/WebApplicationFactory.cs b/TUnit.TestProject/Bugs/3803/WebApplicationFactory.cs new file mode 100644 index 0000000000..3172f4d792 --- /dev/null +++ b/TUnit.TestProject/Bugs/3803/WebApplicationFactory.cs @@ -0,0 +1,61 @@ +using TUnit.Core.Interfaces; + +namespace TUnit.TestProject.Bugs._3803; + +/// +/// Simulates a WebApplicationFactory that depends on test containers. +/// The containers should be shared (SharedType.PerTestSession), meaning: +/// - Each container should be instantiated only ONCE per test session +/// - All instances of WebApplicationFactory should receive the SAME container instances +/// +public class WebApplicationFactory : IAsyncInitializer, IAsyncDisposable +{ + private static int _instanceCount = 0; + private static int _initializeCount = 0; + private static int _disposeCount = 0; + + public static int InstanceCount => _instanceCount; + public static int InitializeCount => _initializeCount; + public static int DisposeCount => _disposeCount; + + public int InstanceId { get; } + public bool IsInitialized { get; private set; } + public bool IsDisposed { get; private set; } + + [ClassDataSource(Shared = SharedType.PerTestSession)] + public required TestRabbitContainer RabbitContainer { get; init; } + + [ClassDataSource(Shared = SharedType.PerTestSession)] + public required TestSqlContainer SqlContainer { get; init; } + + public WebApplicationFactory() + { + InstanceId = Interlocked.Increment(ref _instanceCount); + Console.WriteLine($"[WebApplicationFactory] Constructor called - Instance #{InstanceId}"); + } + + public Task InitializeAsync() + { + Interlocked.Increment(ref _initializeCount); + IsInitialized = true; + Console.WriteLine($"[WebApplicationFactory] InitializeAsync called - Instance #{InstanceId}"); + Console.WriteLine($" -> RabbitContainer Instance: #{RabbitContainer.InstanceId}"); + Console.WriteLine($" -> SqlContainer Instance: #{SqlContainer.InstanceId}"); + return Task.CompletedTask; + } + + public ValueTask DisposeAsync() + { + Interlocked.Increment(ref _disposeCount); + IsDisposed = true; + Console.WriteLine($"[WebApplicationFactory] DisposeAsync called - Instance #{InstanceId}"); + return default; + } + + public static void ResetCounters() + { + _instanceCount = 0; + _initializeCount = 0; + _disposeCount = 0; + } +}