Skip to content

Collect all crash dumps in dump directory (#4186)#8268

Open
Evangelink wants to merge 15 commits into
mainfrom
dev/evangelink/issue-4186-crashdump-collect-all-dumps
Open

Collect all crash dumps in dump directory (#4186)#8268
Evangelink wants to merge 15 commits into
mainfrom
dev/evangelink/issue-4186-crashdump-collect-all-dumps

Conversation

@Evangelink
Copy link
Copy Markdown
Member

Fixes #4186.

Problem

When the testhost crashes alongside one or more child processes, the .NET runtime writes a separate dump for each crashing process using the configured dump file name pattern (e.g. MyApp_%p_crash.dmp, where %p expands to the PID). Previously the crash-dump extension only reported the testhost's own dump (matched by PID), so dumps produced by child processes that crashed at the same time were silently dropped.

Fix

In CrashDumpProcessLifetimeHandler.OnTestHostProcessExitedAsync:

  • Convert the dump file name pattern into a wildcard search pattern by replacing every %X placeholder (%p, %e, %h, %t, …) with * (collapsing adjacent wildcards).
  • Enumerate every file in the dump directory matching that pattern and publish each as a FileArtifact. This collects both the testhost dump and any child-process dumps written during the same crash.
  • The existing *.dmp fallback (when nothing matches the pattern) is kept; the fallback path is also normalized to use CrashDumpArtifactDisplayName instead of the extension's own CrashDumpDisplayName for the artifact display name.

Tests

Added a parameterized unit test in CrashDumpTests.cs (ReplaceCrashDumpPlaceholdersWithWildcard_ConvertsPlaceholdersToWildcards) that covers single placeholder, multiple/adjacent placeholders, no placeholder, and trailing %.

Validated with:

  • dotnet build src/Platform/Microsoft.Testing.Extensions.CrashDump/Microsoft.Testing.Extensions.CrashDump.csproj -c Debug – 0 warnings, 0 errors.
  • Microsoft.Testing.Extensions.UnitTests (net8.0): all 90 tests pass, including 12 CrashDumpTests (6 new placeholder cases).

When the testhost crashes alongside one or more child processes, the .NET runtime writes a separate dump for each crashing process using the configured dump file name pattern. Previously the crash-dump extension only reported the testhost's own dump (matched by PID), losing the child-process dumps.

Convert the dump file name pattern to a wildcard search pattern by replacing every '%X' placeholder with '*' and enumerate every matching file in the dump directory, publishing each one as a FileArtifact.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings May 15, 2026 17:05
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR updates the CrashDump extension to collect and publish all crash dumps produced in the configured dump directory when the testhost crashes, including dumps generated by crashing child processes (fixing #4186).

Changes:

  • Converts the configured dump filename pattern into a wildcard pattern (replacing %X placeholders with *) and publishes all matching dump files as artifacts.
  • Keeps the existing *.dmp fallback when no pattern matches, and normalizes the artifact display name used in that fallback.
  • Adds parameterized unit coverage for placeholder-to-wildcard conversion behavior.
Show a summary per file
File Description
test/UnitTests/Microsoft.Testing.Extensions.UnitTests/CrashDumpTests.cs Adds unit tests for converting dump name placeholders into wildcard search patterns.
src/Platform/Microsoft.Testing.Extensions.CrashDump/CrashDumpProcessLifetimeHandler.cs Updates dump discovery to publish all dumps matching the configured pattern and adds the placeholder-to-wildcard helper.

Copilot's findings

  • Files reviewed: 2/2 changed files
  • Comments generated: 1

Copy link
Copy Markdown
Member Author

@Evangelink Evangelink left a comment

Choose a reason for hiding this comment

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

Review Summary

# Dimension Verdict
1 Algorithmic Correctness 🔴 1 MAJOR
13 Test Completeness & Coverage 🟡 1 NIT

✅ 19/21 dimensions clean.

  • CorrectnessPath.GetDirectoryName returns "" (not null) for bare filenames, causing Directory.Exists("")false and silently skipping all file enumeration when no directory prefix is in the pattern.
  • Test Coverage — no test covers the no-directory-path case in OnTestHostProcessExitedAsync.

Key finding

Path.GetDirectoryName("dump_%p.dmp")"" on .NET Core/5+. The null-guard passes but Directory.Exists("") is false, so neither the wildcard search nor the *.dmp fallback scan executes. Fix: default to "." when the result is null-or-empty.

Generated by Expert Code Review (on open) for issue #8268 · ● 4.9M

Comment thread test/UnitTests/Microsoft.Testing.Extensions.UnitTests/CrashDumpTests.cs Outdated
Evangelink and others added 2 commits May 15, 2026 19:45
Adds CrashDump_TesthostAndChildBothCrash_CollectsAllDumps which spawns a child process from the testhost that also crashes via FailFast, then asserts both dumps end up on disk AND are reported as out-of-process file artifacts. The artifact-output assertion is what guards the #4186 fix: prior to the fix, only the testhost's own PID-matching dump was published as an artifact.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Addresses PR review feedback:

- Switch from glob pattern (Directory.EnumerateFiles searchPattern) to regex matching so that literal glob metacharacters ('*' or '?') in user-supplied dump file names (legal on Linux/macOS) are matched literally and do not cause unrelated .dmp files to be picked up.

- Treat an empty dumpDirectory (returned by Path.GetDirectoryName for a bare filename on .NET Core/5+) as '.' (current working directory) so enumeration is not silently skipped when the configured pattern has no directory prefix.

- Replace ReplaceCrashDumpPlaceholdersWithWildcard with BuildDumpFileNameRegex / BuildDumpFileNameRegexPattern, add GetDumpDirectory helper, and update unit tests to cover literal '*'/'?' in the file name and the no-directory-component case.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings May 16, 2026 08:48
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot's findings

  • Files reviewed: 3/3 changed files
  • Comments generated: 3

Copilot AI and others added 2 commits May 16, 2026 15:04
- Snapshot pre-existing files in the dump directory before the testhost starts and exclude them when publishing crash dumps, so stale dumps from previous runs that match the same pattern are no longer surfaced as artifacts.

- Use Path.GetFileName + ordinal-ignore-case comparison instead of EndsWith for the dotnet executable detection in the CrashDump test asset.

- Add a 60-second bounded WaitForExit for the spawned child process and kill it on timeout to keep acceptance tests reliable.

- Handle empty pattern in GetDumpDirectory so Path.GetDirectoryName('') no longer throws ArgumentException on .NET Framework.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings May 16, 2026 14:27
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot's findings

  • Files reviewed: 3/3 changed files
  • Comments generated: 2

@Evangelink
Copy link
Copy Markdown
Member Author

@copilot address review comments

Co-authored-by: Evangelink <11340282+Evangelink@users.noreply.github.com>
Copilot AI review requested due to automatic review settings May 17, 2026 19:48
@Evangelink Evangelink review requested due to automatic review settings May 17, 2026 19:48
Copy link
Copy Markdown
Contributor

Copilot AI commented May 17, 2026

@copilot address review comments

Addressed in 4965f7b.

I updated the generated CrashDump acceptance test asset to dispose both Process.GetCurrentProcess() and the spawned child Process via using declarations, resolving the two remaining review comments about process handle disposal.

@Evangelink
Copy link
Copy Markdown
Member Author

@copilot address review comments

Copilot AI review requested due to automatic review settings May 18, 2026 13:19
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot's findings

  • Files reviewed: 3/3 changed files
  • Comments generated: 6

Copy link
Copy Markdown
Contributor

Copilot AI commented May 18, 2026

@copilot address review comments

Addressed already in 4965f7b.

The remaining review items were fixed there by disposing both Process.GetCurrentProcess() and the spawned child Process via using declarations in the generated CrashDump acceptance test asset.

@Evangelink
Copy link
Copy Markdown
Member Author

@copilot resolve the merge conflicts in this pull request

Co-authored-by: Evangelink <11340282+Evangelink@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

Copilot AI commented May 18, 2026

@copilot resolve the merge conflicts in this pull request

Resolved in 6c6c335.

I merged latest main into this branch and resolved the conflicts in CrashDumpProcessLifetimeHandler.cs and CrashDumpTests.cs.

Amaury Levé and others added 4 commits May 19, 2026 09:40
- Use OS-appropriate path comparer (case-sensitive on Linux/macOS, case-insensitive on Windows) for the pre-existing dump file snapshot so freshly produced dumps that differ only by casing are not skipped.

- Narrow the primary dump enumeration to files sharing the configured extension (Path.GetExtension of DumpFileNamePattern) to avoid scanning every entry of a busy results folder.

- Treat %% as a literal % in BuildDumpFileNameRegexPattern so the regex does not over-match unrelated files (matches createdump tool semantics). Added unit tests covering My%%App_%p.dmp, %%%p.dmp, %p%%.dmp, plus a regex over-match guard.

- In the integration test asset, prefer Environment.ProcessPath when available and use ProcessStartInfo.ArgumentList to avoid platform-specific quoting issues for paths containing spaces/special characters.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Track 'testhost dump produced' separately from 'any dump published'. The crash banner and the 'expected dump not found' warning are scoped to the testhost dump specifically, so publishing a dump for a crashed child no longer suppresses the warning when the testhost's own dump never materialized.

- Make _preExistingDumpFiles readonly and union into it on OnTestHostProcessStartedAsync (instead of reassigning), so multiple start callbacks (e.g. host restart) cannot drop already-classified pre-existing entries.

- Document the known limitation that two testhosts sharing the same dump directory can publish each other's dumps.

- Drop the redundant Regex.Escape('%') wrapper (% is not a regex metacharacter).

- Add unit test OnTestHostProcessExitedAsync_OnlyPublishesDumpsThatAppearedDuringTheRun that pre-populates a real temp directory with stale dumps, then verifies only fresh ones are published to the message bus.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Fix M1 regression: derive testhostDumpProduced from the regex loop using a PID-baked-in 'testhost-specific' regex, rather than relying on File.Exists with the literal '%p'-substituted path. This restores correct behavior for patterns that use other placeholders (e.g. '%e', '%h', '%t') alongside '%p'. The File.Exists check is retained as a fallback for the '%p'-only/no-placeholder case so behavior is preserved when the dump directory does not exist.

- Add unit test OnTestHostProcessExitedAsync_PatternWithMultiplePlaceholders_DoesNotEmitFalseMissingDumpWarning that uses a CapturingOutputDevice to assert the 'expected dump not found' warning is NOT emitted when the testhost dump is recognized via the regex.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Round 3 review came back with no major findings. Address the three minor items:

- Comment clarity: tighten the testhostDumpRegex comment to call out the degenerate '%p'-absent case where the regex collapses to dumpFileNameRegex and testhost cannot be distinguished from a child dump by name.

- Comment clarity: rewrite the File.Exists(expectedDumpFile) fallback comment to accurately describe the one scenario where it materially fires (testhost dump file was snapshotted as pre-existing) instead of overstating its '%p'-only coverage.

- Tests: add OnTestHostProcessExitedAsync_PatternWithRepeatedPidPlaceholder_RecognizesTesthostDump (proves string.Replace substitutes every '%p') and OnTestHostProcessExitedAsync_TesthostAndChildBothCrashWithMultiPlaceholderPattern_PublishesBothAndSuppressesWarning (the actual M1 regression vector: testhost + child dumps with a multi-placeholder pattern; both must publish and the warning must stay quiet).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings May 19, 2026 09:05
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot's findings

  • Files reviewed: 3/3 changed files
  • Comments generated: 2

Comment on lines +90 to +97
ApplicationStateGuard.Ensure(_netCoreCrashDumpGeneratorConfiguration.DumpFileNamePattern is not null);
string dumpDirectory = GetDumpDirectory(_netCoreCrashDumpGeneratorConfiguration.DumpFileNamePattern);
if (Directory.Exists(dumpDirectory))
{
foreach (string file in Directory.EnumerateFiles(dumpDirectory))
{
_preExistingDumpFiles.Add(file);
}
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Addressed in b2f1577.

The pre-existing snapshot now uses the same GetDumpSearchPattern (extension-based *ext or *) as the exit-time enumeration, so we only walk dump-shaped files instead of every entry in dumpDirectory. The loop now also calls cancellationToken.ThrowIfCancellationRequested() on each iteration so cancellation stays responsive when the directory is large.

if (args.Length > 0 && args[0] == "--child-crash")
{
Environment.FailFast("ChildCrashDump");
return 0; // unreachable
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Addressed in b2f1577.

The return 0 is now guarded by #if NETFRAMEWORK (with a comment) so it stays on .NET Framework — where Environment.FailFast lacks [DoesNotReturn] and the compiler requires a return on all paths — and is omitted on .NET 6+ where it would trigger CS0162. Verified the asset shape compiles cleanly with TreatWarningsAsErrors=true across net10.0;net8.0;net462.

Evangelink and others added 2 commits May 19, 2026 12:57
- Narrow OnTestHostProcessStartedAsync snapshot to the configured dump
  extension search pattern (same as exit-time enumeration) and honor
  the cancellation token while iterating.
- Drop the unreachable `return 0` after Environment.FailFast in the
  child-crash branch on TFMs where FailFast is annotated [DoesNotReturn]
  (net8.0+); keep it under `#if NETFRAMEWORK` so the compiler still
  sees a return path on .NET Framework.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings May 19, 2026 12:00
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot's findings

  • Files reviewed: 3/3 changed files
  • Comments generated: 2

Comment on lines +238 to +265
// Prefer Environment.ProcessPath (available since .NET 6) over Process.MainModule.FileName
// so we avoid loading the process module and any platform-specific failure modes that come
// with it. Fall back to MainModule for older runtimes.
string? path = Environment.ProcessPath;
if (string.IsNullOrEmpty(path))
{
using Process self = Process.GetCurrentProcess();
path = self.MainModule!.FileName!;
}

string fileName = Path.GetFileName(path);
bool isDotnetMuxer = string.Equals(fileName, "dotnet", StringComparison.OrdinalIgnoreCase)
|| string.Equals(fileName, "dotnet.exe", StringComparison.OrdinalIgnoreCase);

// Use ArgumentList instead of a single argument string so the runtime quotes/escapes each
// argument correctly across Windows and Unix; this also makes the test asset robust to
// paths that contain spaces or special characters.
var psi = new ProcessStartInfo(path)
{
UseShellExecute = false,
};
if (isDotnetMuxer)
{
psi.ArgumentList.Add("exec");
psi.ArgumentList.Add(Assembly.GetEntryAssembly()!.Location);
}

psi.ArgumentList.Add("--child-crash");
// Both the testhost and its child process crash with FailFast and must produce a dump each.
// Without the fix for https://github.com/microsoft/testfx/issues/4186, only the dump matching
// the testhost's PID was reported as an artifact and the child dump was silently dropped.
string[] dumpFiles = Directory.GetFiles(resultDirectory, "CrashDump_*.dmp", SearchOption.AllDirectories);
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.

Collect all dumps in the directory inside the crash dump

4 participants