Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 3 additions & 22 deletions TUnit.Core/Attributes/TestData/ClassDataSources.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand All @@ -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;
}
}
51 changes: 51 additions & 0 deletions TUnit.TestProject/Bugs/3803/TestRabbitContainer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using TUnit.Core.Interfaces;

namespace TUnit.TestProject.Bugs._3803;

/// <summary>
/// Simulates a RabbitMQ test container.
/// This class should be instantiated only once per test session when marked as SharedType.PerTestSession.
/// </summary>
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;
}
}
51 changes: 51 additions & 0 deletions TUnit.TestProject/Bugs/3803/TestSqlContainer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using TUnit.Core.Interfaces;

namespace TUnit.TestProject.Bugs._3803;

/// <summary>
/// Simulates a SQL test container.
/// This class should be instantiated only once per test session when marked as SharedType.PerTestSession.
/// </summary>
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;
}
}
135 changes: 135 additions & 0 deletions TUnit.TestProject/Bugs/3803/Tests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
using TUnit.TestProject.Attributes;

namespace TUnit.TestProject.Bugs._3803;

/// <summary>
/// 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
/// </summary>

[NotInParallel]
[EngineTest(ExpectedResult.Pass)]
public class Bug3803_TestClass1
{
[ClassDataSource<WebApplicationFactory>(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<WebApplicationFactory>(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<WebApplicationFactory>(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);
}
}
61 changes: 61 additions & 0 deletions TUnit.TestProject/Bugs/3803/WebApplicationFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using TUnit.Core.Interfaces;

namespace TUnit.TestProject.Bugs._3803;

/// <summary>
/// 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
/// </summary>
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<TestRabbitContainer>(Shared = SharedType.PerTestSession)]
public required TestRabbitContainer RabbitContainer { get; init; }

[ClassDataSource<TestSqlContainer>(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;
}
}
Loading