Skip to content

Add two-stage Ctrl+C cancellation UX (#5345)#8581

Open
Evangelink wants to merge 2 commits into
mainfrom
dev/amauryleve/two-stage-ctrl-c
Open

Add two-stage Ctrl+C cancellation UX (#5345)#8581
Evangelink wants to merge 2 commits into
mainfrom
dev/amauryleve/two-stage-ctrl-c

Conversation

@Evangelink
Copy link
Copy Markdown
Member

Implements two-stage Ctrl+C cancellation UX requested in #5345.

Behavior

  • First Ctrl+C — unchanged cooperative cancellation; the test session is asked to stop gracefully so cleanup (file creation, disposers, etc.) can still run. A new hint line is now printed right after Canceling the test session...:

    Press Ctrl+C again to force exit.

  • Second Ctrl+C — calls IEnvironment.Exit((int)ExitCode.TestSessionAborted) (exit code 3), forcing the process to terminate immediately if cooperative cancellation is taking too long.

Implementation notes

  • CTRLPlusCCancellationTokenSource now takes an optional IEnvironment (defaults to SystemEnvironment) and uses an Interlocked state machine (Idle → Cancelling → Forcing) so repeated presses are race-safe and idempotent.
  • The hint is appended in all three places that render the cancel banner: TerminalTestReporter.StartCancelling, SimplifiedConsoleOutputDeviceBase, and the JSON-discovery branch of TerminalOutputDevice (which goes to stderr to keep stdout JSON clean).
  • New localized string PressCtrlCAgainToForceExit added to PlatformResources.resx with all 13 XLF files regenerated via UpdateXlf.
  • No Forcing exit... confirmation message is printed on the second press to avoid corrupting --list-tests --output json stdout; the exit itself is the confirmation.

Tests

  • Added CTRLPlusCCancellationTokenSourceTests with 4 cases covering first/second press, external-cancellation-then-Ctrl+C, and idempotency.
  • Verified 132 cancellation/stop-policy/terminal-reporter tests still pass.

Fixes #5345.

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>
Copilot AI review requested due to automatic review settings May 25, 2026 21:09
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 CTRLPlusCCancellationTokenSource and added optional IEnvironment injection 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

Copy link
Copy Markdown
Member Author

@Evangelink Evangelink left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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), IsCancellationRequested becomes true but _state stays StateIdle. 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 _state transitions before IsCancellationRequested to 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() throws AggregateException), concurrent first Ctrl+C presses, and console: null constructor 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 with state="new"
  • Security — No exploitable paths, appropriate error logging for developer tool
  • Cross-TFM CompatibilityInterlocked operations, nullable syntax, and OperatingSystem guards 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 ManagementIEnvironment doesn't need disposal, existing CancellationTokenSource correctly disposed
  • Defensive Coding — Event handler crash-proof, proper try/catch on Cancel()
  • 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>
private readonly IEnvironment _environment;
private readonly ILogger? _logger;
private readonly IConsole? _subscribedConsole;
private int _state = StateIdle;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CTRL+C does not complete the test run when there is long running test

2 participants