From 8a798693c612d67b6f1d8fe604892cd7a3266a06 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Sun, 18 Oct 2026 15:32:05 +0100 Subject: [PATCH] test: stop the AOT [EngineTest=Pass] run hitting its 5 minute budget The ExpectedStateTests.Pass AOT run on Windows CI failed with what looked like a NotInParallelLock deadlock: every remaining global [NotInParallel] test logged a TaskCanceledException from EnterExclusiveAsync. It was not a deadlock. The AOT invocation passes --timeout 5m, a whole-session budget, and healthy runs take 3.5-5 minutes on Windows CI. When the budget ran out, the sequential global NotInParallel phase (which runs last) hit the cancelled token at the writer gate for each remaining test. The JIT run in the same job took 5m15s and passed because it has no session timeout. - Raise the AOT session budget to 10 minutes. - DedicatedThreadExecutorCleanUpOrderTests (#6885) took about 16 seconds per test: the executor also runs the project's ~50 global test hooks, each with the 300 ms CleanUp sleep, all on the serial NotInParallel path. Only the test body's CleanUp now sleeps, so the class takes about 4 seconds instead of 52 and the full Pass run drops from about 2.5-3 minutes to under 2 locally. This also makes the regression sharper, since the hooks no longer put seconds between consecutive test bodies. Co-Authored-By: Claude --- tests/TUnit.Engine.Tests/InvokableTestBase.cs | 5 ++++- tests/TUnit.TestProject/Bugs/6885/Tests.cs | 20 +++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/tests/TUnit.Engine.Tests/InvokableTestBase.cs b/tests/TUnit.Engine.Tests/InvokableTestBase.cs index f3da6e3255..b439d608c1 100644 --- a/tests/TUnit.Engine.Tests/InvokableTestBase.cs +++ b/tests/TUnit.Engine.Tests/InvokableTestBase.cs @@ -99,7 +99,10 @@ private async Task RunWithAot(string filter, List> assertions, "--report-trx", "--report-trx-filename", trxFilename, "--diagnostic-verbosity", "Debug", "--diagnostic", "--diagnostic-file-prefix", $"log_{GetType().Name}_AOT_", - "--timeout", "5m", + // A whole-session budget, not a hang detector. The [EngineTest=Pass] run takes + // 3.5-5 minutes on Windows CI, so a 5m budget cancelled healthy runs mid-way + // through the sequential global [NotInParallel] tail. + "--timeout", "10m", ..runOptions.AdditionalArguments ] ) diff --git a/tests/TUnit.TestProject/Bugs/6885/Tests.cs b/tests/TUnit.TestProject/Bugs/6885/Tests.cs index 8241d2e851..29e47403fe 100644 --- a/tests/TUnit.TestProject/Bugs/6885/Tests.cs +++ b/tests/TUnit.TestProject/Bugs/6885/Tests.cs @@ -24,6 +24,8 @@ public class DedicatedThreadExecutorCleanUpOrderTests private static async Task AssertNoOverlapAsync() { + SlowCleanUpExecutor.MarkTestBodyRunning(); + var cleanUpRunningAtStart = SlowCleanUpExecutor.CleanUpRunning; // Longer than CleanUp(), so a previous test's late CleanUp() would reset the state mid-test. @@ -40,15 +42,28 @@ private static async Task AssertNoOverlapAsync() /// /// Sets global state in Initialize() and resets it in a slow CleanUp(). /// +/// +/// The executor also runs every Before/After(Test) hook, including the project's many global +/// [BeforeEvery]/[AfterEvery] hooks, each on its own dedicated thread. Only the test body's +/// CleanUp() is slow: sleeping after every hook made each of these [NotInParallel] tests take +/// about 16 seconds, which pushed the [EngineTest=Pass] run past its time budget. +/// public sealed class SlowCleanUpExecutor : DedicatedThreadExecutor { private static volatile bool s_cleanUpRunning; private static volatile string s_state = "reset"; + // The test body runs on the executor's dedicated thread (its continuations are pumped there), + // so a thread-static flag tells CleanUp() whether this thread ran a test body or a hook. + [ThreadStatic] + private static bool t_ranTestBody; + public static bool CleanUpRunning => s_cleanUpRunning; public static string State => s_state; + public static void MarkTestBodyRunning() => t_ranTestBody = true; + protected override void Initialize() { s_state = "configured"; @@ -56,6 +71,11 @@ protected override void Initialize() protected override void CleanUp() { + if (!t_ranTestBody) + { + return; + } + s_cleanUpRunning = true; Thread.Sleep(300); s_state = "reset";