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
21 changes: 21 additions & 0 deletions TUnit.Engine.Tests/PropertyInjectionInitFailureTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using Shouldly;
using TUnit.Engine.Tests.Enums;

namespace TUnit.Engine.Tests;

public class PropertyInjectionInitFailureTests(TestMode testMode) : InvokableTestBase(testMode)
{
[Test]
public async Task Test()
{
await RunTestsWithFilter(
"/*/*/PropertyInjectionInitFailureTests/*",
[
result => result.ResultSummary.Outcome.ShouldBe("Failed"),
result => result.ResultSummary.Counters.Total.ShouldBe(1),
result => result.ResultSummary.Counters.Passed.ShouldBe(0),
result => result.ResultSummary.Counters.Failed.ShouldBe(1),
result => result.ResultSummary.Counters.NotExecuted.ShouldBe(0)
]);
}
}
4 changes: 2 additions & 2 deletions TUnit.Engine/Building/Interfaces/ITestBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ internal interface ITestBuilder
/// <param name="testBuilderContext"></param>
/// <param name="isReusingDiscoveryInstance">Whether this test is reusing the discovery instance</param>
/// <returns>An executable test ready for execution</returns>
Task<AbstractExecutableTest> BuildTestAsync(TestMetadata metadata, TestBuilder.TestData testData, TestBuilderContext testBuilderContext, bool isReusingDiscoveryInstance = false);
Task<AbstractExecutableTest> BuildTestAsync(TestMetadata metadata, TestBuilder.TestData testData, TestBuilderContext testBuilderContext, bool isReusingDiscoveryInstance = false, CancellationToken cancellationToken = default);

/// <summary>
/// Builds all executable tests from a single TestMetadata using its DataCombinationGenerator delegate.
Expand All @@ -28,7 +28,7 @@ internal interface ITestBuilder
#if NET6_0_OR_GREATER
[RequiresUnreferencedCode("Test building in reflection mode uses generic type resolution which requires unreferenced code")]
#endif
Task<IEnumerable<AbstractExecutableTest>> BuildTestsFromMetadataAsync(TestMetadata metadata, TestBuildingContext buildingContext);
Task<IEnumerable<AbstractExecutableTest>> BuildTestsFromMetadataAsync(TestMetadata metadata, TestBuildingContext buildingContext, CancellationToken cancellationToken = default);

/// <summary>
/// Streaming version that yields tests as they're built without buffering
Expand Down
76 changes: 43 additions & 33 deletions TUnit.Engine/Building/TestBuilder.cs

Large diffs are not rendered by default.

10 changes: 5 additions & 5 deletions TUnit.Engine/Building/TestBuilderPipeline.cs
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ public async Task<IEnumerable<AbstractExecutableTest>> BuildTestsStreamingAsync(
}

return await collectedMetadata
.SelectManyAsync(metadata => BuildTestsFromSingleMetadataAsync(metadata, buildingContext), cancellationToken: cancellationToken)
.SelectManyAsync(metadata => BuildTestsFromSingleMetadataAsync(metadata, buildingContext, cancellationToken), cancellationToken: cancellationToken)
.ProcessInParallel(cancellationToken: cancellationToken);
}

Expand Down Expand Up @@ -170,7 +170,7 @@ public async Task<IEnumerable<AbstractExecutableTest>> BuildTestsFromMetadataAsy
}
else
{
results.Add(await _testBuilder.BuildTestsFromMetadataAsync(metadata, buildingContext).ConfigureAwait(false));
results.Add(await _testBuilder.BuildTestsFromMetadataAsync(metadata, buildingContext, cancellationToken).ConfigureAwait(false));
}
}
catch (Exception ex)
Expand All @@ -194,7 +194,7 @@ public async Task<IEnumerable<AbstractExecutableTest>> BuildTestsFromMetadataAsy
return await GenerateDynamicTests(metadata).ConfigureAwait(false);
}

return await _testBuilder.BuildTestsFromMetadataAsync(metadata, buildingContext).ConfigureAwait(false);
return await _testBuilder.BuildTestsFromMetadataAsync(metadata, buildingContext, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
Expand Down Expand Up @@ -308,7 +308,7 @@ private async Task<AbstractExecutableTest[]> GenerateDynamicTests(TestMetadata m
#if NET6_0_OR_GREATER
[RequiresUnreferencedCode("Test building in reflection mode uses generic type resolution which requires unreferenced code")]
#endif
private async IAsyncEnumerable<AbstractExecutableTest> BuildTestsFromSingleMetadataAsync(TestMetadata metadata, TestBuildingContext buildingContext)
private async IAsyncEnumerable<AbstractExecutableTest> BuildTestsFromSingleMetadataAsync(TestMetadata metadata, TestBuildingContext buildingContext, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken = default)
{
TestMetadata resolvedMetadata;
Exception? resolutionError = null;
Expand Down Expand Up @@ -430,7 +430,7 @@ private async IAsyncEnumerable<AbstractExecutableTest> BuildTestsFromSingleMetad
else
{
// Normal test metadata goes through the standard test builder
var testsFromMetadata = await _testBuilder.BuildTestsFromMetadataAsync(resolvedMetadata, buildingContext).ConfigureAwait(false);
var testsFromMetadata = await _testBuilder.BuildTestsFromMetadataAsync(resolvedMetadata, buildingContext, cancellationToken).ConfigureAwait(false);
testsToYield = new List<AbstractExecutableTest>(testsFromMetadata);
}
}
Expand Down
6 changes: 4 additions & 2 deletions TUnit.Engine/Services/IObjectRegistry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ Task RegisterObjectAsync(
object instance,
ConcurrentDictionary<string, object?> objectBag,
MethodMetadata? methodMetadata,
TestContextEvents events);
TestContextEvents events,
CancellationToken cancellationToken = default);

/// <summary>
/// Registers multiple argument objects during the registration phase.
Expand All @@ -31,5 +32,6 @@ Task RegisterArgumentsAsync(
object?[] arguments,
ConcurrentDictionary<string, object?> objectBag,
MethodMetadata? methodMetadata,
TestContextEvents events);
TestContextEvents events,
CancellationToken cancellationToken = default);
}
21 changes: 12 additions & 9 deletions TUnit.Engine/Services/ObjectLifecycleService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public ObjectLifecycleService(
/// Tracks the resolved objects so reference counting works correctly across all tests.
/// Does NOT call IAsyncInitializer (deferred to execution).
/// </summary>
public async Task RegisterTestAsync(TestContext testContext)
public async Task RegisterTestAsync(TestContext testContext, CancellationToken cancellationToken = default)
{
var objectBag = testContext.StateBag.Items;
var methodMetadata = testContext.Metadata.TestDetails.MethodMetadata;
Expand All @@ -61,7 +61,7 @@ public async Task RegisterTestAsync(TestContext testContext)

// Resolve property values (creating shared objects) and cache them WITHOUT setting on placeholder instance
// This ensures shared objects are created once and tracked with the correct reference count
await PropertyInjector.ResolveAndCachePropertiesAsync(testClassType, objectBag, methodMetadata, events, testContext);
await PropertyInjector.ResolveAndCachePropertiesAsync(testClassType, objectBag, methodMetadata, events, testContext, cancellationToken);

// Track the cached objects so they get the correct reference count
_objectTracker.TrackObjects(testContext);
Expand All @@ -75,15 +75,16 @@ public Task RegisterObjectAsync(
object instance,
ConcurrentDictionary<string, object?> objectBag,
MethodMetadata? methodMetadata,
TestContextEvents events)
TestContextEvents events,
CancellationToken cancellationToken = default)
{
if (instance == null)
{
throw new ArgumentNullException(nameof(instance));
}

// Inject properties during registration
return PropertyInjector.InjectPropertiesAsync(instance, objectBag, methodMetadata, events);
return PropertyInjector.InjectPropertiesAsync(instance, objectBag, methodMetadata, events, cancellationToken);
}

/// <summary>
Expand All @@ -93,7 +94,8 @@ public Task RegisterArgumentsAsync(
object?[] arguments,
ConcurrentDictionary<string, object?> objectBag,
MethodMetadata? methodMetadata,
TestContextEvents events)
TestContextEvents events,
CancellationToken cancellationToken = default)
{
if (arguments == null || arguments.Length == 0)
{
Expand All @@ -106,7 +108,7 @@ public Task RegisterArgumentsAsync(
{
if (argument != null)
{
tasks.Append(RegisterObjectAsync(argument, objectBag, methodMetadata, events));
tasks.Append(RegisterObjectAsync(argument, objectBag, methodMetadata, events, cancellationToken));
}
}

Expand Down Expand Up @@ -290,7 +292,8 @@ public async ValueTask<T> InjectPropertiesAsync<T>(
T obj,
ConcurrentDictionary<string, object?>? objectBag = null,
MethodMetadata? methodMetadata = null,
TestContextEvents? events = null) where T : notnull
TestContextEvents? events = null,
CancellationToken cancellationToken = default) where T : notnull
{
if (obj == null)
{
Expand All @@ -301,7 +304,7 @@ public async ValueTask<T> InjectPropertiesAsync<T>(
events ??= new TestContextEvents();

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

return obj;
}
Expand Down Expand Up @@ -391,7 +394,7 @@ private async Task InitializeObjectCoreAsync(
// This aligns with ObjectInitializer behavior and provides cleaner stack traces

// Step 1: Inject properties
await PropertyInjector.InjectPropertiesAsync(obj, objectBag, methodMetadata, events);
await PropertyInjector.InjectPropertiesAsync(obj, objectBag, methodMetadata, events, cancellationToken);

// Step 2: Initialize nested objects depth-first (discovery-only)
await InitializeNestedObjectsForDiscoveryAsync(obj, cancellationToken);
Expand Down
Loading
Loading