From 5e500ab7f55f41a79d9bbb25ac76fd1caef89e7f Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Tue, 20 Oct 2026 06:02:43 +0100 Subject: [PATCH 1/2] test(assertions): stop cancellation tests depending on sub-second wall-clock budgets The WaitsFor/Eventually cancellation tests asserted elapsed times of 500ms-1s, which loaded macOS CI runners exceeded. Use a 30s internal timeout and a 10s elapsed budget: an ignored token would surface as an AssertionException after 30s, so the OperationCanceledException check plus a budget far below the timeout still proves cancellation short-circuited it. Apply the same treatment to the CompletesWithin unhappy-path tests. Fixes #7169 --- .../ExecutionTimeTests.cs | 8 +++++-- .../WaitsForAssertionTests.cs | 24 ++++++++++++------- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/tests/TUnit.Assertions.Tests/ExecutionTimeTests.cs b/tests/TUnit.Assertions.Tests/ExecutionTimeTests.cs index 0f63da1bb65..211c892c2be 100644 --- a/tests/TUnit.Assertions.Tests/ExecutionTimeTests.cs +++ b/tests/TUnit.Assertions.Tests/ExecutionTimeTests.cs @@ -27,7 +27,9 @@ await Assert.That( var duration = stopwatch.Elapsed; - await Assert.That(duration).IsLessThan(TimeSpan.FromSeconds(1)); + // Well under the 10s action, so the 250ms limit must have cut it short, but loose enough + // for loaded CI runners where a sub-second budget is flaky (see #7169). + await Assert.That(duration).IsLessThan(TimeSpan.FromSeconds(5)); } [Test] @@ -52,6 +54,8 @@ await Assert.That( var duration = stopwatch.Elapsed; - await Assert.That(duration).IsLessThan(TimeSpan.FromSeconds(1)); + // Well under the 10s action, so the 250ms limit must have cut it short, but loose enough + // for loaded CI runners where a sub-second budget is flaky (see #7169). + await Assert.That(duration).IsLessThan(TimeSpan.FromSeconds(5)); } } diff --git a/tests/TUnit.Assertions.Tests/WaitsForAssertionTests.cs b/tests/TUnit.Assertions.Tests/WaitsForAssertionTests.cs index fc335a0bb5c..360bda70f49 100644 --- a/tests/TUnit.Assertions.Tests/WaitsForAssertionTests.cs +++ b/tests/TUnit.Assertions.Tests/WaitsForAssertionTests.cs @@ -5,6 +5,14 @@ namespace TUnit.Assertions.Tests; [NotInParallel] public class WaitsForAssertionTests { + // The cancellation tests use a long internal timeout so that "cancellation won" is never decided + // by wall-clock noise. If the supplied token were ignored, the internal timeout would produce an + // AssertionException (failing the Throws check) after 30s. The elapsed + // budget is a third of that: it still proves the timeout was short-circuited, while tolerating + // heavily loaded CI runners (see #7169), where sub-second budgets proved flaky. + private static readonly TimeSpan CancellationTestTimeout = TimeSpan.FromSeconds(30); + private static readonly TimeSpan CancellationElapsedBudget = TimeSpan.FromSeconds(10); + [Test] public async Task WaitsFor_Passes_Immediately_When_Assertion_Succeeds() { @@ -419,14 +427,14 @@ public async Task WaitsFor_PropagatesExternalCancellation_Before_Internal_Timeou var act = async () => await Assert.That(() => false) .WaitsFor( assert => assert.IsTrue(), - timeout: TimeSpan.FromSeconds(30), + timeout: CancellationTestTimeout, cancellationToken: cts.Token); await Assert.That(act).Throws(); stopwatch.Stop(); - await Assert.That(stopwatch.Elapsed).IsLessThan(TimeSpan.FromSeconds(1)); + await Assert.That(stopwatch.Elapsed).IsLessThan(CancellationElapsedBudget); } [Test] @@ -454,14 +462,14 @@ public async Task WaitsFor_Honours_PreCancelled_Token_Before_First_Poll() var act = async () => await Assert.That(() => false) .WaitsFor( assert => assert.IsTrue(), - timeout: TimeSpan.FromSeconds(5), + timeout: CancellationTestTimeout, cancellationToken: cts.Token); await Assert.That(act).Throws(); stopwatch.Stop(); - await Assert.That(stopwatch.Elapsed).IsLessThan(TimeSpan.FromMilliseconds(500)); + await Assert.That(stopwatch.Elapsed).IsLessThan(CancellationElapsedBudget); } [Test] @@ -475,14 +483,14 @@ public async Task Eventually_PropagatesExternalCancellation_Before_Internal_Time var act = async () => await Assert.That(() => false) .Eventually( assert => assert.IsTrue(), - timeout: TimeSpan.FromSeconds(30), + timeout: CancellationTestTimeout, cancellationToken: cts.Token); await Assert.That(act).Throws(); stopwatch.Stop(); - await Assert.That(stopwatch.Elapsed).IsLessThan(TimeSpan.FromSeconds(1)); + await Assert.That(stopwatch.Elapsed).IsLessThan(CancellationElapsedBudget); } [Test] @@ -502,14 +510,14 @@ public async Task WaitsFor_Propagates_OCE_When_Predicate_Observes_Supplied_Token var act = async () => await Assert.That(tokenAwarePredicate) .WaitsFor( assert => assert.IsTrue(), - timeout: TimeSpan.FromSeconds(30), + timeout: CancellationTestTimeout, cancellationToken: cts.Token); await Assert.That(act).Throws(); stopwatch.Stop(); - await Assert.That(stopwatch.Elapsed).IsLessThan(TimeSpan.FromMilliseconds(500)); + await Assert.That(stopwatch.Elapsed).IsLessThan(CancellationElapsedBudget); } // Helper class for testing complex objects From 99784e8f8c88c9715d0caf1e86c04770e9966526 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Tue, 20 Oct 2026 06:22:28 +0100 Subject: [PATCH 2/2] test(assertions): explain why the predicate cancellation test shares one token --- tests/TUnit.Assertions.Tests/WaitsForAssertionTests.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/TUnit.Assertions.Tests/WaitsForAssertionTests.cs b/tests/TUnit.Assertions.Tests/WaitsForAssertionTests.cs index 360bda70f49..adf490f8fd0 100644 --- a/tests/TUnit.Assertions.Tests/WaitsForAssertionTests.cs +++ b/tests/TUnit.Assertions.Tests/WaitsForAssertionTests.cs @@ -496,6 +496,11 @@ public async Task Eventually_PropagatesExternalCancellation_Before_Internal_Time [Test] public async Task WaitsFor_Propagates_OCE_When_Predicate_Observes_Supplied_Token() { + // The predicate and WaitsFor deliberately share one token. The Func source captures the + // predicate's OperationCanceledException as an evaluation exception, so it never escapes on + // its own; the OCE the caller sees comes from WaitsFor observing the same token. The test + // proves that a token-aware predicate does not turn cancellation into an AssertionException. + // With an independent token passed to WaitsFor, the loop would poll until its 30s timeout. using var cts = new CancellationTokenSource(); cts.CancelAfter(TimeSpan.FromMilliseconds(50));