-
Notifications
You must be signed in to change notification settings - Fork 308
Add --crash-report option to the CrashDump extension #8191
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
fa259c4
b904e02
8882424
142367b
a8755ad
e8b4818
d68feac
64d9b6f
f8497b7
9192db1
1e24076
d92b623
675b6e2
4284c0e
70d0f2a
9ef5281
717f1e5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,6 +11,13 @@ namespace Microsoft.Testing.Extensions.Diagnostics; | |
| internal sealed class CrashDumpCommandLineProvider : ICommandLineOptionsProvider | ||
| { | ||
| private static readonly string[] DumpTypeOptions = ["Mini", "Heap", "Triage", "Full"]; | ||
| private static readonly IReadOnlyCollection<CommandLineOption> CachedCommandLineOptions = | ||
| [ | ||
| new(CrashDumpCommandLineOptions.CrashDumpOptionName, CrashDumpResources.CrashDumpOptionDescription, ArgumentArity.Zero, false), | ||
| new(CrashDumpCommandLineOptions.CrashReportOptionName, CrashDumpResources.CrashReportOptionDescription, ArgumentArity.Zero, false), | ||
| new(CrashDumpCommandLineOptions.CrashDumpFileNameOptionName, CrashDumpResources.CrashDumpFileNameOptionDescription, ArgumentArity.ExactlyOne, false), | ||
| new(CrashDumpCommandLineOptions.CrashDumpTypeOptionName, CrashDumpResources.CrashDumpTypeOptionDescription, ArgumentArity.ExactlyOne, false) | ||
| ]; | ||
|
|
||
| public string Uid => nameof(CrashDumpCommandLineProvider); | ||
|
|
||
|
|
@@ -22,13 +29,7 @@ internal sealed class CrashDumpCommandLineProvider : ICommandLineOptionsProvider | |
|
|
||
| public Task<bool> IsEnabledAsync() => Task.FromResult(true); | ||
|
|
||
| public IReadOnlyCollection<CommandLineOption> GetCommandLineOptions() | ||
| => | ||
| [ | ||
| new CommandLineOption(CrashDumpCommandLineOptions.CrashDumpOptionName, CrashDumpResources.CrashDumpOptionDescription, ArgumentArity.Zero, false), | ||
| new CommandLineOption(CrashDumpCommandLineOptions.CrashDumpFileNameOptionName, CrashDumpResources.CrashDumpFileNameOptionDescription, ArgumentArity.ExactlyOne, false), | ||
| new CommandLineOption(CrashDumpCommandLineOptions.CrashDumpTypeOptionName, CrashDumpResources.CrashDumpTypeOptionDescription, ArgumentArity.ExactlyOne, false) | ||
| ]; | ||
| public IReadOnlyCollection<CommandLineOption> GetCommandLineOptions() => CachedCommandLineOptions; | ||
|
|
||
| public Task<ValidationResult> ValidateOptionArgumentsAsync(CommandLineOption commandOption, string[] arguments) | ||
| { | ||
|
|
@@ -45,5 +46,8 @@ public Task<ValidationResult> ValidateOptionArgumentsAsync(CommandLineOption com | |
| } | ||
|
|
||
| public Task<ValidationResult> ValidateCommandLineOptionsAsync(ICommandLineOptions commandLineOptions) | ||
| => ValidationResult.ValidTask; | ||
| => commandLineOptions.IsOptionSet(CrashDumpCommandLineOptions.CrashReportOptionName) | ||
| && RuntimeInformation.IsOSPlatform(OSPlatform.Windows) | ||
| ? ValidationResult.InvalidTask(CrashDumpResources.CrashReportNotSupportedOnWindowsErrorMessage) | ||
| : ValidationResult.ValidTask; | ||
|
Comment on lines
48
to
+52
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Already addressed in 8763b96 — the |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,6 +14,9 @@ namespace Microsoft.Testing.Extensions.Diagnostics; | |
|
|
||
| internal sealed class CrashDumpProcessLifetimeHandler : ITestHostProcessLifetimeHandler, IDataProducer, IOutputDeviceDataProducer | ||
| { | ||
| private const string CrashReportFileExtension = ".crashreport.json"; | ||
| private const string CrashReportFileSearchPattern = "*" + CrashReportFileExtension; | ||
|
|
||
| private readonly ICommandLineOptions _commandLineOptions; | ||
| private readonly IMessageBus _messageBus; | ||
| private readonly IOutputDevice _outputDisplay; | ||
|
|
@@ -46,8 +49,10 @@ public CrashDumpProcessLifetimeHandler( | |
| public Type[] DataTypesProduced => [typeof(FileArtifact)]; | ||
|
|
||
| public Task<bool> IsEnabledAsync() | ||
| => Task.FromResult(_commandLineOptions.IsOptionSet(CrashDumpCommandLineOptions.CrashDumpOptionName) | ||
| && _netCoreCrashDumpGeneratorConfiguration.Enable); | ||
| => Task.FromResult( | ||
| (_commandLineOptions.IsOptionSet(CrashDumpCommandLineOptions.CrashDumpOptionName) || | ||
| _commandLineOptions.IsOptionSet(CrashDumpCommandLineOptions.CrashReportOptionName)) && | ||
| _netCoreCrashDumpGeneratorConfiguration.Enable); | ||
|
|
||
| public Task BeforeTestHostProcessStartAsync(CancellationToken _) => Task.CompletedTask; | ||
|
|
||
|
|
@@ -63,22 +68,70 @@ public async Task OnTestHostProcessExitedAsync(ITestHostProcessInformation testH | |
| } | ||
|
|
||
| ApplicationStateGuard.Ensure(_netCoreCrashDumpGeneratorConfiguration.DumpFileNamePattern is not null); | ||
| await _outputDisplay.DisplayAsync(this, new ErrorMessageOutputDeviceData(string.Format(CultureInfo.InvariantCulture, CrashDumpResources.CrashDumpProcessCrashedDumpFileCreated, testHostProcessInformation.PID)), cancellationToken).ConfigureAwait(false); | ||
| bool generateDump = _commandLineOptions.IsOptionSet(CrashDumpCommandLineOptions.CrashDumpOptionName); | ||
| bool generateCrashReport = _commandLineOptions.IsOptionSet(CrashDumpCommandLineOptions.CrashReportOptionName); | ||
|
Comment on lines
+71
to
+72
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in #8328 — the generic |
||
|
|
||
| // TODO: Crash dump supports more placeholders that we don't handle here. | ||
| // See "Dump name formatting" in: | ||
| // https://github.com/dotnet/runtime/blob/82742628310076fff22d7e7ee216a74384352056/docs/design/coreclr/botr/xplat-minidump-generation.md | ||
| string expectedDumpFile = _netCoreCrashDumpGeneratorConfiguration.DumpFileNamePattern.Replace("%p", testHostProcessInformation.PID.ToString(CultureInfo.InvariantCulture)); | ||
| if (File.Exists(expectedDumpFile)) | ||
| string expectedCrashReportFile = $"{expectedDumpFile}{CrashReportFileExtension}"; | ||
|
|
||
| // Inspect the disk before emitting the crash banner so the message reflects | ||
| // what was actually produced, not what was requested. The runtime may fail | ||
| // to emit one (or both) of the artifacts, e.g. when EnableCrashReport is | ||
| // unsupported on the current platform/version. | ||
| bool dumpArtifactProduced = generateDump && File.Exists(expectedDumpFile); | ||
| bool crashReportArtifactProduced = generateCrashReport && File.Exists(expectedCrashReportFile); | ||
|
|
||
| string? processCrashedMessage = (dumpArtifactProduced, crashReportArtifactProduced) switch | ||
| { | ||
| await _messageBus.PublishAsync(this, new FileArtifact(new FileInfo(expectedDumpFile), CrashDumpResources.CrashDumpArtifactDisplayName, CrashDumpResources.CrashDumpArtifactDescription)).ConfigureAwait(false); | ||
| (true, true) => string.Format(CultureInfo.InvariantCulture, CrashDumpResources.CrashDumpProcessCrashedDumpAndReportFileCreated, testHostProcessInformation.PID), | ||
| (false, true) => string.Format(CultureInfo.InvariantCulture, CrashDumpResources.CrashDumpProcessCrashedReportFileCreated, testHostProcessInformation.PID), | ||
| (true, false) => string.Format(CultureInfo.InvariantCulture, CrashDumpResources.CrashDumpProcessCrashedDumpFileCreated, testHostProcessInformation.PID), | ||
| (false, false) => string.Format(CultureInfo.InvariantCulture, CrashDumpResources.CrashDumpProcessCrashed, testHostProcessInformation.PID), | ||
| }; | ||
| await _outputDisplay.DisplayAsync(this, new ErrorMessageOutputDeviceData(processCrashedMessage), cancellationToken).ConfigureAwait(false); | ||
|
|
||
| if (generateDump) | ||
| { | ||
| if (dumpArtifactProduced) | ||
| { | ||
| await _messageBus.PublishAsync(this, new FileArtifact(new FileInfo(expectedDumpFile), CrashDumpResources.CrashDumpArtifactDisplayName, CrashDumpResources.CrashDumpArtifactDescription)).ConfigureAwait(false); | ||
| } | ||
| else | ||
| { | ||
| await _outputDisplay.DisplayAsync(this, new ErrorMessageOutputDeviceData(string.Format(CultureInfo.InvariantCulture, CrashDumpResources.CannotFindExpectedCrashDumpFile, expectedDumpFile)), cancellationToken).ConfigureAwait(false); | ||
|
|
||
| // Filter by exact extension to defend against Windows' legacy 8.3 short-name | ||
| // matching where a pattern like '*.dmp' can also match files whose extension | ||
| // merely starts with '.dmp' (for example 'foo.dmp.crashreport.json'). | ||
| foreach (string dumpFile in Directory.EnumerateFiles(Path.GetDirectoryName(expectedDumpFile)!, "*.dmp") | ||
| .Where(static f => Path.GetExtension(f).Equals(".dmp", StringComparison.OrdinalIgnoreCase))) | ||
| { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Already addressed in 8763b96 — the |
||
| await _messageBus.PublishAsync(this, new FileArtifact(new FileInfo(dumpFile), CrashDumpResources.CrashDumpDisplayName, CrashDumpResources.CrashDumpArtifactDescription)).ConfigureAwait(false); | ||
| } | ||
| } | ||
| } | ||
| else | ||
|
|
||
| if (generateCrashReport) | ||
| { | ||
| await _outputDisplay.DisplayAsync(this, new ErrorMessageOutputDeviceData(string.Format(CultureInfo.InvariantCulture, CrashDumpResources.CannotFindExpectedCrashDumpFile, expectedDumpFile)), cancellationToken).ConfigureAwait(false); | ||
| foreach (string dumpFile in Directory.GetFiles(Path.GetDirectoryName(expectedDumpFile)!, "*.dmp")) | ||
| if (crashReportArtifactProduced) | ||
| { | ||
| await _messageBus.PublishAsync(this, new FileArtifact(new FileInfo(dumpFile), CrashDumpResources.CrashDumpDisplayName, CrashDumpResources.CrashDumpArtifactDescription)).ConfigureAwait(false); | ||
| await _messageBus.PublishAsync(this, new FileArtifact(new FileInfo(expectedCrashReportFile), CrashDumpResources.CrashReportArtifactDisplayName, CrashDumpResources.CrashReportArtifactDescription)).ConfigureAwait(false); | ||
| } | ||
| else | ||
| { | ||
| await _outputDisplay.DisplayAsync(this, new ErrorMessageOutputDeviceData(string.Format(CultureInfo.InvariantCulture, CrashDumpResources.CannotFindExpectedCrashReportFile, expectedCrashReportFile, CrashReportFileSearchPattern)), cancellationToken).ConfigureAwait(false); | ||
|
|
||
| // Filter by exact suffix to defend against Windows' legacy 8.3 short-name | ||
| // matching where a pattern can also match files whose extension only starts | ||
| // with the requested extension. | ||
| foreach (string crashReportFile in Directory.GetFiles(Path.GetDirectoryName(expectedCrashReportFile)!, CrashReportFileSearchPattern) | ||
| .Where(static f => f.EndsWith(CrashReportFileExtension, StringComparison.OrdinalIgnoreCase))) | ||
| { | ||
| await _messageBus.PublishAsync(this, new FileArtifact(new FileInfo(crashReportFile), CrashDumpResources.CrashReportArtifactDisplayName, CrashDumpResources.CrashReportArtifactDescription)).ConfigureAwait(false); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,6 @@ | ||
| # Microsoft.Testing.Extensions.CrashDump | ||
|
|
||
| Microsoft.Testing.Extensions.CrashDump is an extension for [Microsoft.Testing.Platform](https://www.nuget.org/packages/Microsoft.Testing.Platform) that captures a crash dump of the test host process when an unhandled exception or crash occurs. | ||
| Microsoft.Testing.Extensions.CrashDump is an extension for [Microsoft.Testing.Platform](https://www.nuget.org/packages/Microsoft.Testing.Platform) that captures a crash dump or crash report for the test host process when an unhandled exception or crash occurs. | ||
|
|
||
| Microsoft.Testing.Platform is open source. You can find `Microsoft.Testing.Extensions.CrashDump` code in the [microsoft/testfx](https://github.com/microsoft/testfx) GitHub repository. | ||
|
|
||
|
|
@@ -15,11 +15,13 @@ dotnet add package Microsoft.Testing.Extensions.CrashDump | |
| This package extends Microsoft.Testing.Platform with: | ||
|
|
||
| - **Crash dump collection**: automatically captures a memory dump when the test process crashes | ||
| - **Crash report collection**: optionally emits a lightweight JSON crash report to help diagnose crashes without uploading a full dump (Linux/macOS only — see [dotnet/runtime#80191](https://github.com/dotnet/runtime/issues/80191); requires .NET 7+ when used alone or .NET 6+ when combined with `--crashdump`) | ||
| - **Post-mortem debugging**: collected dumps can be analyzed with tools like Visual Studio, WinDbg, or `dotnet-dump` | ||
| - **Cross-platform**: supported on Windows, Linux, and macOS. Note that dumps collected on macOS can only be analyzed on macOS | ||
| - **Cross-platform**: crash dumps are supported on Windows, Linux, and macOS (dumps collected on macOS can only be analyzed on macOS). Crash reports are currently only supported on Linux and macOS. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Already addressed in 8763b96 — |
||
| - **Runtime behavior**: supported for .NET 6+; on .NET Framework this extension is ignored | ||
|
|
||
| Enable crash dump collection via the `--crashdump` command line option. | ||
| Add `--crash-report` (Linux/macOS only; requires .NET 7+ when used alone or .NET 6+ with `--crashdump`) to generate a JSON crash report; combine `--crashdump --crash-report` to produce both a dump and a report. | ||
|
|
||
| ## Related packages | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Discussed and kept as
--crash-reportintentionally. Three reasons: (1) the crash report is a distinct artifact (JSON output, separate runtime opt-in viaDOTNET_EnableCrashReport/DOTNET_EnableCrashReportOnly, can stand alone without a dump), so it deserves a first-class option name rather than being acrashdump-*modifier; (2) the older sibling options (crashdump,hangdump) are themselves not strictly following the noun-hyphenation convention —crash-reportis actually the more correct hyphenated form, and renaming the legacy options is a separate breaking-change conversation; (3) renaming a brand-new CLI surface hours after merge would create churn without a real user-facing win. Leaving the hyphenated form per the reviewer's closing note ("If the hyphenated form is intentional, this can be ignored.").