Repository navigation
fix(engine): report every test once on fail-fast runs - #7145
Conversation
When fail-fast stopped the unconstrained and keyed phases while both were running, the scheduler re-threw the failing test's own exception. The framework treated it as an unhandled error and reported the failure a second time as an "Unhandled exception" node. The phases now let the cancellation propagate, and the framework handles it as a fail-fast stop. Tests that a cancelled run stopped before they started got no result, so a run could report total: 1 with many tests selected. The scheduler now reports each test still waiting once the run winds down: skipped after fail-fast, cancelled after any other cancellation. The global [NotInParallel] loop stops on a cancelled token, and TestRunner no longer logs a cancelled lock or slot wait as "Unhandled exception in test", which made timeouts look like deadlocks. Fixes #7119
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. |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info
📝 Walkthrough
Merge Risk: ⚪ Minimal · up to
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7627555057
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
[Medium impact] The reviewed changes appear safe to merge. SummaryCompletes fail-fast reporting by including tests that failed before execution.
No new actionable issues were found in the changes since the previous review. No previous review threads were supplied. Review checks were source-only; no tests were run.
|
Review of #7145I reviewed the diff by reading it. I didn't build the PR or run its tests. The skill-based review tool failed to run, so this was a manual pass. Overall this is a good fix. It addresses each root cause directly:
Minor observations, none blocking:
LGTM. |
Tests that fail before execution (data source, argument registration or dependency resolution errors) already have State == Failed, so the cancelled-run sweep skipped them, and they vanished from the run when fail-fast stopped them before TestCoordinator published the failure. TestRunner now flags a test once it is handed to TestCoordinator, and the sweep publishes the failure of any Failed test that never got there. Also documents why the sweep's state check needs no atomic transition and why only the cancelled branch marks a session failure.
|
How I handled the review feedback:
CI: |
Review of #7145I read the diff but did not build the PR or run its tests. I found no blocking issues. What looks good
Suggestions (non-blocking)
Overall this is a clean root-cause fix with good test coverage. Approving in spirit. |
|
Dispositions for the latest claude-review comment (on 136e062):
No code changes, so no new commit. |
Updated [TUnit](https://github.com/thomhurst/TUnit) from 1.75.0 to 1.76.28. <details> <summary>Release notes</summary> _Sourced from [TUnit's releases](https://github.com/thomhurst/TUnit/releases)._ ## 1.76.28 <!-- Release notes generated using configuration in .github/release.yml at v1.76.28 --> ## What's Changed ### Other Changes * Remove legacy JSON output implementation by @Saad-elnagar in thomhurst/TUnit#7234 * fix(engine): drop row-scoped data sources from failed test metadata by @kenny-ish in thomhurst/TUnit#7079 * Merge the analyzer release tables under one New Rules section (RS2007) by @bfs2021 in thomhurst/TUnit#7174 * fix: release finished test instances before session end by @thomhurst in thomhurst/TUnit#7262 * fix: never hand out a disposed shared data-source instance by @thomhurst in thomhurst/TUnit#7248 ### Dependencies * chore(deps): update all non-major dependencies to 1.76.0 by @thomhurst in thomhurst/TUnit#7233 * chore(deps): bump follow-redirects from 1.16.0 to 1.16.1 in /docs by @dependabot[bot] in thomhurst/TUnit#7232 * chore(deps): bump launch-editor from 2.14.1 to 2.14.2 in /docs by @dependabot[bot] in thomhurst/TUnit#7231 * chore(deps): update all non-major dependencies to 1.20.1 by @thomhurst in thomhurst/TUnit#7235 * chore(deps): update dependency awssdk.sqs to 4.0.100.20 by @thomhurst in thomhurst/TUnit#7236 * chore(deps): update dependency microsoft.playwright to 1.64.0 by @thomhurst in thomhurst/TUnit#7237 * chore(deps): update all non-major dependencies by @thomhurst in thomhurst/TUnit#7238 * chore(deps): update dependency path-to-regexp to v9 by @thomhurst in thomhurst/TUnit#7241 * chore(deps): update all non-major dependencies by @thomhurst in thomhurst/TUnit#7242 * chore(deps): update dependency serialize-javascript to v7.1.3 by @thomhurst in thomhurst/TUnit#7243 * chore(deps): update dependency imposter to 1.0.4 by @thomhurst in thomhurst/TUnit#7244 * chore(deps): update dependency stackexchange.redis to 3.4.20 by @thomhurst in thomhurst/TUnit#7245 * chore(deps): update dependency serialize-javascript to v7.1.4 by @thomhurst in thomhurst/TUnit#7246 * chore(deps): update dependency imposter to 1.0.5 by @thomhurst in thomhurst/TUnit#7247 * chore(deps): update all non-major dependencies to 13.6.4 by @thomhurst in thomhurst/TUnit#7251 * chore(deps): update dependency imposter to 1.0.6 by @thomhurst in thomhurst/TUnit#7258 * chore(deps): bump fast-uri from 3.1.8 to 3.1.9 in /docs by @dependabot[bot] in thomhurst/TUnit#7253 * chore(deps): bump body-parser from 1.20.6 to 1.20.9 in /docs by @dependabot[bot] in thomhurst/TUnit#7252 ## New Contributors * @Saad-elnagar made their first contribution in thomhurst/TUnit#7234 * @kenny-ish made their first contribution in thomhurst/TUnit#7079 * @bfs2021 made their first contribution in thomhurst/TUnit#7174 **Full Changelog**: thomhurst/TUnit@v1.76.0...v1.76.28 ## 1.76.0 <!-- Release notes generated using configuration in .github/release.yml at v1.76.0 --> ## What's Changed ### Other Changes * chore: add OSS scanner config, threat model and SECURITY.md by @thomhurst in thomhurst/TUnit#7080 * fix(deps): patch MessagePack for Aspire and use per-TFM Microsoft.Extensions.Http by @Quattordici in thomhurst/TUnit#7072 * fix: skip tests marked with an empty skip reason by @thomhurst in thomhurst/TUnit#7083 * fix(engine): report the engine mode actually selected in the banner by @thomhurst in thomhurst/TUnit#7085 * fix(engine): order keyed NotInParallel tests by priority, not hash code by @thomhurst in thomhurst/TUnit#7090 * fix: remove root causes of flaky tests and the broken Apple smoke jobs by @thomhurst in thomhurst/TUnit#7095 * fix(engine): run synchronous keyed NotInParallel tests concurrently by @thomhurst in thomhurst/TUnit#7087 * fix(sourcegen): run every [Arguments] row that infers the same generic type by @thomhurst in thomhurst/TUnit#7100 * fix: same filter path and display name for generic tests in both engine modes by @thomhurst in thomhurst/TUnit#7110 * fix(engine): apply max parallelism cap to keyed NotInParallel tests by @thomhurst in thomhurst/TUnit#7084 * fix(ci): install only Chromium for Playwright and wait on dpkg lock by @thomhurst in thomhurst/TUnit#6994 * fix(core): keep the engine-selected execution mode when late assemblies load by @thomhurst in thomhurst/TUnit#7091 * fix(engine): end fail-fast runs without crashing the test host by @thomhurst in thomhurst/TUnit#7115 * test: stop the AOT [EngineTest=Pass] run hitting its 5 minute budget by @thomhurst in thomhurst/TUnit#7125 * fix(engine): align reflection-mode data sources and hooks with source-gen by @thomhurst in thomhurst/TUnit#7094 * fix(pipeline): stop skipping the NuGet package tester modules by @thomhurst in thomhurst/TUnit#7127 * ci: take AOT publishes and doc snippets off the Ubuntu critical path by @thomhurst in thomhurst/TUnit#7128 * fix(analyzers): TUnit0001 false positive for MethodDataSource on generic type-parameter parameters by @thomhurst in thomhurst/TUnit#7109 * fix: report generic class constraint violations as failed tests in both modes by @thomhurst in thomhurst/TUnit#7112 * ci: cut runner contention and the AOT publish off the critical path by @thomhurst in thomhurst/TUnit#7131 * fix(assertions): make cross-type IsEqualTo work for numeric and nullable arguments on C# 12 by @thomhurst in thomhurst/TUnit#7086 * fix(engine): substitute class type arguments and array element types in build-time constraint checks by @thomhurst in thomhurst/TUnit#7102 * ci: test the NuGet packages on Windows in their own job by @thomhurst in thomhurst/TUnit#7130 * fix(sourcegen): bind params T[] on generic test methods by @thomhurst in thomhurst/TUnit#7101 * fix(engine): run every [Arguments] row on generic test classes in both modes by @thomhurst in thomhurst/TUnit#7111 * fix(engine): use last inferred type for repeated generic parameters by @theluckystrike in thomhurst/TUnit#7134 * Make the test suite pass in every environment and run fully in CI by @Quattordici in thomhurst/TUnit#7129 * fix(assertions): skip using directive for global-namespace assertion types by @thomhurst in thomhurst/TUnit#7139 * fix(core): format numeric display-name arguments with the invariant culture by @thomhurst in thomhurst/TUnit#7138 * fix(engine): dispose data-source objects of rows that end as failed tests by @thomhurst in thomhurst/TUnit#7150 * Fix net472 compile gate failure in assertions source generator tests by @thomhurst in thomhurst/TUnit#7170 * fix(sourcegen): close generic methods of generic classes on every class source by @thomhurst in thomhurst/TUnit#7152 * fix(engine): infer object for null rows on generic test methods in reflection mode by @thomhurst in thomhurst/TUnit#7142 * fix(engine): report every test once on fail-fast runs by @thomhurst in thomhurst/TUnit#7145 * test(engine): stop whole-process budgets killing healthy scheduling engine tests by @thomhurst in thomhurst/TUnit#7177 * feat(analyzers): report MethodDataSource Arguments that fit two overloads equally (TUnit0076) by @thomhurst in thomhurst/TUnit#7181 * fix(sourcegen): render top-level dynamic as object so typeof compiles by @thomhurst in thomhurst/TUnit#7182 * fix(sourcegen): pair each GenerateGenericTest type only with its typed ClassDataSource<T> by @thomhurst in thomhurst/TUnit#7188 * fix(engine): run untyped [Arguments(null)] rows on generic classes instead of dropping them by @thomhurst in thomhurst/TUnit#7203 * fix(engine): report generic test classes by their generic type definition in TRX by @thomhurst in thomhurst/TUnit#7200 * test(sourcegen): parse snapshot fixtures as a real build does by @thomhurst in thomhurst/TUnit#7189 * refactor(engine): dedupe reflection generic discovery by @thomhurst in thomhurst/TUnit#7183 * test: make TimeoutHelperTests deterministic under thread-pool load by @thomhurst in thomhurst/TUnit#7180 * fix(analyzers): accept null MethodDataSource arguments for nullable parameters by @thomhurst in thomhurst/TUnit#7179 * test(assertions): make WaitsFor cancellation tests robust to slow CI runners by @thomhurst in thomhurst/TUnit#7176 * fix(assertions): support [AssertionFrom] with unbound generic types by @thomhurst in thomhurst/TUnit#7144 ... (truncated) Commits viewable in [compare view](thomhurst/TUnit@v1.75.0...v1.76.28). </details> [](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) --- <details> <summary>Dependabot commands and options</summary> <br /> You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) </details> Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Summary
Fixes both reporting problems on
--fail-fastruns from #7119, and the related timeout noise from the issue comment.Before (
FailFastReporting7119/* --fail-fast --maximum-parallel-tests 1, both modes):After:
Root cause
TestScheduler.WaitForTasksWithFailFastHandlingcaught the cancellation and re-threw the failing test's own exception (GetFirstFailFastException). That exception is not a cancellation, soTUnitTestFrameworksent it to its generic handler, which publishes an "Unhandled exception" test node. The failing test already had its own node. With only one phase running, the re-throw was skipped, which is why the issue needed two phases.[NotInParallel]lock, or a phase that never ran) never reachedTestCoordinator, which is where results are published. Nothing else reported them.[NotInParallel]loop kept walking every remaining test into a cancelled lock wait, andTestRunnerlogged each one as "Unhandled exception in test ...", so a plain timeout looked like a lock deadlock.Fix
TestScheduler.RunPhasesConcurrentlyAsyncis now plainTask.WhenAll. The cancellation propagates, and the framework's existing fail-fast branch logs it and fails the session (exit code 10, as before --fail-fast: cancelled tests are not reported, and a failure can appear twice as an "Unhandled exception" node #7119). A real non-cancellation fault from a phase is still re-thrown unchanged.TestScheduler.ScheduleAndExecuteAsync: on a cancelled run, before the After(TestSession) hooks, every test still inNotStarted,WaitingForDependenciesorQueuedgets a result. That covers scheduled tests, dynamic tests the queue processor scheduled, and dynamic tests left in the queue. After fail-fast these tests are skipped, with a reason that names--fail-fast, so the failure count stays at the real failures. After any other cancellation they are cancelled. Tests that started already report their own result and are not touched. A test that failed before it ran (data source, argument registration or dependency resolution error) and never reachedTestCoordinatorgets its failure published, so it does not disappear from the results. Reporting is best effort and never replaces the run's outcome.[NotInParallel]loop stops as soon as the token is cancelled.TestRunnertreats anOperationCanceledExceptionon a cancelled token as "not started": it logs at debug level and does not trigger fail-fast again.FailFastsettings table says tests that had not started are reported as skipped.No public API change:
GetFirstFailFastExceptionis kept and still populated, and the newTestRunner.IsFailFastTriggeredis internal.Note: when the platform's own token is cancelled (for example
--timeoutor IDE stop), MTP aborts the session and drops later test-node messages, so those runs still showAbortedwith no per-test nodes. The sweep is a no-op there. The[NotInParallel]"Unhandled exception" log spam is gone in that case too.Tests
FailFastReporting7119scenario inTUnit.TestProject: a Critical-priority failing test, plus unconstrained, keyed and global[NotInParallel]tests, andDataSourceFails, whose data source throws. Both modes now report 8 tests: 2 failed, 6 skipped. Without the pre-failed fix,DataSourceFailswas missing (7 tests, 1 failed).FailFastReportingTests.FailFast_ReportsEveryTestOnce_AndCancelledTestsAsSkipped(--fail-fast --maximum-parallel-tests 1). It asserts there is no "Unhandled exception" node, that all 8 tests have exactly one result, that onlyFailsandDataSourceFailsfailed, that the others are Passed or NotExecuted (at least 4 NotExecuted), and that both global tests are NotExecuted. It failed before the fix (found the extra node, and total was 2) and passes after it.Runs (net10.0, Release, Windows):
FailFastReportingTestsandKeyedNotInParallelFailFastTests: each passed 5 times in a row (Reflection mode; AOT engine tests only run in CI).FailFastReporting7119(above),KeyedNotInParallelFailFastDynamic7074(total 5: 1 failed, 2 passed, 2 skipped, exit 10) andFailFastTest(total 4: 1 failed, 2 passed, 1 skipped, exit 10). None had an "Unhandled exception" node.TestExecutionCancellationTests,CancellationAfterHooksTestsandTestExecutorCancellationTokenTestspassed.ExternalCancellationTests,CanCancelTestsandSigtermTestsare excluded on Windows, so CI on Linux covers them.TUnit.UnitTests: 566 passed, 4 skipped, 0 failed.Fixes #7119
Summary by CodeRabbit
Bug Fixes
Documentation