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
20 changes: 9 additions & 11 deletions TUnit.Engine/Building/TestBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -766,10 +766,10 @@ private async Task<IDataSourceAttribute[]> GetDataSourcesAsync(IDataSourceAttrib
return [NoDataSource.Instance];
}

// Initialize all data sources to ensure properties are injected
// Inject properties into data sources during discovery (IAsyncInitializer deferred to execution)
foreach (var dataSource in dataSources)
{
await _objectLifecycleService.EnsureInitializedAsync(dataSource);
await _objectLifecycleService.InjectPropertiesAsync(dataSource);
}

return dataSources;
Expand All @@ -783,16 +783,15 @@ private async Task<IDataSourceAttribute[]> GetDataSourcesAsync(IDataSourceAttrib
IDataSourceAttribute dataSource,
DataGeneratorMetadata dataGeneratorMetadata)
{
// Ensure the data source is fully initialized before getting data rows
// This includes property injection and IAsyncInitializer.InitializeAsync
var initializedDataSource = await _objectLifecycleService.EnsureInitializedAsync(
// Inject properties into data source during discovery (IAsyncInitializer deferred to execution)
var propertyInjectedDataSource = await _objectLifecycleService.InjectPropertiesAsync(
dataSource,
dataGeneratorMetadata.TestBuilderContext.Current.StateBag,
dataGeneratorMetadata.TestInformation,
dataGeneratorMetadata.TestBuilderContext.Current.Events);

// Now get data rows from the initialized data source
await foreach (var dataRow in initializedDataSource.GetDataRowsAsync(dataGeneratorMetadata))
// Now get data rows from the property-injected data source
await foreach (var dataRow in propertyInjectedDataSource.GetDataRowsAsync(dataGeneratorMetadata))
{
yield return dataRow;
}
Expand Down Expand Up @@ -1017,14 +1016,13 @@ private async Task<TestDetails> CreateFailedTestDetails(TestMetadata metadata, s

private async Task<Attribute[]> InitializeAttributesAsync(Attribute[] attributes)
{
// Initialize any attributes that need property injection or implement IAsyncInitializer
// This ensures they're fully initialized before being used
// Inject properties into data source attributes during discovery
// IAsyncInitializer.InitializeAsync is deferred to execution time
foreach (var attribute in attributes)
{
if (attribute is IDataSourceAttribute dataSource)
{
// Data source attributes need to be initialized with property injection
await _objectLifecycleService.EnsureInitializedAsync(dataSource);
await _objectLifecycleService.InjectPropertiesAsync(dataSource);
}
}

Expand Down
24 changes: 24 additions & 0 deletions TUnit.Engine/Services/ObjectLifecycleService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,30 @@ await EnsureInitializedAsync(

#region Phase 3: Object Initialization

/// <summary>
/// Injects properties into an object without calling IAsyncInitializer.
/// Used during test discovery to prepare data sources without triggering async initialization.
/// </summary>
public async ValueTask<T> InjectPropertiesAsync<T>(
T obj,
ConcurrentDictionary<string, object?>? objectBag = null,
MethodMetadata? methodMetadata = null,
TestContextEvents? events = null) where T : notnull
{
if (obj == null)
{
throw new ArgumentNullException(nameof(obj));
}
Comment on lines +220 to +223
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant null check: the generic constraint where T : notnull already ensures obj cannot be null at compile time. This check is unnecessary and can be removed.

Suggested change
if (obj == null)
{
throw new ArgumentNullException(nameof(obj));
}

Copilot uses AI. Check for mistakes.

objectBag ??= new ConcurrentDictionary<string, object?>();
events ??= new TestContextEvents();

// Only inject properties, do not call IAsyncInitializer
await PropertyInjector.InjectPropertiesAsync(obj, objectBag, methodMetadata, events);

return obj;
}

/// <summary>
/// Ensures an object is fully initialized (property injection + IAsyncInitializer).
/// Thread-safe with fast-path for already-initialized objects.
Expand Down
41 changes: 41 additions & 0 deletions TUnit.TestProject/Bugs/3992/IAsyncInitializerDiscoveryTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using TUnit.Core.Interfaces;
using TUnit.TestProject.Attributes;

namespace TUnit.TestProject.Bugs._3992;

/// <summary>
/// Regression test for issue #3992: IAsyncInitializer should not run during test discovery.
/// Verifies that IAsyncInitializer.InitializeAsync() is only called during test execution,
/// not during the discovery phase.
/// </summary>
[EngineTest(ExpectedResult.Pass)]
public class IAsyncInitializerDiscoveryTests
{
private static int _initializationCount = 0;

public class DataSourceWithAsyncInit : IAsyncInitializer
{
public bool IsInitialized { get; private set; }

public Task InitializeAsync()
{
Interlocked.Increment(ref _initializationCount);
IsInitialized = true;
Console.WriteLine($"InitializeAsync called (count: {_initializationCount})");
return Task.CompletedTask;
}
}

[Test]
[ClassDataSource<DataSourceWithAsyncInit>]
public async Task DataSource_InitializeAsync_OnlyCalledDuringExecution(DataSourceWithAsyncInit dataSource)
{
// Verify that the data source was initialized
await Assert.That(dataSource.IsInitialized).IsTrue();

// Verify that InitializeAsync was called (at least once during execution)
await Assert.That(_initializationCount).IsGreaterThan(0);

Console.WriteLine($"Test execution confirmed: InitializeAsync was called {_initializationCount} time(s)");
}
Comment on lines +29 to +40
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test coverage gap: This test only covers method parameter data sources ([ClassDataSource<T>]). Consider adding a test case for property-based data sources as well, since properties can also use data source attributes and implement IAsyncInitializer.

Example:

public class PropertyDataSourceWithAsyncInit : IAsyncInitializer
{
    public bool IsInitialized { get; private set; }
    public Task InitializeAsync() { IsInitialized = true; return Task.CompletedTask; }
}

[PropertyDataSource<PropertyDataSourceWithAsyncInit>]
public PropertyDataSourceWithAsyncInit PropertyData { get; set; }

[Test]
public async Task PropertyDataSource_InitializeAsync_OnlyCalledDuringExecution()
{
    await Assert.That(PropertyData.IsInitialized).IsTrue();
}

Copilot uses AI. Check for mistakes.
}
Loading