-
-
Notifications
You must be signed in to change notification settings - Fork 97
Description
Problem Description
When using [ClassDataSource<T>(Shared = SharedType.PerTestSession)], the constructor is sometimes called twice instead of once per test session, especially when initialization takes significant time (e.g., spinning up Docker containers).
Code Example
public sealed class TestRabbitContainer : IAsyncInitializer
{
public TestRabbitContainer()
{
Console.WriteLine("Constructor called"); // This gets logged TWICE
// Initialize container that binds to specific ports
}
public async Task InitializeAsync()
{
await _instance.StartAsync(); // Long-running operation (several seconds)
}
}
// Usage in test
[ClassDataSource<TestRabbitContainer>(Shared = SharedType.PerTestSession)]
public required TestRabbitContainer TestRabbitContainer { get; init; }Impact
This causes serious issues with resource-heavy shared data:
- Port binding conflicts: Two containers trying to bind to the same port
- Resource waste: Duplicate expensive operations (Docker container startup, database initialization)
- Test failures: Second instance fails to initialize due to resource conflicts
- Performance degradation: Unnecessary duplication of time-consuming operations
Root Cause Analysis
Based on codebase investigation:
-
Shared Instance Management: In
TestDataContainer.cs, shared instances are stored in static dictionaries:private static readonly ScopedDictionary<string> _globalContainer = new();
-
GetOrCreate Pattern: In
ScopedDictionary.cs(lines 10-22):public object? GetOrCreate(TScope scope, Type type, Func<Type, object?> factory) { var innerDictionary = _scopedContainers.GetOrAdd(scope, ...); var obj = innerDictionary.GetOrAdd(type, factory); // ... }
-
Potential Race Condition: When multiple tests request the same shared instance simultaneously AND the factory function (constructor + initialization) takes significant time, there may be a race condition where:
- Test A requests shared instance → starts creation
- Test B requests shared instance before Test A completes → triggers second creation
- Both constructors run to completion
-
Initialization Tracking: The
ObjectInitializer.csuses aConditionalWeakTableto track initialization, but the check inDataSourceInitializer.cs(line 45) may have a TOCTOU (Time-of-Check-Time-of-Use) issue:if (_initializationTasks.TryGetValue(dataSource, out var existingTcs) && existingTcs.Task.IsCompleted)
Expected vs Actual Behavior
- Expected: Single instance created per test session, constructor runs exactly once
- Actual: Constructor sometimes runs twice when initialization is resource-intensive
Regression Information
- ✅ Worked correctly in: v0.67.19
- ❌ Broken in: v1.0.78
- ❓ Current status: Still experiencing issues in latest versions (reported fix in v1.1.0 didn't fully resolve)
Reproduction
Most likely to occur when:
- Using
SharedType.PerTestSession - Constructor or
IAsyncInitializer.InitializeAsync()takes several seconds - Multiple tests run in parallel attempting to access the shared resource
- Resource involves exclusive locks (ports, files, etc.)
Related
Originally reported in discussion #3803