From 021624118b84c06a0dd0e50a3f415aec2d71d0f2 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Tue, 20 Oct 2026 06:07:33 +0100 Subject: [PATCH 1/2] test: release timeout-helper operation inline so the grace period always observes it The executionCompletesFirst=false path released the operation via a RunContinuationsAsynchronously TCS, so the operation's completion had to be scheduled on the thread pool within TimeoutHelper's 1s grace period. On a busy runner the grace timer won, the outcome was dropped, and InnerException was null. Run the continuation inline and assert the operation has completed before awaiting the helper. Fixes #7172 --- tests/TUnit.UnitTests/TimeoutHelperTests.cs | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/tests/TUnit.UnitTests/TimeoutHelperTests.cs b/tests/TUnit.UnitTests/TimeoutHelperTests.cs index 87925167409..bd295ae2995 100644 --- a/tests/TUnit.UnitTests/TimeoutHelperTests.cs +++ b/tests/TUnit.UnitTests/TimeoutHelperTests.cs @@ -253,7 +253,11 @@ private static async Task ExecuteWithControlledTimeoutAsync( Func operation, bool executionCompletesFirst, TestContext? testContext = null) { using var timeoutCts = new CancellationTokenSource(); - var releaseOperation = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + // Continuations run inline so that SetResult below completes the released operation on this + // thread. Queuing them would let a busy thread pool push the operation past the grace period, + // at which point TimeoutHelper stops waiting and the preserved exception is lost (#7172). + var releaseOperation = new TaskCompletionSource(); + Task? releasedExecution = null; var timeoutTask = TimeoutHelper.ExecuteWithTimeoutAsync( cancellationToken => @@ -261,19 +265,28 @@ private static async Task ExecuteWithControlledTimeoutAsync( testContext?.SetCancellationToken(cancellationToken); // Cancel the caller-owned timeout source without depending on a timer or scheduler delay. timeoutCts.Cancel(); - return executionCompletesFirst ? operation(cancellationToken) : CompleteAfterReleaseAsync(cancellationToken); + return executionCompletesFirst + ? operation(cancellationToken) + : releasedExecution = CompleteAfterReleaseAsync(cancellationToken); }, Timeout.InfiniteTimeSpan, timeoutCts, CancellationToken.None, testContext: testContext); // When both tasks are already complete, WhenAny selects execution (the first argument). // Otherwise timeout detection wins before the operation is released into the grace period. releaseOperation.SetResult(); + + if (releasedExecution is { IsCompleted: false }) + { + throw new InvalidOperationException( + "The released operation must complete synchronously so the grace period observes its outcome."); + } + await timeoutTask; return; async Task CompleteAfterReleaseAsync(CancellationToken cancellationToken) { - await releaseOperation.Task; - await operation(cancellationToken); + await releaseOperation.Task.ConfigureAwait(false); + await operation(cancellationToken).ConfigureAwait(false); } } } From 8445026575cedfe3e724a1e33ae978833f58f323 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Tue, 20 Oct 2026 06:46:36 +0100 Subject: [PATCH 2/2] test: fail the timeout helper guard if the released operation was never created --- tests/TUnit.UnitTests/TimeoutHelperTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/TUnit.UnitTests/TimeoutHelperTests.cs b/tests/TUnit.UnitTests/TimeoutHelperTests.cs index bd295ae2995..e4f6d72dbcc 100644 --- a/tests/TUnit.UnitTests/TimeoutHelperTests.cs +++ b/tests/TUnit.UnitTests/TimeoutHelperTests.cs @@ -274,10 +274,10 @@ private static async Task ExecuteWithControlledTimeoutAsync( // Otherwise timeout detection wins before the operation is released into the grace period. releaseOperation.SetResult(); - if (releasedExecution is { IsCompleted: false }) + if (!executionCompletesFirst && releasedExecution is not { IsCompleted: true }) { throw new InvalidOperationException( - "The released operation must complete synchronously so the grace period observes its outcome."); + "The released operation must be created synchronously by TimeoutHelper and complete synchronously on release so the grace period observes its outcome."); } await timeoutTask;