Repository navigation
fix(engine): end fail-fast runs without crashing the test host - #7115
Conversation
Fail-fast cancels a linked token that Microsoft.Testing.Platform does not own, so the OperationCanceledException the scheduler surfaces was re-thrown to MTP and crashed the host (0xE0434352 on Windows). TUnitTestFramework now fails the session instead, as it already does for SIGTERM, and only re-throws when the platform's own token fired. The dynamic test queue processor also ends its wait cleanly on cancellation. When the failing test is the last to run the phases finish without throwing, and the scheduler then surfaced the processor's cancelled WaitToReadAsync as its own exception. The keyed fail-fast test now queues its tests on one shared key, so Fails deterministically runs first; with distinct keys the tests raced for the single slot and could all pass first (the AOT failure on #7084). Refs #7074, #7084
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
Warning Review limit reachedYou've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Next included review available in 13 minutes. View limit details
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info
📝 Walkthrough
Merge Risk: ⚪ Minimal · up to
|
ReviewThe three fixes match the root causes described in the PR, and I found no blocking issues.
Optional suggestions:
Looks good to merge. |
[High impact] The PR appears safe to merge; the previous test-coverage gap is addressed. SummaryThe PR ends fail-fast runs without crashing the host and waits for dynamic tests already running.
Reviews (2) · Last reviewed commit: "test(engine): require exit code 2 when f..." · Reviewed by Greptile |
…last The framework's fail-fast catch turns a leaked OperationCanceledException into exit code 10, so accepting 10 let the test pass without the dynamic-queue fix in TestScheduler.
|
Review: No blocking issues. The three fixes match the problems described.
Suggestions (non-blocking):
The new FailFast_WhenFailingTestRunsLast_EndsSessionWithoutCrash test covers the previously untested path, and the repeat runs (40/40, 15/15) give good confidence. Looks good to merge. |
Follow-up to #7084 (for #7074). The last CI run of #7084 failed in AOT mode on
KeyedNotInParallelFailFastTests.FailFast_WithWaitingKeyedTests_AwaitsSessionHooksBeforeFixtureDisposal. The output showed the test host crashing with an unhandledOperationCanceledExceptionfromProcessDynamicTestQueueAsync. Looking into it turned up three separate problems.What was wrong
1. Every fail-fast run crashes the test host. This predates #7084. Fail-fast cancels a linked token that Microsoft.Testing.Platform does not own. Whatever
OperationCanceledExceptionthe scheduler throws reachesTUnitTestFramework.ExecuteRequestAsync, which logs "Test execution stopped due to fail-fast." and then re-throws it. MTP only treats that exception as a cancellation when its own token fired, so the exception escapesMainand the process dies with exit code0xE0434352. #6962 already handles this for SIGTERM, but fail-fast takes the same path. I reproduced the crash on currentmainand on the commit before #7084 (faf43b6ecd) with plainFailFastTest --fail-fast. CI didn't catch it because the TRX file still gets written, so the engine tests' assertions passed.2. The dynamic test queue processor faults when the token is cancelled.
ProcessDynamicTestQueueAsyncwaits onWaitToReadAsync(cancellationToken). If the failing test is the last one to run, the phases finish without throwing.ExecuteGroupedTestsAsyncthen awaits the processor on its normal path, and the cancelled wait becomes the scheduler's exception. That code path was the same before #7084. #7084's newcatchonly covers the path where the phases throw. A single failing keyed test (KeyedNotInParallelFailFast7074/Fails --fail-fast) hits this every time.3. The test #7084 added was flaky. It gave each test its own key and relied on
ExecutionPriority(Critical)to makeFailstake the single slot first. With distinct keys, every test starts at once throughTask.Run, and the tests race for the slot. In the AOT run the fourWaitstests won the race and all passed (Passedwas 4, against an assertion that it be less than 4).Failsthen failed last, which led straight into problem 2.Fix
TUnitTestFramework: if a cancellation exception arrives and the platform's token has not fired, log it, mark the session failed and return. This mirrors the SIGTERM branch. The exception is still re-thrown when MTP's own token was cancelled.TestScheduler.ProcessDynamicTestQueueAsync: if the token is cancelled while the processor waits on the queue, the wait loop now ends instead of throwing. The processor also skips draining the queue once the token is cancelled. A dynamic batch that is already running is still awaited, so the fix(engine): apply max parallelism cap to keyed NotInParallel tests #7084 guarantee holds: session teardown waits for in-flight dynamic tests.KeyedNotInParallelFailFast7074: all tests now share one key. The scheduler hands out keys in priority order as it walks the tests, soFailsalways runs first. When it releases the key, fail-fast has already cancelled the token, so eachWaitstest throws on its cancelled slot wait.FailFast_WhenFailingTestRunsLast_EndsSessionWithoutCrash, covers problem 2.A fail-fast run now exits with code 10 (session failed) instead of crashing.
Testing
All runs used
--fail-fastagainst a net10.0 Debug build of TUnit.TestProject.KeyedNotInParallelFailFast7074/*with--maximum-parallel-tests 1KeyedNotInParallelFailFastDynamic7074/*with--maximum-parallel-tests 2KeyedNotInParallelFailFast7074/FailsFailFastTest/*Before the fix, every run I tried of the
KeyedNotInParallelFailFast7074/*,.../FailsandFailFastTest/*filters crashed.Other test runs:
KeyedNotInParallelFailFastTestsengine tests passed 3 times in a row, andKeyedNotInParallelMaxParallelismTestspassed (Reflection mode; AOT is CI-only).TUnit.UnitTestspassed (558 passed, 3 skipped).TUnit.Enginebuilds for all target frameworks with no warnings.AOT was not verified locally: this machine has no MSVC linker, so Native AOT publish isn't possible here. The CI AOT leg covers it.
Summary by CodeRabbit