Add two-stage Ctrl+C cancellation UX (#5345)#8581
Conversation
The first Ctrl+C still triggers cooperative cancellation, but now prints a hint to the user that pressing Ctrl+C again will force-exit. The second press calls IEnvironment.Exit with TestSessionAborted (3). State transitions use Interlocked to be robust across concurrent presses. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Adds a two-stage Ctrl+C cancellation experience to Microsoft.Testing.Platform: first press requests cooperative cancellation (with a new “Press Ctrl+C again to force exit.” hint), second press force-exits the process with ExitCode.TestSessionAborted.
Changes:
- Implemented an interlocked state machine in
CTRLPlusCCancellationTokenSourceand added optionalIEnvironmentinjection to support force-exit on the second Ctrl+C. - Appended the new hint line to all cancellation-banner renderers (terminal, simplified console, and JSON discovery branch via stderr).
- Added a new localized resource string and corresponding unit tests.
Show a summary per file
| File | Description |
|---|---|
| test/UnitTests/Microsoft.Testing.Platform.UnitTests/Services/CTRLPlusCCancellationTokenSourceTests.cs | Adds unit coverage for first/second Ctrl+C behavior, external cancellation + Ctrl+C, and idempotency. |
| src/Platform/Microsoft.Testing.Platform/Services/CTRLPlusCCancellationTokenSource.cs | Adds two-stage Ctrl+C handling (cooperative cancel then forced exit) and environment abstraction for exit. |
| src/Platform/Microsoft.Testing.Platform/Resources/PlatformResources.resx | Adds PressCtrlCAgainToForceExit resource string. |
| src/Platform/Microsoft.Testing.Platform/OutputDevice/TerminalOutputDevice.cs | Ensures cancellation+hint are written to stderr for JSON discovery mode. |
| src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.cs | Adds the hint line to the terminal cancellation banner. |
| src/Platform/Microsoft.Testing.Platform/OutputDevice/SimplifiedConsoleOutputDeviceBase.cs | Adds the hint line to simplified console cancellation output. |
| src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.cs.xlf | Regenerates localization entry for the new hint string. |
| src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.de.xlf | Regenerates localization entry for the new hint string. |
| src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.es.xlf | Regenerates localization entry for the new hint string. |
| src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.fr.xlf | Regenerates localization entry for the new hint string. |
| src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.it.xlf | Regenerates localization entry for the new hint string. |
| src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ja.xlf | Regenerates localization entry for the new hint string. |
| src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ko.xlf | Regenerates localization entry for the new hint string. |
| src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.pl.xlf | Regenerates localization entry for the new hint string. |
| src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.pt-BR.xlf | Regenerates localization entry for the new hint string. |
| src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ru.xlf | Regenerates localization entry for the new hint string. |
| src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.tr.xlf | Regenerates localization entry for the new hint string. |
| src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.zh-Hans.xlf | Regenerates localization entry for the new hint string. |
| src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.zh-Hant.xlf | Regenerates localization entry for the new hint string. |
Copilot's findings
- Files reviewed: 19/19 changed files
- Comments generated: 1
Evangelink
left a comment
There was a problem hiding this comment.
Expert Review Summary
I've conducted a comprehensive 21-dimension review of this PR implementing two-stage Ctrl+C cancellation UX.
| # | Dimension | Verdict |
|---|---|---|
| 2 | Threading & Concurrency | 🔴 1 BLOCKING |
| 1 | Algorithmic Correctness | 🔴 1 BLOCKING (same root cause) |
| 13 | Test Completeness & Coverage | 🟡 1 MAJOR |
✅ 18/21 dimensions clean — remaining dimensions had no findings.
Critical Issues (Must Fix Before Merge)
-
Race condition in state machine — When
Cancel()is called externally (timeout),IsCancellationRequestedbecomestruebut_statestaysStateIdle. The next user Ctrl+C sees cancellation already requested and jumps to force-exit logic (line 58–72), treating the first user Ctrl+C as the second. This violates the advertised UX: "Press Ctrl+C again to force exit."Fix: Check
_statetransitions beforeIsCancellationRequestedto make Ctrl+C counting independent of external cancellation sources. Detailed recommendation in inline comment on line 72. -
Missing test coverage for exception handling path (
Cancel()throwsAggregateException), concurrent first Ctrl+C presses, andconsole: nullconstructor boundary. See inline comment on test file line 68.
Clean Dimensions (LGTM)
The following dimensions had no findings:
- Localization — Resource strings properly added to
.resx, XLF files correctly regenerated withstate="new" - Security — No exploitable paths, appropriate error logging for developer tool
- Cross-TFM Compatibility —
Interlockedoperations, nullable syntax, andOperatingSystemguards work on all TFMs (net462, netstandard2.0, net8.0, net9.0) - Performance — Not a hot path (user-triggered event at most twice per session), appropriately lightweight
- Resource Management —
IEnvironmentdoesn't need disposal, existingCancellationTokenSourcecorrectly disposed - Defensive Coding — Event handler crash-proof, proper
try/catchonCancel() - Code Structure — Clean control flow with early returns, appropriate nesting
- Test Isolation — Per-test mocks, no shared state
- Assertion Quality — Correct assertion methods, helpful messages
- Flakiness Patterns — Synchronous tests, no timing dependencies
- Documentation — Comments explain "why" not "what", accurate
- Public API — Changes are
internal, no public API impact - Scope Discipline — Focused on single feature (two-stage Ctrl+C), proper issue tracking (#5345)
Overall Assessment
Strong implementation with excellent test coverage and documentation. The state machine logic needs adjustment to handle the race between external cancellation and user Ctrl+C correctly. Once the threading issue and test gaps are addressed, this will be ready to merge.
Requested Changes: Fix the state machine race condition and add missing test coverage per inline comments.
Generated by Expert Code Review (on open) for issue #8581 · ● 4.7M
…n Dispose, add coverage - Decouple Ctrl+C counting from IsCancellationRequested so external Cancel() (timeout, max-failed-tests, etc.) no longer turns the first user Ctrl+C into a force-exit. The user must always press Ctrl+C twice to force-exit, matching the advertised 'Press Ctrl+C again to force exit.' contract. - Store the subscribed IConsole and unsubscribe CancelKeyPress in Dispose() to prevent the handler from running on a disposed CancellationTokenSource. - Add tests for: AggregateException-from-Cancel logging, concurrent first Ctrl+C contention, null-console boundary, and Dispose unsubscription. - Update the 'external cancel first' test to assert the new (correct) behavior: first user Ctrl+C is cooperative, second forces exit. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Implements two-stage Ctrl+C cancellation UX requested in #5345.
Behavior
Canceling the test session...:IEnvironment.Exit((int)ExitCode.TestSessionAborted)(exit code3), forcing the process to terminate immediately if cooperative cancellation is taking too long.Implementation notes
CTRLPlusCCancellationTokenSourcenow takes an optionalIEnvironment(defaults toSystemEnvironment) and uses anInterlockedstate machine (Idle → Cancelling → Forcing) so repeated presses are race-safe and idempotent.TerminalTestReporter.StartCancelling,SimplifiedConsoleOutputDeviceBase, and the JSON-discovery branch ofTerminalOutputDevice(which goes to stderr to keep stdout JSON clean).PressCtrlCAgainToForceExitadded toPlatformResources.resxwith all 13 XLF files regenerated viaUpdateXlf.Forcing exit...confirmation message is printed on the second press to avoid corrupting--list-tests --output jsonstdout; the exit itself is the confirmation.Tests
CTRLPlusCCancellationTokenSourceTestswith 4 cases covering first/second press, external-cancellation-then-Ctrl+C, and idempotency.Fixes #5345.