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
1 change: 1 addition & 0 deletions TUnit.Engine/Framework/TUnitServiceProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,7 @@ public TUnitServiceProvider(IExtension extension,
circularDependencyDetector,
constraintKeyScheduler,
hookExecutor,
afterHookPairTracker,
staticPropertyHandler,
dynamicTestQueue));

Expand Down
6 changes: 5 additions & 1 deletion TUnit.Engine/Scheduling/TestScheduler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ internal sealed class TestScheduler : ITestScheduler
private readonly CircularDependencyDetector _circularDependencyDetector;
private readonly IConstraintKeyScheduler _constraintKeyScheduler;
private readonly HookExecutor _hookExecutor;
private readonly AfterHookPairTracker _afterHookPairTracker;
private readonly StaticPropertyHandler _staticPropertyHandler;
private readonly IDynamicTestQueue _dynamicTestQueue;
private readonly int _maxParallelism;
Expand All @@ -39,6 +40,7 @@ public TestScheduler(
CircularDependencyDetector circularDependencyDetector,
IConstraintKeyScheduler constraintKeyScheduler,
HookExecutor hookExecutor,
AfterHookPairTracker afterHookPairTracker,
StaticPropertyHandler staticPropertyHandler,
IDynamicTestQueue dynamicTestQueue)
{
Expand All @@ -51,6 +53,7 @@ public TestScheduler(
_circularDependencyDetector = circularDependencyDetector;
_constraintKeyScheduler = constraintKeyScheduler;
_hookExecutor = hookExecutor;
_afterHookPairTracker = afterHookPairTracker;
_staticPropertyHandler = staticPropertyHandler;
_dynamicTestQueue = dynamicTestQueue;

Expand Down Expand Up @@ -136,7 +139,8 @@ public async Task<bool> ScheduleAndExecuteAsync(
// Execute tests according to their grouping
await ExecuteGroupedTestsAsync(groupedTests, cancellationToken).ConfigureAwait(false);

var sessionHookExceptions = await _hookExecutor.ExecuteAfterTestSessionHooksAsync(cancellationToken).ConfigureAwait(false) ?? [];
var sessionHookExceptions = await _afterHookPairTracker.GetOrCreateAfterTestSessionTask(
() => _hookExecutor.ExecuteAfterTestSessionHooksAsync(cancellationToken)).ConfigureAwait(false) ?? [];

await _staticPropertyHandler.DisposeStaticPropertiesAsync(sessionHookExceptions).ConfigureAwait(false);

Expand Down
15 changes: 14 additions & 1 deletion TUnit.Engine/Services/AfterHookPairTracker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,30 @@ internal sealed class AfterHookPairTracker
private readonly Lock _testSessionLock = new();
private readonly Lock _classLock = new();

// Ensure only the first call to RegisterAfterTestSessionHook registers a callback.
// Subsequent calls (e.g. from per-test timeout tokens) are ignored so that
// a test timeout cannot prematurely trigger session-level After hooks.
private volatile bool _sessionHookRegistered;

// Track cancellation registrations for cleanup
private readonly ConcurrentBag<CancellationTokenRegistration> _registrations = [];

/// <summary>
/// Registers Session After hooks to run on cancellation or normal completion.
/// Ensures After hooks run exactly once even if called both ways.
/// Only the first call registers a callback; subsequent calls are no-ops.
/// This prevents per-test timeout tokens from prematurely firing session hooks.
/// </summary>
public void RegisterAfterTestSessionHook(
CancellationToken cancellationToken,
Func<ValueTask<List<Exception>>> afterHookExecutor)
{
if (_sessionHookRegistered)
{
return;
}

_sessionHookRegistered = true;

// Register callback to run After hook on cancellation
var registration = cancellationToken.Register(() =>
{
Expand Down
Loading