Skip to content

Add MTP server-mode cancellation E2E acceptance test - #10888

Open
Amaury Levé (Evangelink) wants to merge 10 commits into
microsoft:mainfrom
Evangelink:dev/amauryleve/mtp-cancellation-e2e
Open

Add MTP server-mode cancellation E2E acceptance test#10888
Amaury Levé (Evangelink) wants to merge 10 commits into
microsoft:mainfrom
Evangelink:dev/amauryleve/mtp-cancellation-e2e

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Why

Unit tests already prove that cancelling the source-only server-mode client's (Microsoft.Testing.Platform.ServerMode.Client.Sources) RunTestsAsync call emits a $/cancelRequest notification, but nothing proved the cancellation actually reaches a running MSTest test through TestContext.CancellationToken, or that the server/client shut down cleanly afterwards. This adds that missing end-to-end coverage.

What this does

MtpServerClientCancellationAcceptanceTests launches a real MSTest MTP application in server mode via MtpServerClient and:

  • Runs a deliberately long-running test (Task.Delay(5min, TestContext.CancellationToken)).
  • Waits for an in-progress test-node update before cancelling, so the cancel is never racing against test startup (no blind sleep).
  • Cancels the client's RunTestsAsync token, which sends $/cancelRequest.
  • Asserts the executing test itself observed TestContext.CancellationToken - not just that the client-side task was cancelled or that $/cancelRequest was sent - via a marker file the test method writes from inside a catch (OperationCanceledException ex) when (ex.CancellationToken == TestContext.CancellationToken) block.
  • Asserts RunTestsAsync, ExitAsync, and client Dispose all complete within bounded waits, so a regression here fails fast instead of hanging CI.

A note on the design (no product bug found)

My first attempt asserted on a final failed/"was canceled" test-node update sent back over the wire, mirroring the existing self-cancellation tests. That doesn't work for a hard $/cancelRequest cancel: AsyncConsumerDataProcessor.ConsumeAsync intentionally stops relaying further test-node updates once a run's cancellation token fires (its own comments describe this as letting "a cooperative consumer bail out of its own work immediately"). That's different from the cooperative IGracefulStopTestExecutionCapability path (exercised by DotnetTestPipeServerCancellationTests), which does guarantee reporting survives. Since this is intentional platform behavior rather than a propagation gap, I redesigned the test's "did the test observe cancellation" proof around an out-of-band marker file instead of relying on that channel.

Verification

  • dotnet build test\IntegrationTests\MSTest.Acceptance.IntegrationTests\MSTest.Acceptance.IntegrationTests.csproj -c Release -f net11.0 - 0 errors.
  • Ran the new test directly via the built host with --filter "Name~RunTestsAsync_WhenCanceledAfterTestStarts" - passed in ~2s, 5/5 repeated runs green.
  • Sanity check: temporarily removed TestContext.CancellationToken from the test asset's delay call to confirm the test fails loudly (clear TimeoutException) instead of passing vacuously; reverted after confirming.

Add an end-to-end acceptance test proving that cancelling
Microsoft.Testing.Platform.ServerMode.Client.Sources'' RunTestsAsync propagates
all the way into a real, executing MSTest test through
TestContext.CancellationToken, and that the server/client shut down cleanly
afterwards.
The test launches a real MSTest MTP app in server mode via MtpServerClient,
waits for an in-progress test-node update to confirm the test actually
started (avoiding a timing-only race), cancels the run, and asserts the test
itself observed TestContext.CancellationToken via a marker file written from
inside a catch block keyed on that exact token instance.
A marker file is used instead of asserting on a final test-node update over
the wire: AsyncConsumerDataProcessor.ConsumeAsync intentionally stops relaying
test-node updates once a run is hard-canceled via $/cancelRequest (as opposed
to the cooperative IGracefulStopTestExecutionCapability path), so the final
canceled-test node update is not guaranteed to reach the client. That is
expected platform behavior, not a propagation gap.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
CopilotAI balanced review requested due to automatic review settings August 31, 2026 11:49

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

Review tier: Balanced
Findings: 3 Medium severity

New issues introduced by this change (3)
SeverityFinding
Medium severitytest/​IntegrationTests/​MSTest.Acceptance.IntegrationTests/​MtpServerClientCancellationAcceptanceTests.cs — This unfiltered catch also consumes the OperationCanceledException raised when testTimeoutToken
Medium severitytest/​IntegrationTests/​MSTest.Acceptance.IntegrationTests/​MtpServerClientCancellationAcceptanceTests.cs — Because timeoutSource is linked to cancellationToken, this condition is true for both the local…
Medium severitytest/​IntegrationTests/​MSTest.Acceptance.IntegrationTests/​MtpServerClientCancellationAcceptanceTests.cs — This does not verify the ShutsDownCleanly behavior: ExitAsync only sends the notification, and…
What changed in this PR

Adds E2E coverage for MTP server-mode cancellation reaching an executing MSTest test.

Changes:

  • Launches a real server-mode MSTest application.
  • Verifies cancellation through an out-of-band marker file.
  • Adds bounded waits and shutdown handling.
FileDescription
MtpServerClientCancellationAcceptanceTests.csAdds the cancellation acceptance test and generated test asset.

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

- Filter the catch around runTask.WaitAsync on runCancellation.Token so a
harness-level testTimeoutToken cancellation (a real hang) is never
misreported as the expected client-side run cancellation.
- Fix WaitForFileAsync''s catch filter to distinguish the local timeout from
outer-token cancellation, matching the existing pattern in
AcceptanceTestBase.RunWindowsApplicationModelCommandAsync.
- Add WaitForProcessExitAsync to actually verify the server process exits on
its own after ExitAsync, instead of relying on the using block''s Dispose(),
which force-kills the process if it is still alive and would let a server
that ignores ''exit'' still pass.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings August 31, 2026 12:09

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

Review tier: Balanced
Findings: None

Issues resolved since last review (3)
SeverityFinding
Medium severitytest/​IntegrationTests/​MSTest.Acceptance.IntegrationTests/​MtpServerClientCancellationAcceptanceTests.cs — This does not verify the ShutsDownCleanly behavior: ExitAsync only sends the notification, and… View resolved comment
Medium severitytest/​IntegrationTests/​MSTest.Acceptance.IntegrationTests/​MtpServerClientCancellationAcceptanceTests.cs — Because timeoutSource is linked to cancellationToken, this condition is true for both the local… View resolved comment
Medium severitytest/​IntegrationTests/​MSTest.Acceptance.IntegrationTests/​MtpServerClientCancellationAcceptanceTests.cs — This unfiltered catch also consumes the OperationCanceledException raised when testTimeoutTokenView resolved comment
Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

test/IntegrationTests/MSTest.Acceptance.IntegrationTests/MtpServerClientCancellationAcceptanceTests.cs:258

  • File.WriteAllText creates the file before its buffered contents are flushed and closed, while the parent stops waiting as soon as File.Exists is true. The parent can therefore read an empty/partial marker and fail intermittently even though cancellation propagated. Publish the marker atomically by writing a sibling temporary file and renaming it.
 File.WriteAllText(signalFilePath, $"{nameof(LongRunningCancellableTest)} observed TestContext.CancellationToken");

The parent acceptance test polls for the marker file via File.Exists, but
File.WriteAllText makes the file visible before its contents are fully
flushed, so the parent could observe an empty or partial marker under rare
timing. Write to a sibling .tmp file and atomically rename it into place
instead. Also clean up a stray .tmp file in the acceptance test''s cleanup.
Addresses a "previously missed" finding from Copilot code review.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings August 31, 2026 12:26

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

Review tier: Balanced
Findings: None

@EvangelinkAmaury Levé (Evangelink) added the state/needs-review Awaiting review from the team. label Aug 31, 2026
CI build failed on all platforms with IDE0007 (use 'var' instead of
explicit type) because the repo's .editorconfig treats
csharp_style_var_when_type_is_apparent as an error-level rule and
CancellationTokenSource.CreateLinkedTokenSource(...) is a recognized
'apparent type' factory call. Switch both local declarations to 'var'.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings August 31, 2026 12:56
@github-actions

Copy link
Copy Markdown
Contributor

🔍 Build Failure Analysis

Summary — All four Debug/Release build legs (Linux, macOS, Windows Release, Windows Debug) failed identically with the IDE0007 analyzer promoted to error ("use var instead of explicit type") at two lines in the new test file.

Root cause: IDE0007 — explicit type used where var is required

MtpServerClientCancellationAcceptanceTests.cs declares two local using variables with an explicit CancellationTokenSource type instead of var. The repo's .editorconfig enforces csharp_style_var_when_type_is_apparent/csharp_style_var_for_built_in_types as error severity, so both instances fail every leg's build with IDE0007.

Affected files / errors

Proposed fix (inline suggestions posted on both lines)

- using CancellationTokenSource timeoutSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);+ using var timeoutSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);

This matches the style already used a few lines above in the same file (using var client = MtpServerClient.Launch(...)).


Build overview
  • 4 legs analyzed with build errors: Linux Release, macOS Release, Windows Release, Windows Debug — all identical IDE0007 x2 + "Build failed."
  • 2 legs (application-model acceptance x2) reported no compile errors — unaffected/downstream of these two failing legs.
  • Failing project: test/IntegrationTests/MSTest.Acceptance.IntegrationTests/MSTest.Acceptance.IntegrationTests.csproj, target CoreCompile, task Csc.
All MSBuild errors
CodeProjectFile:LineMessage
IDE0007MSTest.Acceptance.IntegrationTestsMtpServerClientCancellationAcceptanceTests.cs:142use 'var' instead of explicit type
IDE0007MSTest.Acceptance.IntegrationTestsMtpServerClientCancellationAcceptanceTests.cs:159use 'var' instead of explicit type

🤖 Generated by the Build Failure Analysis workflow using (a href="(dev.azure.com/redacted) · commit 6fead25

🤖 Automated content by GitHub Copilot. Generated by the Build Failure Analysis workflow. · auto · 96.5 AIC · ⌖ 1.98 AIC · ⊞ 13.3K · [◷]( · )

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 Automated content by GitHub Copilot. Generated by the Build Failure Analysis workflow. · auto · 96.5 AIC · ⌖ 1.98 AIC · ⊞ 13.3K ·

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

Review tier: Balanced
Findings: None

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

test/IntegrationTests/MSTest.Acceptance.IntegrationTests/MtpServerClientCancellationAcceptanceTests.cs:87

  • The in-progress update does not prove that the test method has started. MSTest publishes it before calling RunSingleTestAsync (TestExecutionManager.Runner.cs:113,141), and that call still creates contexts/runs initializers before invoking the method (UnitTestRunner.RunSingleTest.cs:132-238). Cancellation can therefore happen first; the method can later enter with an already-canceled TestContext.CancellationToken, write the marker, and let this test pass without proving cancellation reached an executing test. Have the asset emit a separate “body started” signal immediately before Task.Delay, wait for that signal here, and only then cancel.
 // Wait until the server confirms the test is actually running before canceling. This avoids a
// timing-only race between "cancel" and "test has started" that a blind Task.Delay would risk.
await testStarted.Task.WaitAsync(WaitTimeout, testTimeoutToken);

The in-progress test-node update is published by MtpTestResultRecorder
before RunSingleTestAsync even runs TestInitialize/constructors, let alone
invokes the test method. Waiting only on that update to gate cancellation
left a window where cancellation could land before the test method body -
and its await Task.Delay(..., TestContext.CancellationToken) - had even
started, letting the test pass without proving cancellation reached
genuinely executing test code.
Have the test asset write a separate "body started" marker file
immediately as the first statement of the test method, before entering the
try/await Task.Delay. The acceptance test now waits for both that signal
and the in-progress node update before cancelling, closing the gap. Shared
the atomic write-then-rename helper between both marker files.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings August 31, 2026 13:16

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

Review tier: Balanced
Findings: None

Suppressed comments (1)

test/IntegrationTests/MSTest.Acceptance.IntegrationTests/MtpServerClientCancellationAcceptanceTests.cs:141

  • ProcessId == 0 proves only that the child terminated; MtpServerProcess.ProcessId returns 0 for every exit, including a crash or non-zero exit. Since ExitAsync only writes a notification, that write can also complete before the client detects that the peer has already died. The test could therefore pass when cancellation crashes the server after writing the marker. Capture a System.Diagnostics.Process handle from the initial PID before cancellation, await its exit after ExitAsync, and assert that ExitCode is zero.
 await client.ExitAsync(testTimeoutToken).WaitAsync(WaitTimeout, testTimeoutToken);
await WaitForProcessExitAsync(client, WaitTimeout, testTimeoutToken);

MtpServerProcess.ProcessId returns 0 for BOTH a graceful exit and a crash,
so polling it after ExitAsync could not distinguish "the server processed
exit cleanly" from "the server crashed right after writing the marker
file" - ExitAsync only writes a fire-and-forget notification, so its
completion does not prove the peer processed it before dying.
Capture a System.Diagnostics.Process handle for the launched server''s PID
immediately after Launch (while it is guaranteed alive), await its exit via
Process.WaitForExitAsync after sending ''exit'', and assert ExitCode == 0.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings August 31, 2026 13:45

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

Review tier: Balanced
Findings: None

Avoid Process.GetProcessById when asserting the server's exit code in the cancellation acceptance test. Linux does not treat that Process instance as the owner of the launched child, so ExitCode throws even after the server exits. Reuse the launched process owned by MtpServerClient instead.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings August 31, 2026 14:49

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

Review tier: Balanced
Findings: 1 High severity

New issues introduced by this change (1)
SeverityFinding
High severitysrc/​Platform/​Microsoft.Testing.Platform.ServerMode.Client.Sources/​Client/​MtpServerProcess.cs — This does not compile for the project's netstandard2.0 target:…

Microsoft.Testing.Platform.ServerMode.Client.Sources is a source-only
package that also targets netstandard2.0 for .NET Framework consumers.
ExitCode/WaitForExitAsync (added in 32f1ae4 to let the acceptance test
assert a clean server shutdown) called Process.WaitForExitAsync, a .NET
5+-only BCL method, which failed the netstandard2.0 build leg with
IDE0007/CS-level errors on Linux CI (flagged by review).
Adding the repo''s existing (but currently unpackaged) ProcessExtensions
polyfill to fix this down-level build would expand this PR''s scope beyond
a test-only change and touch the packaging of a shared, shipped library.
Per review guidance, remove the new accessors instead and keep the
acceptance test''s existing ProcessId-based polling approach (proves the
server exited naturally in response to ''exit'' without requiring
ownership-restricted ExitCode access), with a comment documenting why the
test does not assert on exit code specifically.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings August 31, 2026 15:11

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

Review tier: Balanced
Findings: 1 High severity

New issues introduced by this change (1)
SeverityFinding
High severitytest/​IntegrationTests/​MSTest.Acceptance.IntegrationTests/​MtpServerClientCancellationAcceptanceTests.csInitializeAsync is the only server/client operation here without the 60-second bound. If a…
Issues resolved since last review (1)
SeverityFinding
High severitysrc/​Platform/​Microsoft.Testing.Platform.ServerMode.Client.Sources/​Client/​MtpServerProcess.cs — This does not compile for the project's netstandard2.0 target:… View resolved comment

CopilotAI review requested due to automatic review settings September 1, 2026 07:47

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

Review tier: Balanced
Findings: 1 Medium severity

New issues introduced by this change (1)
SeverityFinding
Medium severitytest/​IntegrationTests/​MSTest.Acceptance.IntegrationTests/​MtpServerClientCancellationAcceptanceTests.csusing var invokes synchronous Dispose() only when this scope exits, without any deadline. This…
Issues resolved since last review (1)
SeverityFinding
High severitytest/​IntegrationTests/​MSTest.Acceptance.IntegrationTests/​MtpServerClientCancellationAcceptanceTests.csInitializeAsync is the only server/client operation here without the 60-second bound. If a… View resolved comment

Prevent Windows Release CI from hanging indefinitely by bounding client initialization and moving synchronous disposal to a bounded asynchronous cleanup path.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 2e0816a8-bfba-488c-abb0-1517c2b4cc7b
CopilotAI review requested due to automatic review settings September 1, 2026 13:15

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

Review tier: Balanced
Findings: None

Issues resolved since last review (1)
SeverityFinding
Medium severitytest/​IntegrationTests/​MSTest.Acceptance.IntegrationTests/​MtpServerClientCancellationAcceptanceTests.csusing var invokes synchronous Dispose() only when this scope exits, without any deadline. This… View resolved comment

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

state/needs-reviewAwaiting review from the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@Evangelink@0101
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Add MTP server-mode cancellation E2E acceptance test by Evangelink · Pull Request #10888 · microsoft/testfx · GitHub
Skip to content

Add MTP server-mode cancellation E2E acceptance test - #10888

Open
Amaury Levé (Evangelink) wants to merge 10 commits into
microsoft:mainfrom
Evangelink:dev/amauryleve/mtp-cancellation-e2e
Open

Add MTP server-mode cancellation E2E acceptance test#10888
Amaury Levé (Evangelink) wants to merge 10 commits into
microsoft:mainfrom
Evangelink:dev/amauryleve/mtp-cancellation-e2e

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Why

Unit tests already prove that cancelling the source-only server-mode client's (Microsoft.Testing.Platform.ServerMode.Client.Sources) RunTestsAsync call emits a $/cancelRequest notification, but nothing proved the cancellation actually reaches a running MSTest test through TestContext.CancellationToken, or that the server/client shut down cleanly afterwards. This adds that missing end-to-end coverage.

What this does

MtpServerClientCancellationAcceptanceTests launches a real MSTest MTP application in server mode via MtpServerClient and:

  • Runs a deliberately long-running test (Task.Delay(5min, TestContext.CancellationToken)).
  • Waits for an in-progress test-node update before cancelling, so the cancel is never racing against test startup (no blind sleep).
  • Cancels the client's RunTestsAsync token, which sends $/cancelRequest.
  • Asserts the executing test itself observed TestContext.CancellationToken - not just that the client-side task was cancelled or that $/cancelRequest was sent - via a marker file the test method writes from inside a catch (OperationCanceledException ex) when (ex.CancellationToken == TestContext.CancellationToken) block.
  • Asserts RunTestsAsync, ExitAsync, and client Dispose all complete within bounded waits, so a regression here fails fast instead of hanging CI.

A note on the design (no product bug found)

My first attempt asserted on a final failed/"was canceled" test-node update sent back over the wire, mirroring the existing self-cancellation tests. That doesn't work for a hard $/cancelRequest cancel: AsyncConsumerDataProcessor.ConsumeAsync intentionally stops relaying further test-node updates once a run's cancellation token fires (its own comments describe this as letting "a cooperative consumer bail out of its own work immediately"). That's different from the cooperative IGracefulStopTestExecutionCapability path (exercised by DotnetTestPipeServerCancellationTests), which does guarantee reporting survives. Since this is intentional platform behavior rather than a propagation gap, I redesigned the test's "did the test observe cancellation" proof around an out-of-band marker file instead of relying on that channel.

Verification

  • dotnet build test\IntegrationTests\MSTest.Acceptance.IntegrationTests\MSTest.Acceptance.IntegrationTests.csproj -c Release -f net11.0 - 0 errors.
  • Ran the new test directly via the built host with --filter "Name~RunTestsAsync_WhenCanceledAfterTestStarts" - passed in ~2s, 5/5 repeated runs green.
  • Sanity check: temporarily removed TestContext.CancellationToken from the test asset's delay call to confirm the test fails loudly (clear TimeoutException) instead of passing vacuously; reverted after confirming.

Add an end-to-end acceptance test proving that cancelling
Microsoft.Testing.Platform.ServerMode.Client.Sources'' RunTestsAsync propagates
all the way into a real, executing MSTest test through
TestContext.CancellationToken, and that the server/client shut down cleanly
afterwards.
The test launches a real MSTest MTP app in server mode via MtpServerClient,
waits for an in-progress test-node update to confirm the test actually
started (avoiding a timing-only race), cancels the run, and asserts the test
itself observed TestContext.CancellationToken via a marker file written from
inside a catch block keyed on that exact token instance.
A marker file is used instead of asserting on a final test-node update over
the wire: AsyncConsumerDataProcessor.ConsumeAsync intentionally stops relaying
test-node updates once a run is hard-canceled via $/cancelRequest (as opposed
to the cooperative IGracefulStopTestExecutionCapability path), so the final
canceled-test node update is not guaranteed to reach the client. That is
expected platform behavior, not a propagation gap.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
CopilotAI balanced review requested due to automatic review settings August 31, 2026 11:49

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

Review tier: Balanced
Findings: 3 Medium severity

New issues introduced by this change (3)
SeverityFinding
Medium severitytest/​IntegrationTests/​MSTest.Acceptance.IntegrationTests/​MtpServerClientCancellationAcceptanceTests.cs — This unfiltered catch also consumes the OperationCanceledException raised when testTimeoutToken
Medium severitytest/​IntegrationTests/​MSTest.Acceptance.IntegrationTests/​MtpServerClientCancellationAcceptanceTests.cs — Because timeoutSource is linked to cancellationToken, this condition is true for both the local…
Medium severitytest/​IntegrationTests/​MSTest.Acceptance.IntegrationTests/​MtpServerClientCancellationAcceptanceTests.cs — This does not verify the ShutsDownCleanly behavior: ExitAsync only sends the notification, and…
What changed in this PR

Adds E2E coverage for MTP server-mode cancellation reaching an executing MSTest test.

Changes:

  • Launches a real server-mode MSTest application.
  • Verifies cancellation through an out-of-band marker file.
  • Adds bounded waits and shutdown handling.
FileDescription
MtpServerClientCancellationAcceptanceTests.csAdds the cancellation acceptance test and generated test asset.

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

- Filter the catch around runTask.WaitAsync on runCancellation.Token so a
harness-level testTimeoutToken cancellation (a real hang) is never
misreported as the expected client-side run cancellation.
- Fix WaitForFileAsync''s catch filter to distinguish the local timeout from
outer-token cancellation, matching the existing pattern in
AcceptanceTestBase.RunWindowsApplicationModelCommandAsync.
- Add WaitForProcessExitAsync to actually verify the server process exits on
its own after ExitAsync, instead of relying on the using block''s Dispose(),
which force-kills the process if it is still alive and would let a server
that ignores ''exit'' still pass.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings August 31, 2026 12:09

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

Review tier: Balanced
Findings: None

Issues resolved since last review (3)
SeverityFinding
Medium severitytest/​IntegrationTests/​MSTest.Acceptance.IntegrationTests/​MtpServerClientCancellationAcceptanceTests.cs — This does not verify the ShutsDownCleanly behavior: ExitAsync only sends the notification, and… View resolved comment
Medium severitytest/​IntegrationTests/​MSTest.Acceptance.IntegrationTests/​MtpServerClientCancellationAcceptanceTests.cs — Because timeoutSource is linked to cancellationToken, this condition is true for both the local… View resolved comment
Medium severitytest/​IntegrationTests/​MSTest.Acceptance.IntegrationTests/​MtpServerClientCancellationAcceptanceTests.cs — This unfiltered catch also consumes the OperationCanceledException raised when testTimeoutTokenView resolved comment
Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

test/IntegrationTests/MSTest.Acceptance.IntegrationTests/MtpServerClientCancellationAcceptanceTests.cs:258

  • File.WriteAllText creates the file before its buffered contents are flushed and closed, while the parent stops waiting as soon as File.Exists is true. The parent can therefore read an empty/partial marker and fail intermittently even though cancellation propagated. Publish the marker atomically by writing a sibling temporary file and renaming it.
 File.WriteAllText(signalFilePath, $"{nameof(LongRunningCancellableTest)} observed TestContext.CancellationToken");

The parent acceptance test polls for the marker file via File.Exists, but
File.WriteAllText makes the file visible before its contents are fully
flushed, so the parent could observe an empty or partial marker under rare
timing. Write to a sibling .tmp file and atomically rename it into place
instead. Also clean up a stray .tmp file in the acceptance test''s cleanup.
Addresses a "previously missed" finding from Copilot code review.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings August 31, 2026 12:26

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

Review tier: Balanced
Findings: None

@EvangelinkAmaury Levé (Evangelink) added the state/needs-review Awaiting review from the team. label Aug 31, 2026
CI build failed on all platforms with IDE0007 (use 'var' instead of
explicit type) because the repo's .editorconfig treats
csharp_style_var_when_type_is_apparent as an error-level rule and
CancellationTokenSource.CreateLinkedTokenSource(...) is a recognized
'apparent type' factory call. Switch both local declarations to 'var'.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings August 31, 2026 12:56
@github-actions

Copy link
Copy Markdown
Contributor

🔍 Build Failure Analysis

Summary — All four Debug/Release build legs (Linux, macOS, Windows Release, Windows Debug) failed identically with the IDE0007 analyzer promoted to error ("use var instead of explicit type") at two lines in the new test file.

Root cause: IDE0007 — explicit type used where var is required

MtpServerClientCancellationAcceptanceTests.cs declares two local using variables with an explicit CancellationTokenSource type instead of var. The repo's .editorconfig enforces csharp_style_var_when_type_is_apparent/csharp_style_var_for_built_in_types as error severity, so both instances fail every leg's build with IDE0007.

Affected files / errors

Proposed fix (inline suggestions posted on both lines)

- using CancellationTokenSource timeoutSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);+ using var timeoutSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);

This matches the style already used a few lines above in the same file (using var client = MtpServerClient.Launch(...)).


Build overview
  • 4 legs analyzed with build errors: Linux Release, macOS Release, Windows Release, Windows Debug — all identical IDE0007 x2 + "Build failed."
  • 2 legs (application-model acceptance x2) reported no compile errors — unaffected/downstream of these two failing legs.
  • Failing project: test/IntegrationTests/MSTest.Acceptance.IntegrationTests/MSTest.Acceptance.IntegrationTests.csproj, target CoreCompile, task Csc.
All MSBuild errors
CodeProjectFile:LineMessage
IDE0007MSTest.Acceptance.IntegrationTestsMtpServerClientCancellationAcceptanceTests.cs:142use 'var' instead of explicit type
IDE0007MSTest.Acceptance.IntegrationTestsMtpServerClientCancellationAcceptanceTests.cs:159use 'var' instead of explicit type

🤖 Generated by the Build Failure Analysis workflow using (a href="(dev.azure.com/redacted) · commit 6fead25

🤖 Automated content by GitHub Copilot. Generated by the Build Failure Analysis workflow. · auto · 96.5 AIC · ⌖ 1.98 AIC · ⊞ 13.3K · [◷]( · )

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 Automated content by GitHub Copilot. Generated by the Build Failure Analysis workflow. · auto · 96.5 AIC · ⌖ 1.98 AIC · ⊞ 13.3K ·

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

Review tier: Balanced
Findings: None

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

test/IntegrationTests/MSTest.Acceptance.IntegrationTests/MtpServerClientCancellationAcceptanceTests.cs:87

  • The in-progress update does not prove that the test method has started. MSTest publishes it before calling RunSingleTestAsync (TestExecutionManager.Runner.cs:113,141), and that call still creates contexts/runs initializers before invoking the method (UnitTestRunner.RunSingleTest.cs:132-238). Cancellation can therefore happen first; the method can later enter with an already-canceled TestContext.CancellationToken, write the marker, and let this test pass without proving cancellation reached an executing test. Have the asset emit a separate “body started” signal immediately before Task.Delay, wait for that signal here, and only then cancel.
 // Wait until the server confirms the test is actually running before canceling. This avoids a
// timing-only race between "cancel" and "test has started" that a blind Task.Delay would risk.
await testStarted.Task.WaitAsync(WaitTimeout, testTimeoutToken);

The in-progress test-node update is published by MtpTestResultRecorder
before RunSingleTestAsync even runs TestInitialize/constructors, let alone
invokes the test method. Waiting only on that update to gate cancellation
left a window where cancellation could land before the test method body -
and its await Task.Delay(..., TestContext.CancellationToken) - had even
started, letting the test pass without proving cancellation reached
genuinely executing test code.
Have the test asset write a separate "body started" marker file
immediately as the first statement of the test method, before entering the
try/await Task.Delay. The acceptance test now waits for both that signal
and the in-progress node update before cancelling, closing the gap. Shared
the atomic write-then-rename helper between both marker files.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings August 31, 2026 13:16

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

Review tier: Balanced
Findings: None

Suppressed comments (1)

test/IntegrationTests/MSTest.Acceptance.IntegrationTests/MtpServerClientCancellationAcceptanceTests.cs:141

  • ProcessId == 0 proves only that the child terminated; MtpServerProcess.ProcessId returns 0 for every exit, including a crash or non-zero exit. Since ExitAsync only writes a notification, that write can also complete before the client detects that the peer has already died. The test could therefore pass when cancellation crashes the server after writing the marker. Capture a System.Diagnostics.Process handle from the initial PID before cancellation, await its exit after ExitAsync, and assert that ExitCode is zero.
 await client.ExitAsync(testTimeoutToken).WaitAsync(WaitTimeout, testTimeoutToken);
await WaitForProcessExitAsync(client, WaitTimeout, testTimeoutToken);

MtpServerProcess.ProcessId returns 0 for BOTH a graceful exit and a crash,
so polling it after ExitAsync could not distinguish "the server processed
exit cleanly" from "the server crashed right after writing the marker
file" - ExitAsync only writes a fire-and-forget notification, so its
completion does not prove the peer processed it before dying.
Capture a System.Diagnostics.Process handle for the launched server''s PID
immediately after Launch (while it is guaranteed alive), await its exit via
Process.WaitForExitAsync after sending ''exit'', and assert ExitCode == 0.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings August 31, 2026 13:45

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

Review tier: Balanced
Findings: None

Avoid Process.GetProcessById when asserting the server's exit code in the cancellation acceptance test. Linux does not treat that Process instance as the owner of the launched child, so ExitCode throws even after the server exits. Reuse the launched process owned by MtpServerClient instead.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings August 31, 2026 14:49

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

Review tier: Balanced
Findings: 1 High severity

New issues introduced by this change (1)
SeverityFinding
High severitysrc/​Platform/​Microsoft.Testing.Platform.ServerMode.Client.Sources/​Client/​MtpServerProcess.cs — This does not compile for the project's netstandard2.0 target:…

Microsoft.Testing.Platform.ServerMode.Client.Sources is a source-only
package that also targets netstandard2.0 for .NET Framework consumers.
ExitCode/WaitForExitAsync (added in 32f1ae4 to let the acceptance test
assert a clean server shutdown) called Process.WaitForExitAsync, a .NET
5+-only BCL method, which failed the netstandard2.0 build leg with
IDE0007/CS-level errors on Linux CI (flagged by review).
Adding the repo''s existing (but currently unpackaged) ProcessExtensions
polyfill to fix this down-level build would expand this PR''s scope beyond
a test-only change and touch the packaging of a shared, shipped library.
Per review guidance, remove the new accessors instead and keep the
acceptance test''s existing ProcessId-based polling approach (proves the
server exited naturally in response to ''exit'' without requiring
ownership-restricted ExitCode access), with a comment documenting why the
test does not assert on exit code specifically.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings August 31, 2026 15:11

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

Review tier: Balanced
Findings: 1 High severity

New issues introduced by this change (1)
SeverityFinding
High severitytest/​IntegrationTests/​MSTest.Acceptance.IntegrationTests/​MtpServerClientCancellationAcceptanceTests.csInitializeAsync is the only server/client operation here without the 60-second bound. If a…
Issues resolved since last review (1)
SeverityFinding
High severitysrc/​Platform/​Microsoft.Testing.Platform.ServerMode.Client.Sources/​Client/​MtpServerProcess.cs — This does not compile for the project's netstandard2.0 target:… View resolved comment

CopilotAI review requested due to automatic review settings September 1, 2026 07:47

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

Review tier: Balanced
Findings: 1 Medium severity

New issues introduced by this change (1)
SeverityFinding
Medium severitytest/​IntegrationTests/​MSTest.Acceptance.IntegrationTests/​MtpServerClientCancellationAcceptanceTests.csusing var invokes synchronous Dispose() only when this scope exits, without any deadline. This…
Issues resolved since last review (1)
SeverityFinding
High severitytest/​IntegrationTests/​MSTest.Acceptance.IntegrationTests/​MtpServerClientCancellationAcceptanceTests.csInitializeAsync is the only server/client operation here without the 60-second bound. If a… View resolved comment

Prevent Windows Release CI from hanging indefinitely by bounding client initialization and moving synchronous disposal to a bounded asynchronous cleanup path.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 2e0816a8-bfba-488c-abb0-1517c2b4cc7b
CopilotAI review requested due to automatic review settings September 1, 2026 13:15

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

Review tier: Balanced
Findings: None

Issues resolved since last review (1)
SeverityFinding
Medium severitytest/​IntegrationTests/​MSTest.Acceptance.IntegrationTests/​MtpServerClientCancellationAcceptanceTests.csusing var invokes synchronous Dispose() only when this scope exits, without any deadline. This… View resolved comment

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

state/needs-reviewAwaiting review from the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@Evangelink@0101
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Add MTP server-mode cancellation E2E acceptance test by Evangelink · Pull Request #10888 · microsoft/testfx · GitHub
Skip to content

Add MTP server-mode cancellation E2E acceptance test - #10888

Open
Amaury Levé (Evangelink) wants to merge 10 commits into
microsoft:mainfrom
Evangelink:dev/amauryleve/mtp-cancellation-e2e
Open

Add MTP server-mode cancellation E2E acceptance test#10888
Amaury Levé (Evangelink) wants to merge 10 commits into
microsoft:mainfrom
Evangelink:dev/amauryleve/mtp-cancellation-e2e

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Why

Unit tests already prove that cancelling the source-only server-mode client's (Microsoft.Testing.Platform.ServerMode.Client.Sources) RunTestsAsync call emits a $/cancelRequest notification, but nothing proved the cancellation actually reaches a running MSTest test through TestContext.CancellationToken, or that the server/client shut down cleanly afterwards. This adds that missing end-to-end coverage.

What this does

MtpServerClientCancellationAcceptanceTests launches a real MSTest MTP application in server mode via MtpServerClient and:

  • Runs a deliberately long-running test (Task.Delay(5min, TestContext.CancellationToken)).
  • Waits for an in-progress test-node update before cancelling, so the cancel is never racing against test startup (no blind sleep).
  • Cancels the client's RunTestsAsync token, which sends $/cancelRequest.
  • Asserts the executing test itself observed TestContext.CancellationToken - not just that the client-side task was cancelled or that $/cancelRequest was sent - via a marker file the test method writes from inside a catch (OperationCanceledException ex) when (ex.CancellationToken == TestContext.CancellationToken) block.
  • Asserts RunTestsAsync, ExitAsync, and client Dispose all complete within bounded waits, so a regression here fails fast instead of hanging CI.

A note on the design (no product bug found)

My first attempt asserted on a final failed/"was canceled" test-node update sent back over the wire, mirroring the existing self-cancellation tests. That doesn't work for a hard $/cancelRequest cancel: AsyncConsumerDataProcessor.ConsumeAsync intentionally stops relaying further test-node updates once a run's cancellation token fires (its own comments describe this as letting "a cooperative consumer bail out of its own work immediately"). That's different from the cooperative IGracefulStopTestExecutionCapability path (exercised by DotnetTestPipeServerCancellationTests), which does guarantee reporting survives. Since this is intentional platform behavior rather than a propagation gap, I redesigned the test's "did the test observe cancellation" proof around an out-of-band marker file instead of relying on that channel.

Verification

  • dotnet build test\IntegrationTests\MSTest.Acceptance.IntegrationTests\MSTest.Acceptance.IntegrationTests.csproj -c Release -f net11.0 - 0 errors.
  • Ran the new test directly via the built host with --filter "Name~RunTestsAsync_WhenCanceledAfterTestStarts" - passed in ~2s, 5/5 repeated runs green.
  • Sanity check: temporarily removed TestContext.CancellationToken from the test asset's delay call to confirm the test fails loudly (clear TimeoutException) instead of passing vacuously; reverted after confirming.

Add an end-to-end acceptance test proving that cancelling
Microsoft.Testing.Platform.ServerMode.Client.Sources'' RunTestsAsync propagates
all the way into a real, executing MSTest test through
TestContext.CancellationToken, and that the server/client shut down cleanly
afterwards.
The test launches a real MSTest MTP app in server mode via MtpServerClient,
waits for an in-progress test-node update to confirm the test actually
started (avoiding a timing-only race), cancels the run, and asserts the test
itself observed TestContext.CancellationToken via a marker file written from
inside a catch block keyed on that exact token instance.
A marker file is used instead of asserting on a final test-node update over
the wire: AsyncConsumerDataProcessor.ConsumeAsync intentionally stops relaying
test-node updates once a run is hard-canceled via $/cancelRequest (as opposed
to the cooperative IGracefulStopTestExecutionCapability path), so the final
canceled-test node update is not guaranteed to reach the client. That is
expected platform behavior, not a propagation gap.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
CopilotAI balanced review requested due to automatic review settings August 31, 2026 11:49

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

Review tier: Balanced
Findings: 3 Medium severity

New issues introduced by this change (3)
SeverityFinding
Medium severitytest/​IntegrationTests/​MSTest.Acceptance.IntegrationTests/​MtpServerClientCancellationAcceptanceTests.cs — This unfiltered catch also consumes the OperationCanceledException raised when testTimeoutToken
Medium severitytest/​IntegrationTests/​MSTest.Acceptance.IntegrationTests/​MtpServerClientCancellationAcceptanceTests.cs — Because timeoutSource is linked to cancellationToken, this condition is true for both the local…
Medium severitytest/​IntegrationTests/​MSTest.Acceptance.IntegrationTests/​MtpServerClientCancellationAcceptanceTests.cs — This does not verify the ShutsDownCleanly behavior: ExitAsync only sends the notification, and…
What changed in this PR

Adds E2E coverage for MTP server-mode cancellation reaching an executing MSTest test.

Changes:

  • Launches a real server-mode MSTest application.
  • Verifies cancellation through an out-of-band marker file.
  • Adds bounded waits and shutdown handling.
FileDescription
MtpServerClientCancellationAcceptanceTests.csAdds the cancellation acceptance test and generated test asset.

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

- Filter the catch around runTask.WaitAsync on runCancellation.Token so a
harness-level testTimeoutToken cancellation (a real hang) is never
misreported as the expected client-side run cancellation.
- Fix WaitForFileAsync''s catch filter to distinguish the local timeout from
outer-token cancellation, matching the existing pattern in
AcceptanceTestBase.RunWindowsApplicationModelCommandAsync.
- Add WaitForProcessExitAsync to actually verify the server process exits on
its own after ExitAsync, instead of relying on the using block''s Dispose(),
which force-kills the process if it is still alive and would let a server
that ignores ''exit'' still pass.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings August 31, 2026 12:09

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

Review tier: Balanced
Findings: None

Issues resolved since last review (3)
SeverityFinding
Medium severitytest/​IntegrationTests/​MSTest.Acceptance.IntegrationTests/​MtpServerClientCancellationAcceptanceTests.cs — This does not verify the ShutsDownCleanly behavior: ExitAsync only sends the notification, and… View resolved comment
Medium severitytest/​IntegrationTests/​MSTest.Acceptance.IntegrationTests/​MtpServerClientCancellationAcceptanceTests.cs — Because timeoutSource is linked to cancellationToken, this condition is true for both the local… View resolved comment
Medium severitytest/​IntegrationTests/​MSTest.Acceptance.IntegrationTests/​MtpServerClientCancellationAcceptanceTests.cs — This unfiltered catch also consumes the OperationCanceledException raised when testTimeoutTokenView resolved comment
Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

test/IntegrationTests/MSTest.Acceptance.IntegrationTests/MtpServerClientCancellationAcceptanceTests.cs:258

  • File.WriteAllText creates the file before its buffered contents are flushed and closed, while the parent stops waiting as soon as File.Exists is true. The parent can therefore read an empty/partial marker and fail intermittently even though cancellation propagated. Publish the marker atomically by writing a sibling temporary file and renaming it.
 File.WriteAllText(signalFilePath, $"{nameof(LongRunningCancellableTest)} observed TestContext.CancellationToken");

The parent acceptance test polls for the marker file via File.Exists, but
File.WriteAllText makes the file visible before its contents are fully
flushed, so the parent could observe an empty or partial marker under rare
timing. Write to a sibling .tmp file and atomically rename it into place
instead. Also clean up a stray .tmp file in the acceptance test''s cleanup.
Addresses a "previously missed" finding from Copilot code review.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings August 31, 2026 12:26

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

Review tier: Balanced
Findings: None

@EvangelinkAmaury Levé (Evangelink) added the state/needs-review Awaiting review from the team. label Aug 31, 2026
CI build failed on all platforms with IDE0007 (use 'var' instead of
explicit type) because the repo's .editorconfig treats
csharp_style_var_when_type_is_apparent as an error-level rule and
CancellationTokenSource.CreateLinkedTokenSource(...) is a recognized
'apparent type' factory call. Switch both local declarations to 'var'.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings August 31, 2026 12:56
@github-actions

Copy link
Copy Markdown
Contributor

🔍 Build Failure Analysis

Summary — All four Debug/Release build legs (Linux, macOS, Windows Release, Windows Debug) failed identically with the IDE0007 analyzer promoted to error ("use var instead of explicit type") at two lines in the new test file.

Root cause: IDE0007 — explicit type used where var is required

MtpServerClientCancellationAcceptanceTests.cs declares two local using variables with an explicit CancellationTokenSource type instead of var. The repo's .editorconfig enforces csharp_style_var_when_type_is_apparent/csharp_style_var_for_built_in_types as error severity, so both instances fail every leg's build with IDE0007.

Affected files / errors

Proposed fix (inline suggestions posted on both lines)

- using CancellationTokenSource timeoutSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);+ using var timeoutSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);

This matches the style already used a few lines above in the same file (using var client = MtpServerClient.Launch(...)).


Build overview
  • 4 legs analyzed with build errors: Linux Release, macOS Release, Windows Release, Windows Debug — all identical IDE0007 x2 + "Build failed."
  • 2 legs (application-model acceptance x2) reported no compile errors — unaffected/downstream of these two failing legs.
  • Failing project: test/IntegrationTests/MSTest.Acceptance.IntegrationTests/MSTest.Acceptance.IntegrationTests.csproj, target CoreCompile, task Csc.
All MSBuild errors
CodeProjectFile:LineMessage
IDE0007MSTest.Acceptance.IntegrationTestsMtpServerClientCancellationAcceptanceTests.cs:142use 'var' instead of explicit type
IDE0007MSTest.Acceptance.IntegrationTestsMtpServerClientCancellationAcceptanceTests.cs:159use 'var' instead of explicit type

🤖 Generated by the Build Failure Analysis workflow using (a href="(dev.azure.com/redacted) · commit 6fead25

🤖 Automated content by GitHub Copilot. Generated by the Build Failure Analysis workflow. · auto · 96.5 AIC · ⌖ 1.98 AIC · ⊞ 13.3K · [◷]( · )

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 Automated content by GitHub Copilot. Generated by the Build Failure Analysis workflow. · auto · 96.5 AIC · ⌖ 1.98 AIC · ⊞ 13.3K ·

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

Review tier: Balanced
Findings: None

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

test/IntegrationTests/MSTest.Acceptance.IntegrationTests/MtpServerClientCancellationAcceptanceTests.cs:87

  • The in-progress update does not prove that the test method has started. MSTest publishes it before calling RunSingleTestAsync (TestExecutionManager.Runner.cs:113,141), and that call still creates contexts/runs initializers before invoking the method (UnitTestRunner.RunSingleTest.cs:132-238). Cancellation can therefore happen first; the method can later enter with an already-canceled TestContext.CancellationToken, write the marker, and let this test pass without proving cancellation reached an executing test. Have the asset emit a separate “body started” signal immediately before Task.Delay, wait for that signal here, and only then cancel.
 // Wait until the server confirms the test is actually running before canceling. This avoids a
// timing-only race between "cancel" and "test has started" that a blind Task.Delay would risk.
await testStarted.Task.WaitAsync(WaitTimeout, testTimeoutToken);

The in-progress test-node update is published by MtpTestResultRecorder
before RunSingleTestAsync even runs TestInitialize/constructors, let alone
invokes the test method. Waiting only on that update to gate cancellation
left a window where cancellation could land before the test method body -
and its await Task.Delay(..., TestContext.CancellationToken) - had even
started, letting the test pass without proving cancellation reached
genuinely executing test code.
Have the test asset write a separate "body started" marker file
immediately as the first statement of the test method, before entering the
try/await Task.Delay. The acceptance test now waits for both that signal
and the in-progress node update before cancelling, closing the gap. Shared
the atomic write-then-rename helper between both marker files.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings August 31, 2026 13:16

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

Review tier: Balanced
Findings: None

Suppressed comments (1)

test/IntegrationTests/MSTest.Acceptance.IntegrationTests/MtpServerClientCancellationAcceptanceTests.cs:141

  • ProcessId == 0 proves only that the child terminated; MtpServerProcess.ProcessId returns 0 for every exit, including a crash or non-zero exit. Since ExitAsync only writes a notification, that write can also complete before the client detects that the peer has already died. The test could therefore pass when cancellation crashes the server after writing the marker. Capture a System.Diagnostics.Process handle from the initial PID before cancellation, await its exit after ExitAsync, and assert that ExitCode is zero.
 await client.ExitAsync(testTimeoutToken).WaitAsync(WaitTimeout, testTimeoutToken);
await WaitForProcessExitAsync(client, WaitTimeout, testTimeoutToken);

MtpServerProcess.ProcessId returns 0 for BOTH a graceful exit and a crash,
so polling it after ExitAsync could not distinguish "the server processed
exit cleanly" from "the server crashed right after writing the marker
file" - ExitAsync only writes a fire-and-forget notification, so its
completion does not prove the peer processed it before dying.
Capture a System.Diagnostics.Process handle for the launched server''s PID
immediately after Launch (while it is guaranteed alive), await its exit via
Process.WaitForExitAsync after sending ''exit'', and assert ExitCode == 0.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings August 31, 2026 13:45

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

Review tier: Balanced
Findings: None

Avoid Process.GetProcessById when asserting the server's exit code in the cancellation acceptance test. Linux does not treat that Process instance as the owner of the launched child, so ExitCode throws even after the server exits. Reuse the launched process owned by MtpServerClient instead.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings August 31, 2026 14:49

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

Review tier: Balanced
Findings: 1 High severity

New issues introduced by this change (1)
SeverityFinding
High severitysrc/​Platform/​Microsoft.Testing.Platform.ServerMode.Client.Sources/​Client/​MtpServerProcess.cs — This does not compile for the project's netstandard2.0 target:…

Microsoft.Testing.Platform.ServerMode.Client.Sources is a source-only
package that also targets netstandard2.0 for .NET Framework consumers.
ExitCode/WaitForExitAsync (added in 32f1ae4 to let the acceptance test
assert a clean server shutdown) called Process.WaitForExitAsync, a .NET
5+-only BCL method, which failed the netstandard2.0 build leg with
IDE0007/CS-level errors on Linux CI (flagged by review).
Adding the repo''s existing (but currently unpackaged) ProcessExtensions
polyfill to fix this down-level build would expand this PR''s scope beyond
a test-only change and touch the packaging of a shared, shipped library.
Per review guidance, remove the new accessors instead and keep the
acceptance test''s existing ProcessId-based polling approach (proves the
server exited naturally in response to ''exit'' without requiring
ownership-restricted ExitCode access), with a comment documenting why the
test does not assert on exit code specifically.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings August 31, 2026 15:11

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

Review tier: Balanced
Findings: 1 High severity

New issues introduced by this change (1)
SeverityFinding
High severitytest/​IntegrationTests/​MSTest.Acceptance.IntegrationTests/​MtpServerClientCancellationAcceptanceTests.csInitializeAsync is the only server/client operation here without the 60-second bound. If a…
Issues resolved since last review (1)
SeverityFinding
High severitysrc/​Platform/​Microsoft.Testing.Platform.ServerMode.Client.Sources/​Client/​MtpServerProcess.cs — This does not compile for the project's netstandard2.0 target:… View resolved comment

CopilotAI review requested due to automatic review settings September 1, 2026 07:47

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

Review tier: Balanced
Findings: 1 Medium severity

New issues introduced by this change (1)
SeverityFinding
Medium severitytest/​IntegrationTests/​MSTest.Acceptance.IntegrationTests/​MtpServerClientCancellationAcceptanceTests.csusing var invokes synchronous Dispose() only when this scope exits, without any deadline. This…
Issues resolved since last review (1)
SeverityFinding
High severitytest/​IntegrationTests/​MSTest.Acceptance.IntegrationTests/​MtpServerClientCancellationAcceptanceTests.csInitializeAsync is the only server/client operation here without the 60-second bound. If a… View resolved comment

Prevent Windows Release CI from hanging indefinitely by bounding client initialization and moving synchronous disposal to a bounded asynchronous cleanup path.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 2e0816a8-bfba-488c-abb0-1517c2b4cc7b
CopilotAI review requested due to automatic review settings September 1, 2026 13:15

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

Review tier: Balanced
Findings: None

Issues resolved since last review (1)
SeverityFinding
Medium severitytest/​IntegrationTests/​MSTest.Acceptance.IntegrationTests/​MtpServerClientCancellationAcceptanceTests.csusing var invokes synchronous Dispose() only when this scope exits, without any deadline. This… View resolved comment

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

state/needs-reviewAwaiting review from the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@Evangelink@0101
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Add MTP server-mode cancellation E2E acceptance test by Evangelink · Pull Request #10888 · microsoft/testfx · GitHub
Skip to content

Add MTP server-mode cancellation E2E acceptance test - #10888

Open
Amaury Levé (Evangelink) wants to merge 10 commits into
microsoft:mainfrom
Evangelink:dev/amauryleve/mtp-cancellation-e2e
Open

Add MTP server-mode cancellation E2E acceptance test#10888
Amaury Levé (Evangelink) wants to merge 10 commits into
microsoft:mainfrom
Evangelink:dev/amauryleve/mtp-cancellation-e2e

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Why

Unit tests already prove that cancelling the source-only server-mode client's (Microsoft.Testing.Platform.ServerMode.Client.Sources) RunTestsAsync call emits a $/cancelRequest notification, but nothing proved the cancellation actually reaches a running MSTest test through TestContext.CancellationToken, or that the server/client shut down cleanly afterwards. This adds that missing end-to-end coverage.

What this does

MtpServerClientCancellationAcceptanceTests launches a real MSTest MTP application in server mode via MtpServerClient and:

  • Runs a deliberately long-running test (Task.Delay(5min, TestContext.CancellationToken)).
  • Waits for an in-progress test-node update before cancelling, so the cancel is never racing against test startup (no blind sleep).
  • Cancels the client's RunTestsAsync token, which sends $/cancelRequest.
  • Asserts the executing test itself observed TestContext.CancellationToken - not just that the client-side task was cancelled or that $/cancelRequest was sent - via a marker file the test method writes from inside a catch (OperationCanceledException ex) when (ex.CancellationToken == TestContext.CancellationToken) block.
  • Asserts RunTestsAsync, ExitAsync, and client Dispose all complete within bounded waits, so a regression here fails fast instead of hanging CI.

A note on the design (no product bug found)

My first attempt asserted on a final failed/"was canceled" test-node update sent back over the wire, mirroring the existing self-cancellation tests. That doesn't work for a hard $/cancelRequest cancel: AsyncConsumerDataProcessor.ConsumeAsync intentionally stops relaying further test-node updates once a run's cancellation token fires (its own comments describe this as letting "a cooperative consumer bail out of its own work immediately"). That's different from the cooperative IGracefulStopTestExecutionCapability path (exercised by DotnetTestPipeServerCancellationTests), which does guarantee reporting survives. Since this is intentional platform behavior rather than a propagation gap, I redesigned the test's "did the test observe cancellation" proof around an out-of-band marker file instead of relying on that channel.

Verification

  • dotnet build test\IntegrationTests\MSTest.Acceptance.IntegrationTests\MSTest.Acceptance.IntegrationTests.csproj -c Release -f net11.0 - 0 errors.
  • Ran the new test directly via the built host with --filter "Name~RunTestsAsync_WhenCanceledAfterTestStarts" - passed in ~2s, 5/5 repeated runs green.
  • Sanity check: temporarily removed TestContext.CancellationToken from the test asset's delay call to confirm the test fails loudly (clear TimeoutException) instead of passing vacuously; reverted after confirming.

Add an end-to-end acceptance test proving that cancelling
Microsoft.Testing.Platform.ServerMode.Client.Sources'' RunTestsAsync propagates
all the way into a real, executing MSTest test through
TestContext.CancellationToken, and that the server/client shut down cleanly
afterwards.
The test launches a real MSTest MTP app in server mode via MtpServerClient,
waits for an in-progress test-node update to confirm the test actually
started (avoiding a timing-only race), cancels the run, and asserts the test
itself observed TestContext.CancellationToken via a marker file written from
inside a catch block keyed on that exact token instance.
A marker file is used instead of asserting on a final test-node update over
the wire: AsyncConsumerDataProcessor.ConsumeAsync intentionally stops relaying
test-node updates once a run is hard-canceled via $/cancelRequest (as opposed
to the cooperative IGracefulStopTestExecutionCapability path), so the final
canceled-test node update is not guaranteed to reach the client. That is
expected platform behavior, not a propagation gap.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
CopilotAI balanced review requested due to automatic review settings August 31, 2026 11:49

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

Review tier: Balanced
Findings: 3 Medium severity

New issues introduced by this change (3)
SeverityFinding
Medium severitytest/​IntegrationTests/​MSTest.Acceptance.IntegrationTests/​MtpServerClientCancellationAcceptanceTests.cs — This unfiltered catch also consumes the OperationCanceledException raised when testTimeoutToken
Medium severitytest/​IntegrationTests/​MSTest.Acceptance.IntegrationTests/​MtpServerClientCancellationAcceptanceTests.cs — Because timeoutSource is linked to cancellationToken, this condition is true for both the local…
Medium severitytest/​IntegrationTests/​MSTest.Acceptance.IntegrationTests/​MtpServerClientCancellationAcceptanceTests.cs — This does not verify the ShutsDownCleanly behavior: ExitAsync only sends the notification, and…
What changed in this PR

Adds E2E coverage for MTP server-mode cancellation reaching an executing MSTest test.

Changes:

  • Launches a real server-mode MSTest application.
  • Verifies cancellation through an out-of-band marker file.
  • Adds bounded waits and shutdown handling.
FileDescription
MtpServerClientCancellationAcceptanceTests.csAdds the cancellation acceptance test and generated test asset.

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

- Filter the catch around runTask.WaitAsync on runCancellation.Token so a
harness-level testTimeoutToken cancellation (a real hang) is never
misreported as the expected client-side run cancellation.
- Fix WaitForFileAsync''s catch filter to distinguish the local timeout from
outer-token cancellation, matching the existing pattern in
AcceptanceTestBase.RunWindowsApplicationModelCommandAsync.
- Add WaitForProcessExitAsync to actually verify the server process exits on
its own after ExitAsync, instead of relying on the using block''s Dispose(),
which force-kills the process if it is still alive and would let a server
that ignores ''exit'' still pass.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings August 31, 2026 12:09

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

Review tier: Balanced
Findings: None

Issues resolved since last review (3)
SeverityFinding
Medium severitytest/​IntegrationTests/​MSTest.Acceptance.IntegrationTests/​MtpServerClientCancellationAcceptanceTests.cs — This does not verify the ShutsDownCleanly behavior: ExitAsync only sends the notification, and… View resolved comment
Medium severitytest/​IntegrationTests/​MSTest.Acceptance.IntegrationTests/​MtpServerClientCancellationAcceptanceTests.cs — Because timeoutSource is linked to cancellationToken, this condition is true for both the local… View resolved comment
Medium severitytest/​IntegrationTests/​MSTest.Acceptance.IntegrationTests/​MtpServerClientCancellationAcceptanceTests.cs — This unfiltered catch also consumes the OperationCanceledException raised when testTimeoutTokenView resolved comment
Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

test/IntegrationTests/MSTest.Acceptance.IntegrationTests/MtpServerClientCancellationAcceptanceTests.cs:258

  • File.WriteAllText creates the file before its buffered contents are flushed and closed, while the parent stops waiting as soon as File.Exists is true. The parent can therefore read an empty/partial marker and fail intermittently even though cancellation propagated. Publish the marker atomically by writing a sibling temporary file and renaming it.
 File.WriteAllText(signalFilePath, $"{nameof(LongRunningCancellableTest)} observed TestContext.CancellationToken");

The parent acceptance test polls for the marker file via File.Exists, but
File.WriteAllText makes the file visible before its contents are fully
flushed, so the parent could observe an empty or partial marker under rare
timing. Write to a sibling .tmp file and atomically rename it into place
instead. Also clean up a stray .tmp file in the acceptance test''s cleanup.
Addresses a "previously missed" finding from Copilot code review.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings August 31, 2026 12:26

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

Review tier: Balanced
Findings: None

@EvangelinkAmaury Levé (Evangelink) added the state/needs-review Awaiting review from the team. label Aug 31, 2026
CI build failed on all platforms with IDE0007 (use 'var' instead of
explicit type) because the repo's .editorconfig treats
csharp_style_var_when_type_is_apparent as an error-level rule and
CancellationTokenSource.CreateLinkedTokenSource(...) is a recognized
'apparent type' factory call. Switch both local declarations to 'var'.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings August 31, 2026 12:56
@github-actions

Copy link
Copy Markdown
Contributor

🔍 Build Failure Analysis

Summary — All four Debug/Release build legs (Linux, macOS, Windows Release, Windows Debug) failed identically with the IDE0007 analyzer promoted to error ("use var instead of explicit type") at two lines in the new test file.

Root cause: IDE0007 — explicit type used where var is required

MtpServerClientCancellationAcceptanceTests.cs declares two local using variables with an explicit CancellationTokenSource type instead of var. The repo's .editorconfig enforces csharp_style_var_when_type_is_apparent/csharp_style_var_for_built_in_types as error severity, so both instances fail every leg's build with IDE0007.

Affected files / errors

Proposed fix (inline suggestions posted on both lines)

- using CancellationTokenSource timeoutSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);+ using var timeoutSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);

This matches the style already used a few lines above in the same file (using var client = MtpServerClient.Launch(...)).


Build overview
  • 4 legs analyzed with build errors: Linux Release, macOS Release, Windows Release, Windows Debug — all identical IDE0007 x2 + "Build failed."
  • 2 legs (application-model acceptance x2) reported no compile errors — unaffected/downstream of these two failing legs.
  • Failing project: test/IntegrationTests/MSTest.Acceptance.IntegrationTests/MSTest.Acceptance.IntegrationTests.csproj, target CoreCompile, task Csc.
All MSBuild errors
CodeProjectFile:LineMessage
IDE0007MSTest.Acceptance.IntegrationTestsMtpServerClientCancellationAcceptanceTests.cs:142use 'var' instead of explicit type
IDE0007MSTest.Acceptance.IntegrationTestsMtpServerClientCancellationAcceptanceTests.cs:159use 'var' instead of explicit type

🤖 Generated by the Build Failure Analysis workflow using (a href="(dev.azure.com/redacted) · commit 6fead25

🤖 Automated content by GitHub Copilot. Generated by the Build Failure Analysis workflow. · auto · 96.5 AIC · ⌖ 1.98 AIC · ⊞ 13.3K · [◷]( · )

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 Automated content by GitHub Copilot. Generated by the Build Failure Analysis workflow. · auto · 96.5 AIC · ⌖ 1.98 AIC · ⊞ 13.3K ·

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

Review tier: Balanced
Findings: None

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

test/IntegrationTests/MSTest.Acceptance.IntegrationTests/MtpServerClientCancellationAcceptanceTests.cs:87

  • The in-progress update does not prove that the test method has started. MSTest publishes it before calling RunSingleTestAsync (TestExecutionManager.Runner.cs:113,141), and that call still creates contexts/runs initializers before invoking the method (UnitTestRunner.RunSingleTest.cs:132-238). Cancellation can therefore happen first; the method can later enter with an already-canceled TestContext.CancellationToken, write the marker, and let this test pass without proving cancellation reached an executing test. Have the asset emit a separate “body started” signal immediately before Task.Delay, wait for that signal here, and only then cancel.
 // Wait until the server confirms the test is actually running before canceling. This avoids a
// timing-only race between "cancel" and "test has started" that a blind Task.Delay would risk.
await testStarted.Task.WaitAsync(WaitTimeout, testTimeoutToken);

The in-progress test-node update is published by MtpTestResultRecorder
before RunSingleTestAsync even runs TestInitialize/constructors, let alone
invokes the test method. Waiting only on that update to gate cancellation
left a window where cancellation could land before the test method body -
and its await Task.Delay(..., TestContext.CancellationToken) - had even
started, letting the test pass without proving cancellation reached
genuinely executing test code.
Have the test asset write a separate "body started" marker file
immediately as the first statement of the test method, before entering the
try/await Task.Delay. The acceptance test now waits for both that signal
and the in-progress node update before cancelling, closing the gap. Shared
the atomic write-then-rename helper between both marker files.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings August 31, 2026 13:16

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

Review tier: Balanced
Findings: None

Suppressed comments (1)

test/IntegrationTests/MSTest.Acceptance.IntegrationTests/MtpServerClientCancellationAcceptanceTests.cs:141

  • ProcessId == 0 proves only that the child terminated; MtpServerProcess.ProcessId returns 0 for every exit, including a crash or non-zero exit. Since ExitAsync only writes a notification, that write can also complete before the client detects that the peer has already died. The test could therefore pass when cancellation crashes the server after writing the marker. Capture a System.Diagnostics.Process handle from the initial PID before cancellation, await its exit after ExitAsync, and assert that ExitCode is zero.
 await client.ExitAsync(testTimeoutToken).WaitAsync(WaitTimeout, testTimeoutToken);
await WaitForProcessExitAsync(client, WaitTimeout, testTimeoutToken);

MtpServerProcess.ProcessId returns 0 for BOTH a graceful exit and a crash,
so polling it after ExitAsync could not distinguish "the server processed
exit cleanly" from "the server crashed right after writing the marker
file" - ExitAsync only writes a fire-and-forget notification, so its
completion does not prove the peer processed it before dying.
Capture a System.Diagnostics.Process handle for the launched server''s PID
immediately after Launch (while it is guaranteed alive), await its exit via
Process.WaitForExitAsync after sending ''exit'', and assert ExitCode == 0.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings August 31, 2026 13:45

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

Review tier: Balanced
Findings: None

Avoid Process.GetProcessById when asserting the server's exit code in the cancellation acceptance test. Linux does not treat that Process instance as the owner of the launched child, so ExitCode throws even after the server exits. Reuse the launched process owned by MtpServerClient instead.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings August 31, 2026 14:49

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

Review tier: Balanced
Findings: 1 High severity

New issues introduced by this change (1)
SeverityFinding
High severitysrc/​Platform/​Microsoft.Testing.Platform.ServerMode.Client.Sources/​Client/​MtpServerProcess.cs — This does not compile for the project's netstandard2.0 target:…

Microsoft.Testing.Platform.ServerMode.Client.Sources is a source-only
package that also targets netstandard2.0 for .NET Framework consumers.
ExitCode/WaitForExitAsync (added in 32f1ae4 to let the acceptance test
assert a clean server shutdown) called Process.WaitForExitAsync, a .NET
5+-only BCL method, which failed the netstandard2.0 build leg with
IDE0007/CS-level errors on Linux CI (flagged by review).
Adding the repo''s existing (but currently unpackaged) ProcessExtensions
polyfill to fix this down-level build would expand this PR''s scope beyond
a test-only change and touch the packaging of a shared, shipped library.
Per review guidance, remove the new accessors instead and keep the
acceptance test''s existing ProcessId-based polling approach (proves the
server exited naturally in response to ''exit'' without requiring
ownership-restricted ExitCode access), with a comment documenting why the
test does not assert on exit code specifically.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings August 31, 2026 15:11

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

Review tier: Balanced
Findings: 1 High severity

New issues introduced by this change (1)
SeverityFinding
High severitytest/​IntegrationTests/​MSTest.Acceptance.IntegrationTests/​MtpServerClientCancellationAcceptanceTests.csInitializeAsync is the only server/client operation here without the 60-second bound. If a…
Issues resolved since last review (1)
SeverityFinding
High severitysrc/​Platform/​Microsoft.Testing.Platform.ServerMode.Client.Sources/​Client/​MtpServerProcess.cs — This does not compile for the project's netstandard2.0 target:… View resolved comment

CopilotAI review requested due to automatic review settings September 1, 2026 07:47

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

Review tier: Balanced
Findings: 1 Medium severity

New issues introduced by this change (1)
SeverityFinding
Medium severitytest/​IntegrationTests/​MSTest.Acceptance.IntegrationTests/​MtpServerClientCancellationAcceptanceTests.csusing var invokes synchronous Dispose() only when this scope exits, without any deadline. This…
Issues resolved since last review (1)
SeverityFinding
High severitytest/​IntegrationTests/​MSTest.Acceptance.IntegrationTests/​MtpServerClientCancellationAcceptanceTests.csInitializeAsync is the only server/client operation here without the 60-second bound. If a… View resolved comment

Prevent Windows Release CI from hanging indefinitely by bounding client initialization and moving synchronous disposal to a bounded asynchronous cleanup path.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 2e0816a8-bfba-488c-abb0-1517c2b4cc7b
CopilotAI review requested due to automatic review settings September 1, 2026 13:15

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

Review tier: Balanced
Findings: None

Issues resolved since last review (1)
SeverityFinding
Medium severitytest/​IntegrationTests/​MSTest.Acceptance.IntegrationTests/​MtpServerClientCancellationAcceptanceTests.csusing var invokes synchronous Dispose() only when this scope exits, without any deadline. This… View resolved comment

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

state/needs-reviewAwaiting review from the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@Evangelink@0101
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' Add MTP server-mode cancellation E2E acceptance test by Evangelink · Pull Request #10888 · microsoft/testfx · GitHub
Skip to content

Add MTP server-mode cancellation E2E acceptance test - #10888

Open
Amaury Levé (Evangelink) wants to merge 10 commits into
microsoft:mainfrom
Evangelink:dev/amauryleve/mtp-cancellation-e2e
Open

Add MTP server-mode cancellation E2E acceptance test#10888
Amaury Levé (Evangelink) wants to merge 10 commits into
microsoft:mainfrom
Evangelink:dev/amauryleve/mtp-cancellation-e2e

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Why

Unit tests already prove that cancelling the source-only server-mode client's (Microsoft.Testing.Platform.ServerMode.Client.Sources) RunTestsAsync call emits a $/cancelRequest notification, but nothing proved the cancellation actually reaches a running MSTest test through TestContext.CancellationToken, or that the server/client shut down cleanly afterwards. This adds that missing end-to-end coverage.

What this does

MtpServerClientCancellationAcceptanceTests launches a real MSTest MTP application in server mode via MtpServerClient and:

  • Runs a deliberately long-running test (Task.Delay(5min, TestContext.CancellationToken)).
  • Waits for an in-progress test-node update before cancelling, so the cancel is never racing against test startup (no blind sleep).
  • Cancels the client's RunTestsAsync token, which sends $/cancelRequest.
  • Asserts the executing test itself observed TestContext.CancellationToken - not just that the client-side task was cancelled or that $/cancelRequest was sent - via a marker file the test method writes from inside a catch (OperationCanceledException ex) when (ex.CancellationToken == TestContext.CancellationToken) block.
  • Asserts RunTestsAsync, ExitAsync, and client Dispose all complete within bounded waits, so a regression here fails fast instead of hanging CI.

A note on the design (no product bug found)

My first attempt asserted on a final failed/"was canceled" test-node update sent back over the wire, mirroring the existing self-cancellation tests. That doesn't work for a hard $/cancelRequest cancel: AsyncConsumerDataProcessor.ConsumeAsync intentionally stops relaying further test-node updates once a run's cancellation token fires (its own comments describe this as letting "a cooperative consumer bail out of its own work immediately"). That's different from the cooperative IGracefulStopTestExecutionCapability path (exercised by DotnetTestPipeServerCancellationTests), which does guarantee reporting survives. Since this is intentional platform behavior rather than a propagation gap, I redesigned the test's "did the test observe cancellation" proof around an out-of-band marker file instead of relying on that channel.

Verification

  • dotnet build test\IntegrationTests\MSTest.Acceptance.IntegrationTests\MSTest.Acceptance.IntegrationTests.csproj -c Release -f net11.0 - 0 errors.
  • Ran the new test directly via the built host with --filter "Name~RunTestsAsync_WhenCanceledAfterTestStarts" - passed in ~2s, 5/5 repeated runs green.
  • Sanity check: temporarily removed TestContext.CancellationToken from the test asset's delay call to confirm the test fails loudly (clear TimeoutException) instead of passing vacuously; reverted after confirming.

Add an end-to-end acceptance test proving that cancelling
Microsoft.Testing.Platform.ServerMode.Client.Sources'' RunTestsAsync propagates
all the way into a real, executing MSTest test through
TestContext.CancellationToken, and that the server/client shut down cleanly
afterwards.
The test launches a real MSTest MTP app in server mode via MtpServerClient,
waits for an in-progress test-node update to confirm the test actually
started (avoiding a timing-only race), cancels the run, and asserts the test
itself observed TestContext.CancellationToken via a marker file written from
inside a catch block keyed on that exact token instance.
A marker file is used instead of asserting on a final test-node update over
the wire: AsyncConsumerDataProcessor.ConsumeAsync intentionally stops relaying
test-node updates once a run is hard-canceled via $/cancelRequest (as opposed
to the cooperative IGracefulStopTestExecutionCapability path), so the final
canceled-test node update is not guaranteed to reach the client. That is
expected platform behavior, not a propagation gap.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
CopilotAI balanced review requested due to automatic review settings August 31, 2026 11:49

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

Review tier: Balanced
Findings: 3 Medium severity

New issues introduced by this change (3)
SeverityFinding
Medium severitytest/​IntegrationTests/​MSTest.Acceptance.IntegrationTests/​MtpServerClientCancellationAcceptanceTests.cs — This unfiltered catch also consumes the OperationCanceledException raised when testTimeoutToken
Medium severitytest/​IntegrationTests/​MSTest.Acceptance.IntegrationTests/​MtpServerClientCancellationAcceptanceTests.cs — Because timeoutSource is linked to cancellationToken, this condition is true for both the local…
Medium severitytest/​IntegrationTests/​MSTest.Acceptance.IntegrationTests/​MtpServerClientCancellationAcceptanceTests.cs — This does not verify the ShutsDownCleanly behavior: ExitAsync only sends the notification, and…
What changed in this PR

Adds E2E coverage for MTP server-mode cancellation reaching an executing MSTest test.

Changes:

  • Launches a real server-mode MSTest application.
  • Verifies cancellation through an out-of-band marker file.
  • Adds bounded waits and shutdown handling.
FileDescription
MtpServerClientCancellationAcceptanceTests.csAdds the cancellation acceptance test and generated test asset.

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

- Filter the catch around runTask.WaitAsync on runCancellation.Token so a
harness-level testTimeoutToken cancellation (a real hang) is never
misreported as the expected client-side run cancellation.
- Fix WaitForFileAsync''s catch filter to distinguish the local timeout from
outer-token cancellation, matching the existing pattern in
AcceptanceTestBase.RunWindowsApplicationModelCommandAsync.
- Add WaitForProcessExitAsync to actually verify the server process exits on
its own after ExitAsync, instead of relying on the using block''s Dispose(),
which force-kills the process if it is still alive and would let a server
that ignores ''exit'' still pass.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings August 31, 2026 12:09

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

Review tier: Balanced
Findings: None

Issues resolved since last review (3)
SeverityFinding
Medium severitytest/​IntegrationTests/​MSTest.Acceptance.IntegrationTests/​MtpServerClientCancellationAcceptanceTests.cs — This does not verify the ShutsDownCleanly behavior: ExitAsync only sends the notification, and… View resolved comment
Medium severitytest/​IntegrationTests/​MSTest.Acceptance.IntegrationTests/​MtpServerClientCancellationAcceptanceTests.cs — Because timeoutSource is linked to cancellationToken, this condition is true for both the local… View resolved comment
Medium severitytest/​IntegrationTests/​MSTest.Acceptance.IntegrationTests/​MtpServerClientCancellationAcceptanceTests.cs — This unfiltered catch also consumes the OperationCanceledException raised when testTimeoutTokenView resolved comment
Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

test/IntegrationTests/MSTest.Acceptance.IntegrationTests/MtpServerClientCancellationAcceptanceTests.cs:258

  • File.WriteAllText creates the file before its buffered contents are flushed and closed, while the parent stops waiting as soon as File.Exists is true. The parent can therefore read an empty/partial marker and fail intermittently even though cancellation propagated. Publish the marker atomically by writing a sibling temporary file and renaming it.
 File.WriteAllText(signalFilePath, $"{nameof(LongRunningCancellableTest)} observed TestContext.CancellationToken");

The parent acceptance test polls for the marker file via File.Exists, but
File.WriteAllText makes the file visible before its contents are fully
flushed, so the parent could observe an empty or partial marker under rare
timing. Write to a sibling .tmp file and atomically rename it into place
instead. Also clean up a stray .tmp file in the acceptance test''s cleanup.
Addresses a "previously missed" finding from Copilot code review.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings August 31, 2026 12:26

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

Review tier: Balanced
Findings: None

@EvangelinkAmaury Levé (Evangelink) added the state/needs-review Awaiting review from the team. label Aug 31, 2026
CI build failed on all platforms with IDE0007 (use 'var' instead of
explicit type) because the repo's .editorconfig treats
csharp_style_var_when_type_is_apparent as an error-level rule and
CancellationTokenSource.CreateLinkedTokenSource(...) is a recognized
'apparent type' factory call. Switch both local declarations to 'var'.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings August 31, 2026 12:56
@github-actions

Copy link
Copy Markdown
Contributor

🔍 Build Failure Analysis

Summary — All four Debug/Release build legs (Linux, macOS, Windows Release, Windows Debug) failed identically with the IDE0007 analyzer promoted to error ("use var instead of explicit type") at two lines in the new test file.

Root cause: IDE0007 — explicit type used where var is required

MtpServerClientCancellationAcceptanceTests.cs declares two local using variables with an explicit CancellationTokenSource type instead of var. The repo's .editorconfig enforces csharp_style_var_when_type_is_apparent/csharp_style_var_for_built_in_types as error severity, so both instances fail every leg's build with IDE0007.

Affected files / errors

Proposed fix (inline suggestions posted on both lines)

- using CancellationTokenSource timeoutSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);+ using var timeoutSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);

This matches the style already used a few lines above in the same file (using var client = MtpServerClient.Launch(...)).


Build overview
  • 4 legs analyzed with build errors: Linux Release, macOS Release, Windows Release, Windows Debug — all identical IDE0007 x2 + "Build failed."
  • 2 legs (application-model acceptance x2) reported no compile errors — unaffected/downstream of these two failing legs.
  • Failing project: test/IntegrationTests/MSTest.Acceptance.IntegrationTests/MSTest.Acceptance.IntegrationTests.csproj, target CoreCompile, task Csc.
All MSBuild errors
CodeProjectFile:LineMessage
IDE0007MSTest.Acceptance.IntegrationTestsMtpServerClientCancellationAcceptanceTests.cs:142use 'var' instead of explicit type
IDE0007MSTest.Acceptance.IntegrationTestsMtpServerClientCancellationAcceptanceTests.cs:159use 'var' instead of explicit type

🤖 Generated by the Build Failure Analysis workflow using (a href="(dev.azure.com/redacted) · commit 6fead25

🤖 Automated content by GitHub Copilot. Generated by the Build Failure Analysis workflow. · auto · 96.5 AIC · ⌖ 1.98 AIC · ⊞ 13.3K · [◷]( · )

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 Automated content by GitHub Copilot. Generated by the Build Failure Analysis workflow. · auto · 96.5 AIC · ⌖ 1.98 AIC · ⊞ 13.3K ·

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

Review tier: Balanced
Findings: None

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

test/IntegrationTests/MSTest.Acceptance.IntegrationTests/MtpServerClientCancellationAcceptanceTests.cs:87

  • The in-progress update does not prove that the test method has started. MSTest publishes it before calling RunSingleTestAsync (TestExecutionManager.Runner.cs:113,141), and that call still creates contexts/runs initializers before invoking the method (UnitTestRunner.RunSingleTest.cs:132-238). Cancellation can therefore happen first; the method can later enter with an already-canceled TestContext.CancellationToken, write the marker, and let this test pass without proving cancellation reached an executing test. Have the asset emit a separate “body started” signal immediately before Task.Delay, wait for that signal here, and only then cancel.
 // Wait until the server confirms the test is actually running before canceling. This avoids a
// timing-only race between "cancel" and "test has started" that a blind Task.Delay would risk.
await testStarted.Task.WaitAsync(WaitTimeout, testTimeoutToken);

The in-progress test-node update is published by MtpTestResultRecorder
before RunSingleTestAsync even runs TestInitialize/constructors, let alone
invokes the test method. Waiting only on that update to gate cancellation
left a window where cancellation could land before the test method body -
and its await Task.Delay(..., TestContext.CancellationToken) - had even
started, letting the test pass without proving cancellation reached
genuinely executing test code.
Have the test asset write a separate "body started" marker file
immediately as the first statement of the test method, before entering the
try/await Task.Delay. The acceptance test now waits for both that signal
and the in-progress node update before cancelling, closing the gap. Shared
the atomic write-then-rename helper between both marker files.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings August 31, 2026 13:16

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

Review tier: Balanced
Findings: None

Suppressed comments (1)

test/IntegrationTests/MSTest.Acceptance.IntegrationTests/MtpServerClientCancellationAcceptanceTests.cs:141

  • ProcessId == 0 proves only that the child terminated; MtpServerProcess.ProcessId returns 0 for every exit, including a crash or non-zero exit. Since ExitAsync only writes a notification, that write can also complete before the client detects that the peer has already died. The test could therefore pass when cancellation crashes the server after writing the marker. Capture a System.Diagnostics.Process handle from the initial PID before cancellation, await its exit after ExitAsync, and assert that ExitCode is zero.
 await client.ExitAsync(testTimeoutToken).WaitAsync(WaitTimeout, testTimeoutToken);
await WaitForProcessExitAsync(client, WaitTimeout, testTimeoutToken);

MtpServerProcess.ProcessId returns 0 for BOTH a graceful exit and a crash,
so polling it after ExitAsync could not distinguish "the server processed
exit cleanly" from "the server crashed right after writing the marker
file" - ExitAsync only writes a fire-and-forget notification, so its
completion does not prove the peer processed it before dying.
Capture a System.Diagnostics.Process handle for the launched server''s PID
immediately after Launch (while it is guaranteed alive), await its exit via
Process.WaitForExitAsync after sending ''exit'', and assert ExitCode == 0.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings August 31, 2026 13:45

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

Review tier: Balanced
Findings: None

Avoid Process.GetProcessById when asserting the server's exit code in the cancellation acceptance test. Linux does not treat that Process instance as the owner of the launched child, so ExitCode throws even after the server exits. Reuse the launched process owned by MtpServerClient instead.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings August 31, 2026 14:49

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

Review tier: Balanced
Findings: 1 High severity

New issues introduced by this change (1)
SeverityFinding
High severitysrc/​Platform/​Microsoft.Testing.Platform.ServerMode.Client.Sources/​Client/​MtpServerProcess.cs — This does not compile for the project's netstandard2.0 target:…

Microsoft.Testing.Platform.ServerMode.Client.Sources is a source-only
package that also targets netstandard2.0 for .NET Framework consumers.
ExitCode/WaitForExitAsync (added in 32f1ae4 to let the acceptance test
assert a clean server shutdown) called Process.WaitForExitAsync, a .NET
5+-only BCL method, which failed the netstandard2.0 build leg with
IDE0007/CS-level errors on Linux CI (flagged by review).
Adding the repo''s existing (but currently unpackaged) ProcessExtensions
polyfill to fix this down-level build would expand this PR''s scope beyond
a test-only change and touch the packaging of a shared, shipped library.
Per review guidance, remove the new accessors instead and keep the
acceptance test''s existing ProcessId-based polling approach (proves the
server exited naturally in response to ''exit'' without requiring
ownership-restricted ExitCode access), with a comment documenting why the
test does not assert on exit code specifically.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings August 31, 2026 15:11

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

Review tier: Balanced
Findings: 1 High severity

New issues introduced by this change (1)
SeverityFinding
High severitytest/​IntegrationTests/​MSTest.Acceptance.IntegrationTests/​MtpServerClientCancellationAcceptanceTests.csInitializeAsync is the only server/client operation here without the 60-second bound. If a…
Issues resolved since last review (1)
SeverityFinding
High severitysrc/​Platform/​Microsoft.Testing.Platform.ServerMode.Client.Sources/​Client/​MtpServerProcess.cs — This does not compile for the project's netstandard2.0 target:… View resolved comment

CopilotAI review requested due to automatic review settings September 1, 2026 07:47

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

Review tier: Balanced
Findings: 1 Medium severity

New issues introduced by this change (1)
SeverityFinding
Medium severitytest/​IntegrationTests/​MSTest.Acceptance.IntegrationTests/​MtpServerClientCancellationAcceptanceTests.csusing var invokes synchronous Dispose() only when this scope exits, without any deadline. This…
Issues resolved since last review (1)
SeverityFinding
High severitytest/​IntegrationTests/​MSTest.Acceptance.IntegrationTests/​MtpServerClientCancellationAcceptanceTests.csInitializeAsync is the only server/client operation here without the 60-second bound. If a… View resolved comment

Prevent Windows Release CI from hanging indefinitely by bounding client initialization and moving synchronous disposal to a bounded asynchronous cleanup path.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 2e0816a8-bfba-488c-abb0-1517c2b4cc7b
CopilotAI review requested due to automatic review settings September 1, 2026 13:15

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

Review tier: Balanced
Findings: None

Issues resolved since last review (1)
SeverityFinding
Medium severitytest/​IntegrationTests/​MSTest.Acceptance.IntegrationTests/​MtpServerClientCancellationAcceptanceTests.csusing var invokes synchronous Dispose() only when this scope exits, without any deadline. This… View resolved comment

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

state/needs-reviewAwaiting review from the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@Evangelink@0101
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Add MTP server-mode cancellation E2E acceptance test by Evangelink · Pull Request #10888 · microsoft/testfx · GitHub
Skip to content

Add MTP server-mode cancellation E2E acceptance test - #10888

Open
Amaury Levé (Evangelink) wants to merge 10 commits into
microsoft:mainfrom
Evangelink:dev/amauryleve/mtp-cancellation-e2e
Open

Add MTP server-mode cancellation E2E acceptance test#10888
Amaury Levé (Evangelink) wants to merge 10 commits into
microsoft:mainfrom
Evangelink:dev/amauryleve/mtp-cancellation-e2e

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Why

Unit tests already prove that cancelling the source-only server-mode client's (Microsoft.Testing.Platform.ServerMode.Client.Sources) RunTestsAsync call emits a $/cancelRequest notification, but nothing proved the cancellation actually reaches a running MSTest test through TestContext.CancellationToken, or that the server/client shut down cleanly afterwards. This adds that missing end-to-end coverage.

What this does

MtpServerClientCancellationAcceptanceTests launches a real MSTest MTP application in server mode via MtpServerClient and:

  • Runs a deliberately long-running test (Task.Delay(5min, TestContext.CancellationToken)).
  • Waits for an in-progress test-node update before cancelling, so the cancel is never racing against test startup (no blind sleep).
  • Cancels the client's RunTestsAsync token, which sends $/cancelRequest.
  • Asserts the executing test itself observed TestContext.CancellationToken - not just that the client-side task was cancelled or that $/cancelRequest was sent - via a marker file the test method writes from inside a catch (OperationCanceledException ex) when (ex.CancellationToken == TestContext.CancellationToken) block.
  • Asserts RunTestsAsync, ExitAsync, and client Dispose all complete within bounded waits, so a regression here fails fast instead of hanging CI.

A note on the design (no product bug found)

My first attempt asserted on a final failed/"was canceled" test-node update sent back over the wire, mirroring the existing self-cancellation tests. That doesn't work for a hard $/cancelRequest cancel: AsyncConsumerDataProcessor.ConsumeAsync intentionally stops relaying further test-node updates once a run's cancellation token fires (its own comments describe this as letting "a cooperative consumer bail out of its own work immediately"). That's different from the cooperative IGracefulStopTestExecutionCapability path (exercised by DotnetTestPipeServerCancellationTests), which does guarantee reporting survives. Since this is intentional platform behavior rather than a propagation gap, I redesigned the test's "did the test observe cancellation" proof around an out-of-band marker file instead of relying on that channel.

Verification

  • dotnet build test\IntegrationTests\MSTest.Acceptance.IntegrationTests\MSTest.Acceptance.IntegrationTests.csproj -c Release -f net11.0 - 0 errors.
  • Ran the new test directly via the built host with --filter "Name~RunTestsAsync_WhenCanceledAfterTestStarts" - passed in ~2s, 5/5 repeated runs green.
  • Sanity check: temporarily removed TestContext.CancellationToken from the test asset's delay call to confirm the test fails loudly (clear TimeoutException) instead of passing vacuously; reverted after confirming.

Add an end-to-end acceptance test proving that cancelling
Microsoft.Testing.Platform.ServerMode.Client.Sources'' RunTestsAsync propagates
all the way into a real, executing MSTest test through
TestContext.CancellationToken, and that the server/client shut down cleanly
afterwards.
The test launches a real MSTest MTP app in server mode via MtpServerClient,
waits for an in-progress test-node update to confirm the test actually
started (avoiding a timing-only race), cancels the run, and asserts the test
itself observed TestContext.CancellationToken via a marker file written from
inside a catch block keyed on that exact token instance.
A marker file is used instead of asserting on a final test-node update over
the wire: AsyncConsumerDataProcessor.ConsumeAsync intentionally stops relaying
test-node updates once a run is hard-canceled via $/cancelRequest (as opposed
to the cooperative IGracefulStopTestExecutionCapability path), so the final
canceled-test node update is not guaranteed to reach the client. That is
expected platform behavior, not a propagation gap.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
CopilotAI balanced review requested due to automatic review settings August 31, 2026 11:49

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

Review tier: Balanced
Findings: 3 Medium severity

New issues introduced by this change (3)
SeverityFinding
Medium severitytest/​IntegrationTests/​MSTest.Acceptance.IntegrationTests/​MtpServerClientCancellationAcceptanceTests.cs — This unfiltered catch also consumes the OperationCanceledException raised when testTimeoutToken
Medium severitytest/​IntegrationTests/​MSTest.Acceptance.IntegrationTests/​MtpServerClientCancellationAcceptanceTests.cs — Because timeoutSource is linked to cancellationToken, this condition is true for both the local…
Medium severitytest/​IntegrationTests/​MSTest.Acceptance.IntegrationTests/​MtpServerClientCancellationAcceptanceTests.cs — This does not verify the ShutsDownCleanly behavior: ExitAsync only sends the notification, and…
What changed in this PR

Adds E2E coverage for MTP server-mode cancellation reaching an executing MSTest test.

Changes:

  • Launches a real server-mode MSTest application.
  • Verifies cancellation through an out-of-band marker file.
  • Adds bounded waits and shutdown handling.
FileDescription
MtpServerClientCancellationAcceptanceTests.csAdds the cancellation acceptance test and generated test asset.

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

- Filter the catch around runTask.WaitAsync on runCancellation.Token so a
harness-level testTimeoutToken cancellation (a real hang) is never
misreported as the expected client-side run cancellation.
- Fix WaitForFileAsync''s catch filter to distinguish the local timeout from
outer-token cancellation, matching the existing pattern in
AcceptanceTestBase.RunWindowsApplicationModelCommandAsync.
- Add WaitForProcessExitAsync to actually verify the server process exits on
its own after ExitAsync, instead of relying on the using block''s Dispose(),
which force-kills the process if it is still alive and would let a server
that ignores ''exit'' still pass.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings August 31, 2026 12:09

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

Review tier: Balanced
Findings: None

Issues resolved since last review (3)
SeverityFinding
Medium severitytest/​IntegrationTests/​MSTest.Acceptance.IntegrationTests/​MtpServerClientCancellationAcceptanceTests.cs — This does not verify the ShutsDownCleanly behavior: ExitAsync only sends the notification, and… View resolved comment
Medium severitytest/​IntegrationTests/​MSTest.Acceptance.IntegrationTests/​MtpServerClientCancellationAcceptanceTests.cs — Because timeoutSource is linked to cancellationToken, this condition is true for both the local… View resolved comment
Medium severitytest/​IntegrationTests/​MSTest.Acceptance.IntegrationTests/​MtpServerClientCancellationAcceptanceTests.cs — This unfiltered catch also consumes the OperationCanceledException raised when testTimeoutTokenView resolved comment
Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

test/IntegrationTests/MSTest.Acceptance.IntegrationTests/MtpServerClientCancellationAcceptanceTests.cs:258

  • File.WriteAllText creates the file before its buffered contents are flushed and closed, while the parent stops waiting as soon as File.Exists is true. The parent can therefore read an empty/partial marker and fail intermittently even though cancellation propagated. Publish the marker atomically by writing a sibling temporary file and renaming it.
 File.WriteAllText(signalFilePath, $"{nameof(LongRunningCancellableTest)} observed TestContext.CancellationToken");

The parent acceptance test polls for the marker file via File.Exists, but
File.WriteAllText makes the file visible before its contents are fully
flushed, so the parent could observe an empty or partial marker under rare
timing. Write to a sibling .tmp file and atomically rename it into place
instead. Also clean up a stray .tmp file in the acceptance test''s cleanup.
Addresses a "previously missed" finding from Copilot code review.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings August 31, 2026 12:26

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

Review tier: Balanced
Findings: None

@EvangelinkAmaury Levé (Evangelink) added the state/needs-review Awaiting review from the team. label Aug 31, 2026
CI build failed on all platforms with IDE0007 (use 'var' instead of
explicit type) because the repo's .editorconfig treats
csharp_style_var_when_type_is_apparent as an error-level rule and
CancellationTokenSource.CreateLinkedTokenSource(...) is a recognized
'apparent type' factory call. Switch both local declarations to 'var'.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings August 31, 2026 12:56
@github-actions

Copy link
Copy Markdown
Contributor

🔍 Build Failure Analysis

Summary — All four Debug/Release build legs (Linux, macOS, Windows Release, Windows Debug) failed identically with the IDE0007 analyzer promoted to error ("use var instead of explicit type") at two lines in the new test file.

Root cause: IDE0007 — explicit type used where var is required

MtpServerClientCancellationAcceptanceTests.cs declares two local using variables with an explicit CancellationTokenSource type instead of var. The repo's .editorconfig enforces csharp_style_var_when_type_is_apparent/csharp_style_var_for_built_in_types as error severity, so both instances fail every leg's build with IDE0007.

Affected files / errors

Proposed fix (inline suggestions posted on both lines)

- using CancellationTokenSource timeoutSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);+ using var timeoutSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);

This matches the style already used a few lines above in the same file (using var client = MtpServerClient.Launch(...)).


Build overview
  • 4 legs analyzed with build errors: Linux Release, macOS Release, Windows Release, Windows Debug — all identical IDE0007 x2 + "Build failed."
  • 2 legs (application-model acceptance x2) reported no compile errors — unaffected/downstream of these two failing legs.
  • Failing project: test/IntegrationTests/MSTest.Acceptance.IntegrationTests/MSTest.Acceptance.IntegrationTests.csproj, target CoreCompile, task Csc.
All MSBuild errors
CodeProjectFile:LineMessage
IDE0007MSTest.Acceptance.IntegrationTestsMtpServerClientCancellationAcceptanceTests.cs:142use 'var' instead of explicit type
IDE0007MSTest.Acceptance.IntegrationTestsMtpServerClientCancellationAcceptanceTests.cs:159use 'var' instead of explicit type

🤖 Generated by the Build Failure Analysis workflow using (a href="(dev.azure.com/redacted) · commit 6fead25

🤖 Automated content by GitHub Copilot. Generated by the Build Failure Analysis workflow. · auto · 96.5 AIC · ⌖ 1.98 AIC · ⊞ 13.3K · [◷]( · )

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 Automated content by GitHub Copilot. Generated by the Build Failure Analysis workflow. · auto · 96.5 AIC · ⌖ 1.98 AIC · ⊞ 13.3K ·

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

Review tier: Balanced
Findings: None

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

test/IntegrationTests/MSTest.Acceptance.IntegrationTests/MtpServerClientCancellationAcceptanceTests.cs:87

  • The in-progress update does not prove that the test method has started. MSTest publishes it before calling RunSingleTestAsync (TestExecutionManager.Runner.cs:113,141), and that call still creates contexts/runs initializers before invoking the method (UnitTestRunner.RunSingleTest.cs:132-238). Cancellation can therefore happen first; the method can later enter with an already-canceled TestContext.CancellationToken, write the marker, and let this test pass without proving cancellation reached an executing test. Have the asset emit a separate “body started” signal immediately before Task.Delay, wait for that signal here, and only then cancel.
 // Wait until the server confirms the test is actually running before canceling. This avoids a
// timing-only race between "cancel" and "test has started" that a blind Task.Delay would risk.
await testStarted.Task.WaitAsync(WaitTimeout, testTimeoutToken);

The in-progress test-node update is published by MtpTestResultRecorder
before RunSingleTestAsync even runs TestInitialize/constructors, let alone
invokes the test method. Waiting only on that update to gate cancellation
left a window where cancellation could land before the test method body -
and its await Task.Delay(..., TestContext.CancellationToken) - had even
started, letting the test pass without proving cancellation reached
genuinely executing test code.
Have the test asset write a separate "body started" marker file
immediately as the first statement of the test method, before entering the
try/await Task.Delay. The acceptance test now waits for both that signal
and the in-progress node update before cancelling, closing the gap. Shared
the atomic write-then-rename helper between both marker files.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings August 31, 2026 13:16

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

Review tier: Balanced
Findings: None

Suppressed comments (1)

test/IntegrationTests/MSTest.Acceptance.IntegrationTests/MtpServerClientCancellationAcceptanceTests.cs:141

  • ProcessId == 0 proves only that the child terminated; MtpServerProcess.ProcessId returns 0 for every exit, including a crash or non-zero exit. Since ExitAsync only writes a notification, that write can also complete before the client detects that the peer has already died. The test could therefore pass when cancellation crashes the server after writing the marker. Capture a System.Diagnostics.Process handle from the initial PID before cancellation, await its exit after ExitAsync, and assert that ExitCode is zero.
 await client.ExitAsync(testTimeoutToken).WaitAsync(WaitTimeout, testTimeoutToken);
await WaitForProcessExitAsync(client, WaitTimeout, testTimeoutToken);

MtpServerProcess.ProcessId returns 0 for BOTH a graceful exit and a crash,
so polling it after ExitAsync could not distinguish "the server processed
exit cleanly" from "the server crashed right after writing the marker
file" - ExitAsync only writes a fire-and-forget notification, so its
completion does not prove the peer processed it before dying.
Capture a System.Diagnostics.Process handle for the launched server''s PID
immediately after Launch (while it is guaranteed alive), await its exit via
Process.WaitForExitAsync after sending ''exit'', and assert ExitCode == 0.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings August 31, 2026 13:45

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

Review tier: Balanced
Findings: None

Avoid Process.GetProcessById when asserting the server's exit code in the cancellation acceptance test. Linux does not treat that Process instance as the owner of the launched child, so ExitCode throws even after the server exits. Reuse the launched process owned by MtpServerClient instead.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings August 31, 2026 14:49

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

Review tier: Balanced
Findings: 1 High severity

New issues introduced by this change (1)
SeverityFinding
High severitysrc/​Platform/​Microsoft.Testing.Platform.ServerMode.Client.Sources/​Client/​MtpServerProcess.cs — This does not compile for the project's netstandard2.0 target:…

Microsoft.Testing.Platform.ServerMode.Client.Sources is a source-only
package that also targets netstandard2.0 for .NET Framework consumers.
ExitCode/WaitForExitAsync (added in 32f1ae4 to let the acceptance test
assert a clean server shutdown) called Process.WaitForExitAsync, a .NET
5+-only BCL method, which failed the netstandard2.0 build leg with
IDE0007/CS-level errors on Linux CI (flagged by review).
Adding the repo''s existing (but currently unpackaged) ProcessExtensions
polyfill to fix this down-level build would expand this PR''s scope beyond
a test-only change and touch the packaging of a shared, shipped library.
Per review guidance, remove the new accessors instead and keep the
acceptance test''s existing ProcessId-based polling approach (proves the
server exited naturally in response to ''exit'' without requiring
ownership-restricted ExitCode access), with a comment documenting why the
test does not assert on exit code specifically.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings August 31, 2026 15:11

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

Review tier: Balanced
Findings: 1 High severity

New issues introduced by this change (1)
SeverityFinding
High severitytest/​IntegrationTests/​MSTest.Acceptance.IntegrationTests/​MtpServerClientCancellationAcceptanceTests.csInitializeAsync is the only server/client operation here without the 60-second bound. If a…
Issues resolved since last review (1)
SeverityFinding
High severitysrc/​Platform/​Microsoft.Testing.Platform.ServerMode.Client.Sources/​Client/​MtpServerProcess.cs — This does not compile for the project's netstandard2.0 target:… View resolved comment

CopilotAI review requested due to automatic review settings September 1, 2026 07:47

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

Review tier: Balanced
Findings: 1 Medium severity

New issues introduced by this change (1)
SeverityFinding
Medium severitytest/​IntegrationTests/​MSTest.Acceptance.IntegrationTests/​MtpServerClientCancellationAcceptanceTests.csusing var invokes synchronous Dispose() only when this scope exits, without any deadline. This…
Issues resolved since last review (1)
SeverityFinding
High severitytest/​IntegrationTests/​MSTest.Acceptance.IntegrationTests/​MtpServerClientCancellationAcceptanceTests.csInitializeAsync is the only server/client operation here without the 60-second bound. If a… View resolved comment

Prevent Windows Release CI from hanging indefinitely by bounding client initialization and moving synchronous disposal to a bounded asynchronous cleanup path.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 2e0816a8-bfba-488c-abb0-1517c2b4cc7b
CopilotAI review requested due to automatic review settings September 1, 2026 13:15

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

Review tier: Balanced
Findings: None

Issues resolved since last review (1)
SeverityFinding
Medium severitytest/​IntegrationTests/​MSTest.Acceptance.IntegrationTests/​MtpServerClientCancellationAcceptanceTests.csusing var invokes synchronous Dispose() only when this scope exits, without any deadline. This… View resolved comment

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

state/needs-reviewAwaiting review from the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@Evangelink@0101
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Add MTP server-mode cancellation E2E acceptance test by Evangelink · Pull Request #10888 · microsoft/testfx · GitHub
Skip to content

Add MTP server-mode cancellation E2E acceptance test - #10888

Open
Amaury Levé (Evangelink) wants to merge 10 commits into
microsoft:mainfrom
Evangelink:dev/amauryleve/mtp-cancellation-e2e
Open

Add MTP server-mode cancellation E2E acceptance test#10888
Amaury Levé (Evangelink) wants to merge 10 commits into
microsoft:mainfrom
Evangelink:dev/amauryleve/mtp-cancellation-e2e

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Why

Unit tests already prove that cancelling the source-only server-mode client's (Microsoft.Testing.Platform.ServerMode.Client.Sources) RunTestsAsync call emits a $/cancelRequest notification, but nothing proved the cancellation actually reaches a running MSTest test through TestContext.CancellationToken, or that the server/client shut down cleanly afterwards. This adds that missing end-to-end coverage.

What this does

MtpServerClientCancellationAcceptanceTests launches a real MSTest MTP application in server mode via MtpServerClient and:

  • Runs a deliberately long-running test (Task.Delay(5min, TestContext.CancellationToken)).
  • Waits for an in-progress test-node update before cancelling, so the cancel is never racing against test startup (no blind sleep).
  • Cancels the client's RunTestsAsync token, which sends $/cancelRequest.
  • Asserts the executing test itself observed TestContext.CancellationToken - not just that the client-side task was cancelled or that $/cancelRequest was sent - via a marker file the test method writes from inside a catch (OperationCanceledException ex) when (ex.CancellationToken == TestContext.CancellationToken) block.
  • Asserts RunTestsAsync, ExitAsync, and client Dispose all complete within bounded waits, so a regression here fails fast instead of hanging CI.

A note on the design (no product bug found)

My first attempt asserted on a final failed/"was canceled" test-node update sent back over the wire, mirroring the existing self-cancellation tests. That doesn't work for a hard $/cancelRequest cancel: AsyncConsumerDataProcessor.ConsumeAsync intentionally stops relaying further test-node updates once a run's cancellation token fires (its own comments describe this as letting "a cooperative consumer bail out of its own work immediately"). That's different from the cooperative IGracefulStopTestExecutionCapability path (exercised by DotnetTestPipeServerCancellationTests), which does guarantee reporting survives. Since this is intentional platform behavior rather than a propagation gap, I redesigned the test's "did the test observe cancellation" proof around an out-of-band marker file instead of relying on that channel.

Verification

  • dotnet build test\IntegrationTests\MSTest.Acceptance.IntegrationTests\MSTest.Acceptance.IntegrationTests.csproj -c Release -f net11.0 - 0 errors.
  • Ran the new test directly via the built host with --filter "Name~RunTestsAsync_WhenCanceledAfterTestStarts" - passed in ~2s, 5/5 repeated runs green.
  • Sanity check: temporarily removed TestContext.CancellationToken from the test asset's delay call to confirm the test fails loudly (clear TimeoutException) instead of passing vacuously; reverted after confirming.

Add an end-to-end acceptance test proving that cancelling
Microsoft.Testing.Platform.ServerMode.Client.Sources'' RunTestsAsync propagates
all the way into a real, executing MSTest test through
TestContext.CancellationToken, and that the server/client shut down cleanly
afterwards.
The test launches a real MSTest MTP app in server mode via MtpServerClient,
waits for an in-progress test-node update to confirm the test actually
started (avoiding a timing-only race), cancels the run, and asserts the test
itself observed TestContext.CancellationToken via a marker file written from
inside a catch block keyed on that exact token instance.
A marker file is used instead of asserting on a final test-node update over
the wire: AsyncConsumerDataProcessor.ConsumeAsync intentionally stops relaying
test-node updates once a run is hard-canceled via $/cancelRequest (as opposed
to the cooperative IGracefulStopTestExecutionCapability path), so the final
canceled-test node update is not guaranteed to reach the client. That is
expected platform behavior, not a propagation gap.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
CopilotAI balanced review requested due to automatic review settings August 31, 2026 11:49

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

Review tier: Balanced
Findings: 3 Medium severity

New issues introduced by this change (3)
SeverityFinding
Medium severitytest/​IntegrationTests/​MSTest.Acceptance.IntegrationTests/​MtpServerClientCancellationAcceptanceTests.cs — This unfiltered catch also consumes the OperationCanceledException raised when testTimeoutToken
Medium severitytest/​IntegrationTests/​MSTest.Acceptance.IntegrationTests/​MtpServerClientCancellationAcceptanceTests.cs — Because timeoutSource is linked to cancellationToken, this condition is true for both the local…
Medium severitytest/​IntegrationTests/​MSTest.Acceptance.IntegrationTests/​MtpServerClientCancellationAcceptanceTests.cs — This does not verify the ShutsDownCleanly behavior: ExitAsync only sends the notification, and…
What changed in this PR

Adds E2E coverage for MTP server-mode cancellation reaching an executing MSTest test.

Changes:

  • Launches a real server-mode MSTest application.
  • Verifies cancellation through an out-of-band marker file.
  • Adds bounded waits and shutdown handling.
FileDescription
MtpServerClientCancellationAcceptanceTests.csAdds the cancellation acceptance test and generated test asset.

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

- Filter the catch around runTask.WaitAsync on runCancellation.Token so a
harness-level testTimeoutToken cancellation (a real hang) is never
misreported as the expected client-side run cancellation.
- Fix WaitForFileAsync''s catch filter to distinguish the local timeout from
outer-token cancellation, matching the existing pattern in
AcceptanceTestBase.RunWindowsApplicationModelCommandAsync.
- Add WaitForProcessExitAsync to actually verify the server process exits on
its own after ExitAsync, instead of relying on the using block''s Dispose(),
which force-kills the process if it is still alive and would let a server
that ignores ''exit'' still pass.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings August 31, 2026 12:09

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

Review tier: Balanced
Findings: None

Issues resolved since last review (3)
SeverityFinding
Medium severitytest/​IntegrationTests/​MSTest.Acceptance.IntegrationTests/​MtpServerClientCancellationAcceptanceTests.cs — This does not verify the ShutsDownCleanly behavior: ExitAsync only sends the notification, and… View resolved comment
Medium severitytest/​IntegrationTests/​MSTest.Acceptance.IntegrationTests/​MtpServerClientCancellationAcceptanceTests.cs — Because timeoutSource is linked to cancellationToken, this condition is true for both the local… View resolved comment
Medium severitytest/​IntegrationTests/​MSTest.Acceptance.IntegrationTests/​MtpServerClientCancellationAcceptanceTests.cs — This unfiltered catch also consumes the OperationCanceledException raised when testTimeoutTokenView resolved comment
Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

test/IntegrationTests/MSTest.Acceptance.IntegrationTests/MtpServerClientCancellationAcceptanceTests.cs:258

  • File.WriteAllText creates the file before its buffered contents are flushed and closed, while the parent stops waiting as soon as File.Exists is true. The parent can therefore read an empty/partial marker and fail intermittently even though cancellation propagated. Publish the marker atomically by writing a sibling temporary file and renaming it.
 File.WriteAllText(signalFilePath, $"{nameof(LongRunningCancellableTest)} observed TestContext.CancellationToken");

The parent acceptance test polls for the marker file via File.Exists, but
File.WriteAllText makes the file visible before its contents are fully
flushed, so the parent could observe an empty or partial marker under rare
timing. Write to a sibling .tmp file and atomically rename it into place
instead. Also clean up a stray .tmp file in the acceptance test''s cleanup.
Addresses a "previously missed" finding from Copilot code review.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings August 31, 2026 12:26

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

Review tier: Balanced
Findings: None

@EvangelinkAmaury Levé (Evangelink) added the state/needs-review Awaiting review from the team. label Aug 31, 2026
CI build failed on all platforms with IDE0007 (use 'var' instead of
explicit type) because the repo's .editorconfig treats
csharp_style_var_when_type_is_apparent as an error-level rule and
CancellationTokenSource.CreateLinkedTokenSource(...) is a recognized
'apparent type' factory call. Switch both local declarations to 'var'.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings August 31, 2026 12:56
@github-actions

Copy link
Copy Markdown
Contributor

🔍 Build Failure Analysis

Summary — All four Debug/Release build legs (Linux, macOS, Windows Release, Windows Debug) failed identically with the IDE0007 analyzer promoted to error ("use var instead of explicit type") at two lines in the new test file.

Root cause: IDE0007 — explicit type used where var is required

MtpServerClientCancellationAcceptanceTests.cs declares two local using variables with an explicit CancellationTokenSource type instead of var. The repo's .editorconfig enforces csharp_style_var_when_type_is_apparent/csharp_style_var_for_built_in_types as error severity, so both instances fail every leg's build with IDE0007.

Affected files / errors

Proposed fix (inline suggestions posted on both lines)

- using CancellationTokenSource timeoutSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);+ using var timeoutSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);

This matches the style already used a few lines above in the same file (using var client = MtpServerClient.Launch(...)).


Build overview
  • 4 legs analyzed with build errors: Linux Release, macOS Release, Windows Release, Windows Debug — all identical IDE0007 x2 + "Build failed."
  • 2 legs (application-model acceptance x2) reported no compile errors — unaffected/downstream of these two failing legs.
  • Failing project: test/IntegrationTests/MSTest.Acceptance.IntegrationTests/MSTest.Acceptance.IntegrationTests.csproj, target CoreCompile, task Csc.
All MSBuild errors
CodeProjectFile:LineMessage
IDE0007MSTest.Acceptance.IntegrationTestsMtpServerClientCancellationAcceptanceTests.cs:142use 'var' instead of explicit type
IDE0007MSTest.Acceptance.IntegrationTestsMtpServerClientCancellationAcceptanceTests.cs:159use 'var' instead of explicit type

🤖 Generated by the Build Failure Analysis workflow using (a href="(dev.azure.com/redacted) · commit 6fead25

🤖 Automated content by GitHub Copilot. Generated by the Build Failure Analysis workflow. · auto · 96.5 AIC · ⌖ 1.98 AIC · ⊞ 13.3K · [◷]( · )

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 Automated content by GitHub Copilot. Generated by the Build Failure Analysis workflow. · auto · 96.5 AIC · ⌖ 1.98 AIC · ⊞ 13.3K ·

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

Review tier: Balanced
Findings: None

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

test/IntegrationTests/MSTest.Acceptance.IntegrationTests/MtpServerClientCancellationAcceptanceTests.cs:87

  • The in-progress update does not prove that the test method has started. MSTest publishes it before calling RunSingleTestAsync (TestExecutionManager.Runner.cs:113,141), and that call still creates contexts/runs initializers before invoking the method (UnitTestRunner.RunSingleTest.cs:132-238). Cancellation can therefore happen first; the method can later enter with an already-canceled TestContext.CancellationToken, write the marker, and let this test pass without proving cancellation reached an executing test. Have the asset emit a separate “body started” signal immediately before Task.Delay, wait for that signal here, and only then cancel.
 // Wait until the server confirms the test is actually running before canceling. This avoids a
// timing-only race between "cancel" and "test has started" that a blind Task.Delay would risk.
await testStarted.Task.WaitAsync(WaitTimeout, testTimeoutToken);

The in-progress test-node update is published by MtpTestResultRecorder
before RunSingleTestAsync even runs TestInitialize/constructors, let alone
invokes the test method. Waiting only on that update to gate cancellation
left a window where cancellation could land before the test method body -
and its await Task.Delay(..., TestContext.CancellationToken) - had even
started, letting the test pass without proving cancellation reached
genuinely executing test code.
Have the test asset write a separate "body started" marker file
immediately as the first statement of the test method, before entering the
try/await Task.Delay. The acceptance test now waits for both that signal
and the in-progress node update before cancelling, closing the gap. Shared
the atomic write-then-rename helper between both marker files.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings August 31, 2026 13:16

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

Review tier: Balanced
Findings: None

Suppressed comments (1)

test/IntegrationTests/MSTest.Acceptance.IntegrationTests/MtpServerClientCancellationAcceptanceTests.cs:141

  • ProcessId == 0 proves only that the child terminated; MtpServerProcess.ProcessId returns 0 for every exit, including a crash or non-zero exit. Since ExitAsync only writes a notification, that write can also complete before the client detects that the peer has already died. The test could therefore pass when cancellation crashes the server after writing the marker. Capture a System.Diagnostics.Process handle from the initial PID before cancellation, await its exit after ExitAsync, and assert that ExitCode is zero.
 await client.ExitAsync(testTimeoutToken).WaitAsync(WaitTimeout, testTimeoutToken);
await WaitForProcessExitAsync(client, WaitTimeout, testTimeoutToken);

MtpServerProcess.ProcessId returns 0 for BOTH a graceful exit and a crash,
so polling it after ExitAsync could not distinguish "the server processed
exit cleanly" from "the server crashed right after writing the marker
file" - ExitAsync only writes a fire-and-forget notification, so its
completion does not prove the peer processed it before dying.
Capture a System.Diagnostics.Process handle for the launched server''s PID
immediately after Launch (while it is guaranteed alive), await its exit via
Process.WaitForExitAsync after sending ''exit'', and assert ExitCode == 0.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings August 31, 2026 13:45

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

Review tier: Balanced
Findings: None

Avoid Process.GetProcessById when asserting the server's exit code in the cancellation acceptance test. Linux does not treat that Process instance as the owner of the launched child, so ExitCode throws even after the server exits. Reuse the launched process owned by MtpServerClient instead.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings August 31, 2026 14:49

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

Review tier: Balanced
Findings: 1 High severity

New issues introduced by this change (1)
SeverityFinding
High severitysrc/​Platform/​Microsoft.Testing.Platform.ServerMode.Client.Sources/​Client/​MtpServerProcess.cs — This does not compile for the project's netstandard2.0 target:…

Microsoft.Testing.Platform.ServerMode.Client.Sources is a source-only
package that also targets netstandard2.0 for .NET Framework consumers.
ExitCode/WaitForExitAsync (added in 32f1ae4 to let the acceptance test
assert a clean server shutdown) called Process.WaitForExitAsync, a .NET
5+-only BCL method, which failed the netstandard2.0 build leg with
IDE0007/CS-level errors on Linux CI (flagged by review).
Adding the repo''s existing (but currently unpackaged) ProcessExtensions
polyfill to fix this down-level build would expand this PR''s scope beyond
a test-only change and touch the packaging of a shared, shipped library.
Per review guidance, remove the new accessors instead and keep the
acceptance test''s existing ProcessId-based polling approach (proves the
server exited naturally in response to ''exit'' without requiring
ownership-restricted ExitCode access), with a comment documenting why the
test does not assert on exit code specifically.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings August 31, 2026 15:11

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

Review tier: Balanced
Findings: 1 High severity

New issues introduced by this change (1)
SeverityFinding
High severitytest/​IntegrationTests/​MSTest.Acceptance.IntegrationTests/​MtpServerClientCancellationAcceptanceTests.csInitializeAsync is the only server/client operation here without the 60-second bound. If a…
Issues resolved since last review (1)
SeverityFinding
High severitysrc/​Platform/​Microsoft.Testing.Platform.ServerMode.Client.Sources/​Client/​MtpServerProcess.cs — This does not compile for the project's netstandard2.0 target:… View resolved comment

CopilotAI review requested due to automatic review settings September 1, 2026 07:47

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

Review tier: Balanced
Findings: 1 Medium severity

New issues introduced by this change (1)
SeverityFinding
Medium severitytest/​IntegrationTests/​MSTest.Acceptance.IntegrationTests/​MtpServerClientCancellationAcceptanceTests.csusing var invokes synchronous Dispose() only when this scope exits, without any deadline. This…
Issues resolved since last review (1)
SeverityFinding
High severitytest/​IntegrationTests/​MSTest.Acceptance.IntegrationTests/​MtpServerClientCancellationAcceptanceTests.csInitializeAsync is the only server/client operation here without the 60-second bound. If a… View resolved comment

Prevent Windows Release CI from hanging indefinitely by bounding client initialization and moving synchronous disposal to a bounded asynchronous cleanup path.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 2e0816a8-bfba-488c-abb0-1517c2b4cc7b
CopilotAI review requested due to automatic review settings September 1, 2026 13:15

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

Review tier: Balanced
Findings: None

Issues resolved since last review (1)
SeverityFinding
Medium severitytest/​IntegrationTests/​MSTest.Acceptance.IntegrationTests/​MtpServerClientCancellationAcceptanceTests.csusing var invokes synchronous Dispose() only when this scope exits, without any deadline. This… View resolved comment

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

state/needs-reviewAwaiting review from the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@Evangelink@0101
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); Add MTP server-mode cancellation E2E acceptance test by Evangelink · Pull Request #10888 · microsoft/testfx · GitHub
Skip to content

Add MTP server-mode cancellation E2E acceptance test - #10888

Open
Amaury Levé (Evangelink) wants to merge 10 commits into
microsoft:mainfrom
Evangelink:dev/amauryleve/mtp-cancellation-e2e
Open

Add MTP server-mode cancellation E2E acceptance test#10888
Amaury Levé (Evangelink) wants to merge 10 commits into
microsoft:mainfrom
Evangelink:dev/amauryleve/mtp-cancellation-e2e

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Why

Unit tests already prove that cancelling the source-only server-mode client's (Microsoft.Testing.Platform.ServerMode.Client.Sources) RunTestsAsync call emits a $/cancelRequest notification, but nothing proved the cancellation actually reaches a running MSTest test through TestContext.CancellationToken, or that the server/client shut down cleanly afterwards. This adds that missing end-to-end coverage.

What this does

MtpServerClientCancellationAcceptanceTests launches a real MSTest MTP application in server mode via MtpServerClient and:

  • Runs a deliberately long-running test (Task.Delay(5min, TestContext.CancellationToken)).
  • Waits for an in-progress test-node update before cancelling, so the cancel is never racing against test startup (no blind sleep).
  • Cancels the client's RunTestsAsync token, which sends $/cancelRequest.
  • Asserts the executing test itself observed TestContext.CancellationToken - not just that the client-side task was cancelled or that $/cancelRequest was sent - via a marker file the test method writes from inside a catch (OperationCanceledException ex) when (ex.CancellationToken == TestContext.CancellationToken) block.
  • Asserts RunTestsAsync, ExitAsync, and client Dispose all complete within bounded waits, so a regression here fails fast instead of hanging CI.

A note on the design (no product bug found)

My first attempt asserted on a final failed/"was canceled" test-node update sent back over the wire, mirroring the existing self-cancellation tests. That doesn't work for a hard $/cancelRequest cancel: AsyncConsumerDataProcessor.ConsumeAsync intentionally stops relaying further test-node updates once a run's cancellation token fires (its own comments describe this as letting "a cooperative consumer bail out of its own work immediately"). That's different from the cooperative IGracefulStopTestExecutionCapability path (exercised by DotnetTestPipeServerCancellationTests), which does guarantee reporting survives. Since this is intentional platform behavior rather than a propagation gap, I redesigned the test's "did the test observe cancellation" proof around an out-of-band marker file instead of relying on that channel.

Verification

  • dotnet build test\IntegrationTests\MSTest.Acceptance.IntegrationTests\MSTest.Acceptance.IntegrationTests.csproj -c Release -f net11.0 - 0 errors.
  • Ran the new test directly via the built host with --filter "Name~RunTestsAsync_WhenCanceledAfterTestStarts" - passed in ~2s, 5/5 repeated runs green.
  • Sanity check: temporarily removed TestContext.CancellationToken from the test asset's delay call to confirm the test fails loudly (clear TimeoutException) instead of passing vacuously; reverted after confirming.

Add an end-to-end acceptance test proving that cancelling
Microsoft.Testing.Platform.ServerMode.Client.Sources'' RunTestsAsync propagates
all the way into a real, executing MSTest test through
TestContext.CancellationToken, and that the server/client shut down cleanly
afterwards.
The test launches a real MSTest MTP app in server mode via MtpServerClient,
waits for an in-progress test-node update to confirm the test actually
started (avoiding a timing-only race), cancels the run, and asserts the test
itself observed TestContext.CancellationToken via a marker file written from
inside a catch block keyed on that exact token instance.
A marker file is used instead of asserting on a final test-node update over
the wire: AsyncConsumerDataProcessor.ConsumeAsync intentionally stops relaying
test-node updates once a run is hard-canceled via $/cancelRequest (as opposed
to the cooperative IGracefulStopTestExecutionCapability path), so the final
canceled-test node update is not guaranteed to reach the client. That is
expected platform behavior, not a propagation gap.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
CopilotAI balanced review requested due to automatic review settings August 31, 2026 11:49

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

Review tier: Balanced
Findings: 3 Medium severity

New issues introduced by this change (3)
SeverityFinding
Medium severitytest/​IntegrationTests/​MSTest.Acceptance.IntegrationTests/​MtpServerClientCancellationAcceptanceTests.cs — This unfiltered catch also consumes the OperationCanceledException raised when testTimeoutToken
Medium severitytest/​IntegrationTests/​MSTest.Acceptance.IntegrationTests/​MtpServerClientCancellationAcceptanceTests.cs — Because timeoutSource is linked to cancellationToken, this condition is true for both the local…
Medium severitytest/​IntegrationTests/​MSTest.Acceptance.IntegrationTests/​MtpServerClientCancellationAcceptanceTests.cs — This does not verify the ShutsDownCleanly behavior: ExitAsync only sends the notification, and…
What changed in this PR

Adds E2E coverage for MTP server-mode cancellation reaching an executing MSTest test.

Changes:

  • Launches a real server-mode MSTest application.
  • Verifies cancellation through an out-of-band marker file.
  • Adds bounded waits and shutdown handling.
FileDescription
MtpServerClientCancellationAcceptanceTests.csAdds the cancellation acceptance test and generated test asset.

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

- Filter the catch around runTask.WaitAsync on runCancellation.Token so a
harness-level testTimeoutToken cancellation (a real hang) is never
misreported as the expected client-side run cancellation.
- Fix WaitForFileAsync''s catch filter to distinguish the local timeout from
outer-token cancellation, matching the existing pattern in
AcceptanceTestBase.RunWindowsApplicationModelCommandAsync.
- Add WaitForProcessExitAsync to actually verify the server process exits on
its own after ExitAsync, instead of relying on the using block''s Dispose(),
which force-kills the process if it is still alive and would let a server
that ignores ''exit'' still pass.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings August 31, 2026 12:09

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

Review tier: Balanced
Findings: None

Issues resolved since last review (3)
SeverityFinding
Medium severitytest/​IntegrationTests/​MSTest.Acceptance.IntegrationTests/​MtpServerClientCancellationAcceptanceTests.cs — This does not verify the ShutsDownCleanly behavior: ExitAsync only sends the notification, and… View resolved comment
Medium severitytest/​IntegrationTests/​MSTest.Acceptance.IntegrationTests/​MtpServerClientCancellationAcceptanceTests.cs — Because timeoutSource is linked to cancellationToken, this condition is true for both the local… View resolved comment
Medium severitytest/​IntegrationTests/​MSTest.Acceptance.IntegrationTests/​MtpServerClientCancellationAcceptanceTests.cs — This unfiltered catch also consumes the OperationCanceledException raised when testTimeoutTokenView resolved comment
Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

test/IntegrationTests/MSTest.Acceptance.IntegrationTests/MtpServerClientCancellationAcceptanceTests.cs:258

  • File.WriteAllText creates the file before its buffered contents are flushed and closed, while the parent stops waiting as soon as File.Exists is true. The parent can therefore read an empty/partial marker and fail intermittently even though cancellation propagated. Publish the marker atomically by writing a sibling temporary file and renaming it.
 File.WriteAllText(signalFilePath, $"{nameof(LongRunningCancellableTest)} observed TestContext.CancellationToken");

The parent acceptance test polls for the marker file via File.Exists, but
File.WriteAllText makes the file visible before its contents are fully
flushed, so the parent could observe an empty or partial marker under rare
timing. Write to a sibling .tmp file and atomically rename it into place
instead. Also clean up a stray .tmp file in the acceptance test''s cleanup.
Addresses a "previously missed" finding from Copilot code review.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings August 31, 2026 12:26

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

Review tier: Balanced
Findings: None

@EvangelinkAmaury Levé (Evangelink) added the state/needs-review Awaiting review from the team. label Aug 31, 2026
CI build failed on all platforms with IDE0007 (use 'var' instead of
explicit type) because the repo's .editorconfig treats
csharp_style_var_when_type_is_apparent as an error-level rule and
CancellationTokenSource.CreateLinkedTokenSource(...) is a recognized
'apparent type' factory call. Switch both local declarations to 'var'.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings August 31, 2026 12:56
@github-actions

Copy link
Copy Markdown
Contributor

🔍 Build Failure Analysis

Summary — All four Debug/Release build legs (Linux, macOS, Windows Release, Windows Debug) failed identically with the IDE0007 analyzer promoted to error ("use var instead of explicit type") at two lines in the new test file.

Root cause: IDE0007 — explicit type used where var is required

MtpServerClientCancellationAcceptanceTests.cs declares two local using variables with an explicit CancellationTokenSource type instead of var. The repo's .editorconfig enforces csharp_style_var_when_type_is_apparent/csharp_style_var_for_built_in_types as error severity, so both instances fail every leg's build with IDE0007.

Affected files / errors

Proposed fix (inline suggestions posted on both lines)

- using CancellationTokenSource timeoutSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);+ using var timeoutSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);

This matches the style already used a few lines above in the same file (using var client = MtpServerClient.Launch(...)).


Build overview
  • 4 legs analyzed with build errors: Linux Release, macOS Release, Windows Release, Windows Debug — all identical IDE0007 x2 + "Build failed."
  • 2 legs (application-model acceptance x2) reported no compile errors — unaffected/downstream of these two failing legs.
  • Failing project: test/IntegrationTests/MSTest.Acceptance.IntegrationTests/MSTest.Acceptance.IntegrationTests.csproj, target CoreCompile, task Csc.
All MSBuild errors
CodeProjectFile:LineMessage
IDE0007MSTest.Acceptance.IntegrationTestsMtpServerClientCancellationAcceptanceTests.cs:142use 'var' instead of explicit type
IDE0007MSTest.Acceptance.IntegrationTestsMtpServerClientCancellationAcceptanceTests.cs:159use 'var' instead of explicit type

🤖 Generated by the Build Failure Analysis workflow using (a href="(dev.azure.com/redacted) · commit 6fead25

🤖 Automated content by GitHub Copilot. Generated by the Build Failure Analysis workflow. · auto · 96.5 AIC · ⌖ 1.98 AIC · ⊞ 13.3K · [◷]( · )

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 Automated content by GitHub Copilot. Generated by the Build Failure Analysis workflow. · auto · 96.5 AIC · ⌖ 1.98 AIC · ⊞ 13.3K ·

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

Review tier: Balanced
Findings: None

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

test/IntegrationTests/MSTest.Acceptance.IntegrationTests/MtpServerClientCancellationAcceptanceTests.cs:87

  • The in-progress update does not prove that the test method has started. MSTest publishes it before calling RunSingleTestAsync (TestExecutionManager.Runner.cs:113,141), and that call still creates contexts/runs initializers before invoking the method (UnitTestRunner.RunSingleTest.cs:132-238). Cancellation can therefore happen first; the method can later enter with an already-canceled TestContext.CancellationToken, write the marker, and let this test pass without proving cancellation reached an executing test. Have the asset emit a separate “body started” signal immediately before Task.Delay, wait for that signal here, and only then cancel.
 // Wait until the server confirms the test is actually running before canceling. This avoids a
// timing-only race between "cancel" and "test has started" that a blind Task.Delay would risk.
await testStarted.Task.WaitAsync(WaitTimeout, testTimeoutToken);

The in-progress test-node update is published by MtpTestResultRecorder
before RunSingleTestAsync even runs TestInitialize/constructors, let alone
invokes the test method. Waiting only on that update to gate cancellation
left a window where cancellation could land before the test method body -
and its await Task.Delay(..., TestContext.CancellationToken) - had even
started, letting the test pass without proving cancellation reached
genuinely executing test code.
Have the test asset write a separate "body started" marker file
immediately as the first statement of the test method, before entering the
try/await Task.Delay. The acceptance test now waits for both that signal
and the in-progress node update before cancelling, closing the gap. Shared
the atomic write-then-rename helper between both marker files.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings August 31, 2026 13:16

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

Review tier: Balanced
Findings: None

Suppressed comments (1)

test/IntegrationTests/MSTest.Acceptance.IntegrationTests/MtpServerClientCancellationAcceptanceTests.cs:141

  • ProcessId == 0 proves only that the child terminated; MtpServerProcess.ProcessId returns 0 for every exit, including a crash or non-zero exit. Since ExitAsync only writes a notification, that write can also complete before the client detects that the peer has already died. The test could therefore pass when cancellation crashes the server after writing the marker. Capture a System.Diagnostics.Process handle from the initial PID before cancellation, await its exit after ExitAsync, and assert that ExitCode is zero.
 await client.ExitAsync(testTimeoutToken).WaitAsync(WaitTimeout, testTimeoutToken);
await WaitForProcessExitAsync(client, WaitTimeout, testTimeoutToken);

MtpServerProcess.ProcessId returns 0 for BOTH a graceful exit and a crash,
so polling it after ExitAsync could not distinguish "the server processed
exit cleanly" from "the server crashed right after writing the marker
file" - ExitAsync only writes a fire-and-forget notification, so its
completion does not prove the peer processed it before dying.
Capture a System.Diagnostics.Process handle for the launched server''s PID
immediately after Launch (while it is guaranteed alive), await its exit via
Process.WaitForExitAsync after sending ''exit'', and assert ExitCode == 0.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings August 31, 2026 13:45

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

Review tier: Balanced
Findings: None

Avoid Process.GetProcessById when asserting the server's exit code in the cancellation acceptance test. Linux does not treat that Process instance as the owner of the launched child, so ExitCode throws even after the server exits. Reuse the launched process owned by MtpServerClient instead.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings August 31, 2026 14:49

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

Review tier: Balanced
Findings: 1 High severity

New issues introduced by this change (1)
SeverityFinding
High severitysrc/​Platform/​Microsoft.Testing.Platform.ServerMode.Client.Sources/​Client/​MtpServerProcess.cs — This does not compile for the project's netstandard2.0 target:…

Microsoft.Testing.Platform.ServerMode.Client.Sources is a source-only
package that also targets netstandard2.0 for .NET Framework consumers.
ExitCode/WaitForExitAsync (added in 32f1ae4 to let the acceptance test
assert a clean server shutdown) called Process.WaitForExitAsync, a .NET
5+-only BCL method, which failed the netstandard2.0 build leg with
IDE0007/CS-level errors on Linux CI (flagged by review).
Adding the repo''s existing (but currently unpackaged) ProcessExtensions
polyfill to fix this down-level build would expand this PR''s scope beyond
a test-only change and touch the packaging of a shared, shipped library.
Per review guidance, remove the new accessors instead and keep the
acceptance test''s existing ProcessId-based polling approach (proves the
server exited naturally in response to ''exit'' without requiring
ownership-restricted ExitCode access), with a comment documenting why the
test does not assert on exit code specifically.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings August 31, 2026 15:11

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

Review tier: Balanced
Findings: 1 High severity

New issues introduced by this change (1)
SeverityFinding
High severitytest/​IntegrationTests/​MSTest.Acceptance.IntegrationTests/​MtpServerClientCancellationAcceptanceTests.csInitializeAsync is the only server/client operation here without the 60-second bound. If a…
Issues resolved since last review (1)
SeverityFinding
High severitysrc/​Platform/​Microsoft.Testing.Platform.ServerMode.Client.Sources/​Client/​MtpServerProcess.cs — This does not compile for the project's netstandard2.0 target:… View resolved comment

CopilotAI review requested due to automatic review settings September 1, 2026 07:47

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

Review tier: Balanced
Findings: 1 Medium severity

New issues introduced by this change (1)
SeverityFinding
Medium severitytest/​IntegrationTests/​MSTest.Acceptance.IntegrationTests/​MtpServerClientCancellationAcceptanceTests.csusing var invokes synchronous Dispose() only when this scope exits, without any deadline. This…
Issues resolved since last review (1)
SeverityFinding
High severitytest/​IntegrationTests/​MSTest.Acceptance.IntegrationTests/​MtpServerClientCancellationAcceptanceTests.csInitializeAsync is the only server/client operation here without the 60-second bound. If a… View resolved comment

Prevent Windows Release CI from hanging indefinitely by bounding client initialization and moving synchronous disposal to a bounded asynchronous cleanup path.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 2e0816a8-bfba-488c-abb0-1517c2b4cc7b
CopilotAI review requested due to automatic review settings September 1, 2026 13:15

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

Review tier: Balanced
Findings: None

Issues resolved since last review (1)
SeverityFinding
Medium severitytest/​IntegrationTests/​MSTest.Acceptance.IntegrationTests/​MtpServerClientCancellationAcceptanceTests.csusing var invokes synchronous Dispose() only when this scope exits, without any deadline. This… View resolved comment

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

state/needs-reviewAwaiting review from the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@Evangelink@0101