Skip to content

Fix the two flaky tests taking down build-and-package - #751

Merged
Nikola Metulev (nmetulev) merged 1 commit into
mainfrom
azchohfi-fix-flaky-ci-tests
Aug 14, 2026
Merged

Fix the two flaky tests taking down build-and-package#751
Nikola Metulev (nmetulev) merged 1 commit into
mainfrom
azchohfi-fix-flaky-ci-tests

Conversation

@azchohfi

Copy link
Copy Markdown
Contributor

Problem

The last ten Build and Package failures came from two independent flaky tests, not one. On Aug 13 the workflow failed 13 of 33 runs (~39%).

Test Runs Failure
DotNetServiceTests.RunDotnetProcessAsync_Cancellation_KillsProcessTree 6/10 TimeoutException: descendant PID file was not written within 00:00:15
CrashDumpServiceWorkflowTests.AnalyzeDumpAsync_RealManagedDump_RunsClrMdManagedEnumeration 5/10 StringAssert.Contains — analysis log lacks CLR Version

1. Tree-kill — #732 fixed a real bug, just not this one

#732 and #737 fixed a sharing-violation IOException in WaitForPidFileAsync, and that signature is gone. The current failure is a different mode. It already appeared on Aug 12 at 17:39, before #737 merged, and 4 of the 6 failures are on commits that contain the fix — including one on main. Issue #729 is still open.

Root cause, reproduced directly: Start-Process failure is a non-terminating error, so the script ran on to Set-Content with $p still null and published a 0-byte file:

File created: YES; length=0 bytes; content=<>; int.TryParse=False

int.TryParse silently skipped it and the reader polled the full 15s. The message "was not written" was actively misleading — the file was written, just empty. The child's stderr was redirected and discarded, so the real error never reached CI. For scale, that script publishes in 373 ms on an unloaded machine, against a 15 s budget.

2. Managed dump — never fixed at all

TestProcessDump.TryCreateManagedDump dumped a PowerShell child after a blind Thread.Sleep(1500). Under load the child was still in native startup — the CI dumps show only hostfxr/hostpolicy loading coreclr — so ClrMD found no runtime. No commit had ever touched this.

Fixes

Tree-kill

  • $ErrorActionPreference = 'Stop' plus an explicit null guard, so a failed child start aborts before Set-Content rather than publishing an empty file
  • WaitForPidFileAsync now watches the launcher task and fails fast with the root's exit code and stderr instead of waiting out the clock
  • the timeout message distinguishes never-created, unreadable, and created-but-unparsable

Managed dump

  • the child allocates, forces a collection, and only then signals readiness, so the dump is taken with the runtime up and its heap walkable

On the second fix, specifically

Signalling readiness alone was not enough, and this is worth naming because the obvious fix is wrong. A token emitted from the first line of managed code proves the CLR started but not that ClrMD can walk it — that version made the dump test fail or go inconclusive on a machine where the old sleep passed 6/6. Warming the heap before signalling is what actually fixes it:

Version Idle Under 100% CPU load
Original (Thread.Sleep(1500)) 6/6 pass 6/6 pass
Token only 3/6 pass
Token after heap warm-up 8/8 pass 6/6 pass

Verification

  • Both regression guards were checked against the original code, not just the new code. TreeKillScript_WhenTheChildCannotStart_PublishesNoPidFileAtAll fails with the original script (Assert.IsFalse failed ... File.Exists(pidFile)) and passes with the hardened one.
  • The script builder is shared between the tree-kill test and its regression test, so the script proven not to publish an empty file is the one actually run — the two cannot drift.
  • Full suite: 4,572 passed / 4,581. The 4 failures are winapp init E2E tests, confirmed pre-existing by re-running them with these changes stashed — they fail on Failed to get version for Microsoft.WindowsAppSDK, the documented corp-machine nuget.org restriction.

Local load testing could not reproduce CI's 4-core I/O-bound conditions, so the load column above is weaker evidence than the idle column. The structural argument stands on its own: the old code bounded CLR startup with a fixed 1.5 s guess, and CI captured dumps proving that guess was exceeded.

Closes #729

Ten consecutive Build and Package failures came from two independent
flaky tests, not one.

RunDotnetProcessAsync_Cancellation_KillsProcessTree timed out waiting for
its PID file. Start-Process failure is a NON-terminating error, so the
root script carried on to Set-Content with $p still null, published a
0-byte file, and left the reader polling something that would never
parse -- then reported the file "was not written" about a file that had
in fact been written, empty. The child's stderr was redirected and
discarded, so the real reason never reached CI. #732 fixed a different
failure mode in this helper (a sharing violation); this timeout signature
predates that fix and still reproduces on commits containing it.

AnalyzeDumpAsync_RealManagedDump_RunsClrMdManagedEnumeration dumped a
PowerShell child after a blind 1.5s sleep. Under load the child was still
in native startup -- CI dumps show only hostfxr/hostpolicy loading
coreclr -- so ClrMD found no runtime and the "CLR Version" assertion
failed. Nothing had ever fixed this one.

Fixes:
- $ErrorActionPreference = 'Stop' plus an explicit null guard, so a failed
  child start aborts before Set-Content instead of publishing an empty file
- WaitForPidFileAsync watches the launcher task and fails fast with the
  root's exit code and stderr rather than waiting out the clock
- the timeout message now distinguishes never-created, unreadable, and
  created-but-unparsable
- the managed child allocates, collects, and only then signals readiness,
  so the dump is taken with the runtime up AND its heap walkable

Signalling readiness alone was not enough: a token emitted from the first
line of managed code made the dump test fail or go inconclusive on a
machine where the old sleep passed 6/6, which is why the child warms its
heap before signalling.

The script builder is shared with its regression test so the script proven
not to publish an empty file is the one the tree-kill test runs. Verified
by removing the fix: the guard fails with the original script and passes
with the hardened one.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: c8e8ead7-3a65-4148-b196-a4c217fde912
Copilot AI balanced review requested due to automatic review settings August 14, 2026 00:14
if (child.HasExited)
// Drain stderr so a chatty host can never fill the pipe and wedge the child.
var draining = child;
_ = Task.Run(() => { try { draining.StandardError.ReadToEnd(); } catch { /* ignore */ } });

Copilot AI 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.

Pull request overview

Hardens two flaky process-based tests in the CLI test suite.

Changes:

  • Adds deterministic managed-runtime readiness signaling before dump capture.
  • Improves PID-file failure handling and diagnostics.
  • Adds regression coverage for launcher failures and malformed PID files.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
TestProcessDump.cs Warms the managed heap and waits for readiness before dumping.
DotNetServiceTests.cs Hardens tree-kill setup, PID polling, diagnostics, and tests.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +2061 to +2069
var sw = Stopwatch.StartNew();
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
async () => await WaitForPidFileAsync(pidFile, TimeSpan.FromSeconds(30), TestContext.CancellationToken, rootTask));
sw.Stop();

StringAssert.Contains(ex.Message, "exited with code 1");
StringAssert.Contains(ex.Message, "cannot find the file", "The root's stderr is the whole point of the diagnostic.");
Assert.IsLessThan(TimeSpan.FromSeconds(5), sw.Elapsed,
"A dead root must fail fast rather than wait out the full timeout.");
var state = (everExisted, lastRead) switch
{
(false, _) => "it was never created",
(true, null) => "it was created but could never be read (the writer held it exclusively for the whole wait)",
@github-actions

Copy link
Copy Markdown
Contributor

Build Metrics Report

Binary Sizes

Artifact Baseline Current Delta
CLI (ARM64) 38.62 MB 38.62 MB ✅ 0.0 KB (0.00%)
CLI (x64) 38.74 MB 38.74 MB ✅ 0.0 KB (0.00%)
MSIX (ARM64) 16.02 MB 16.02 MB 📈 +0.2 KB (+0.00%)
MSIX (x64) 17.02 MB 17.02 MB 📉 -0.3 KB (-0.00%)
NPM Package 33.42 MB 33.42 MB 📈 +0.2 KB (+0.00%)
NuGet Package 33.46 MB 33.46 MB 📉 -0.3 KB (-0.00%)

Test Results

4576 passed, 5 skipped out of 4581 tests in 605.3s (+4 tests, -12.1s vs. baseline)

Test Coverage

89.1% line coverage, 82.4% branch coverage · ✅ no change vs. baseline

CLI Startup Time

50ms median (x64, winapp --version) · ✅ -7ms vs. baseline

Try This Build

Installs the MSIX for your architecture, replacing any previously installed build. Needs the GitHub CLI — the command offers to install it and sign you in if it is missing.

& ([scriptblock]::Create((irm https://raw.githubusercontent.com/microsoft/winappCli/main/scripts/winapp-pr.ps1))) 751
Switching between builds often?

Put the tool on your PATH once:

& ([scriptblock]::Create((irm https://raw.githubusercontent.com/microsoft/winappCli/main/scripts/winapp-pr.ps1))) -AddToPath

Then this build is just:

winapp-pr 751

Run winapp-pr with no arguments to pick from a list of open PRs.


Updated 2026-08-14 00:35:58 UTC · commit 08ed77d · workflow run

@zateutsch Zach Teutsch (zateutsch) 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.

PR review skill was all clean, looks good to me.

@nmetulev
Nikola Metulev (nmetulev) merged commit 786f49f into main Aug 14, 2026
31 checks passed
@nmetulev
Nikola Metulev (nmetulev) deleted the azchohfi-fix-flaky-ci-tests branch August 14, 2026 04:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Flaky test: RunDotnetProcessAsync_Cancellation_KillsProcessTree intermittently fails in CI

4 participants