Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ namespace Microsoft.Testing.Extensions.Diagnostics;
internal static class CrashDumpCommandLineOptions
{
public const string CrashDumpOptionName = "crashdump";
public const string CrashReportOptionName = "crash-report";

Copy link
Copy Markdown
Member

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-report intentionally. Three reasons: (1) the crash report is a distinct artifact (JSON output, separate runtime opt-in via DOTNET_EnableCrashReport/DOTNET_EnableCrashReportOnly, can stand alone without a dump), so it deserves a first-class option name rather than being a crashdump-* modifier; (2) the older sibling options (crashdump, hangdump) are themselves not strictly following the noun-hyphenation convention — crash-report is 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.").

public const string CrashDumpFileNameOptionName = "crashdump-filename";
public const string CrashDumpTypeOptionName = "crashdump-type";
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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)
{
Expand All @@ -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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Already addressed in 8763b96 — the --crash-report option description in CrashDumpResources.resx:182-184 and PACKAGE.md:18,24 both explicitly state "Requires .NET 7+ when used alone; .NET 6+ when combined with --crashdump" and call out "This runtime requirement is not enforced by the tool: on unsupported runtimes no crash report will be emitted." That meets the "at minimum, the help text should make clear" bar. Surfacing a warning at runtime would require detecting the runtime version up-front, which we deliberately punted as out of scope for the initial PR.

}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ internal sealed class CrashDumpEnvironmentVariableProvider : ITestHostEnvironmen
private const string MiniDumpNameVariable = "DbgMiniDumpName";
private const string CreateDumpDiagnosticsVariable = "CreateDumpDiagnostics";
private const string CreateDumpVerboseDiagnosticsVariable = "CreateDumpVerboseDiagnostics";
private const string EnableMiniDumpValue = "1";
private const string EnableCrashReportVariable = "EnableCrashReport";
private const string EnableCrashReportOnlyVariable = "EnableCrashReportOnly";
private const string EnabledValue = "1";

private static readonly string[] Prefixes = ["DOTNET_", "COMPlus_"];
private readonly IConfiguration _configuration;
Expand Down Expand Up @@ -54,15 +56,34 @@ public CrashDumpEnvironmentVariableProvider(

/// <inheritdoc />
public Task<bool> IsEnabledAsync()
=> Task.FromResult(_commandLineOptions.IsOptionSet(CrashDumpCommandLineOptions.CrashDumpOptionName) && _crashDumpGeneratorConfiguration.Enable);
=> Task.FromResult(
(_commandLineOptions.IsOptionSet(CrashDumpCommandLineOptions.CrashDumpOptionName) ||
_commandLineOptions.IsOptionSet(CrashDumpCommandLineOptions.CrashReportOptionName)) &&
_crashDumpGeneratorConfiguration.Enable);

public Task UpdateAsync(IEnvironmentVariables environmentVariables)
{
// IsEnabledAsync gates this method, so at least one of --crashdump / --crash-report is set here.
bool crashReportEnabled = _commandLineOptions.IsOptionSet(CrashDumpCommandLineOptions.CrashReportOptionName);

foreach (string prefix in Prefixes)
{
environmentVariables.SetVariable(new($"{prefix}{EnableMiniDumpVariable}", EnableMiniDumpValue, false, true));
environmentVariables.SetVariable(new($"{prefix}{CreateDumpDiagnosticsVariable}", EnableMiniDumpValue, false, true));
environmentVariables.SetVariable(new($"{prefix}{CreateDumpVerboseDiagnosticsVariable}", EnableMiniDumpValue, false, true));
environmentVariables.SetVariable(new($"{prefix}{EnableMiniDumpVariable}", EnabledValue, false, true));
environmentVariables.SetVariable(new($"{prefix}{CreateDumpDiagnosticsVariable}", EnabledValue, false, true));
environmentVariables.SetVariable(new($"{prefix}{CreateDumpVerboseDiagnosticsVariable}", EnabledValue, false, true));
}

if (crashReportEnabled)
{
bool crashDumpEnabled = _commandLineOptions.IsOptionSet(CrashDumpCommandLineOptions.CrashDumpOptionName);

// When a dump is also requested, emit a crash report alongside it.
// Otherwise emit only the crash report (no dump file).
string reportVariable = crashDumpEnabled ? EnableCrashReportVariable : EnableCrashReportOnlyVariable;
foreach (string prefix in Prefixes)
{
environmentVariables.SetVariable(new($"{prefix}{reportVariable}", EnabledValue, false, true));
}
}
Comment thread
Evangelink marked this conversation as resolved.

string miniDumpTypeValue = "4";
Expand Down Expand Up @@ -133,31 +154,19 @@ public Task<ValidationResult> ValidateTestHostEnvironmentVariablesAsync(IReadOnl
return ValidationResult.InvalidTask(CrashDumpResources.CrashDumpNotSupportedInNonNetCoreErrorMessage);
#else
StringBuilder errors = new();
foreach (string prefix in Prefixes)
{
if (!environmentVariables.TryGetVariable($"{prefix}{EnableMiniDumpVariable}", out OwnedEnvironmentVariable? enableMiniDump)
|| enableMiniDump.Value != EnableMiniDumpValue)
{
AddError(errors, $"{prefix}{EnableMiniDumpVariable}", EnableMiniDumpValue, enableMiniDump?.Value);
}
}

foreach (string prefix in Prefixes)
{
if (!environmentVariables.TryGetVariable($"{prefix}{CreateDumpDiagnosticsVariable}", out OwnedEnvironmentVariable? enableMiniDump)
|| enableMiniDump.Value != EnableMiniDumpValue)
{
AddError(errors, $"{prefix}{CreateDumpDiagnosticsVariable}", EnableMiniDumpValue, enableMiniDump?.Value);
}
}
// IsEnabledAsync gates this method, so at least one of --crashdump / --crash-report is set here.
bool crashReportEnabled = _commandLineOptions.IsOptionSet(CrashDumpCommandLineOptions.CrashReportOptionName);

foreach (string prefix in Prefixes)
ValidateBothPrefixes(EnableMiniDumpVariable, EnabledValue);
ValidateBothPrefixes(CreateDumpDiagnosticsVariable, EnabledValue);
ValidateBothPrefixes(CreateDumpVerboseDiagnosticsVariable, EnabledValue);

if (crashReportEnabled)
{
if (!environmentVariables.TryGetVariable($"{prefix}{CreateDumpVerboseDiagnosticsVariable}", out OwnedEnvironmentVariable? enableMiniDump)
|| enableMiniDump.Value != EnableMiniDumpValue)
{
AddError(errors, $"{prefix}{CreateDumpVerboseDiagnosticsVariable}", EnableMiniDumpValue, enableMiniDump?.Value);
}
bool crashDumpEnabled = _commandLineOptions.IsOptionSet(CrashDumpCommandLineOptions.CrashDumpOptionName);
string reportVariable = crashDumpEnabled ? EnableCrashReportVariable : EnableCrashReportOnlyVariable;
ValidateBothPrefixes(reportVariable, EnabledValue);
}

foreach (string prefix in Prefixes)
Expand Down Expand Up @@ -199,6 +208,18 @@ static void AddError(StringBuilder errors, string variableName, string? expected
string actualValueString = actualValue ?? "<null>";
errors.AppendLine(string.Format(CultureInfo.InvariantCulture, CrashDumpResources.CrashDumpInvalidEnvironmentVariableValueErrorMessage, variableName, expectedValue, actualValueString));
}

void ValidateBothPrefixes(string variableName, string expectedValue)
{
foreach (string prefix in Prefixes)
{
if (!environmentVariables.TryGetVariable($"{prefix}{variableName}", out OwnedEnvironmentVariable? variable)
|| variable.Value != expectedValue)
{
AddError(errors, $"{prefix}{variableName}", expectedValue, variable?.Value);
}
}
}
#endif
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;

Expand All @@ -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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Fixed in #8328 — the generic CrashDumpProcessCrashed banner now reads "Test host process with PID '{0}' crashed but the expected diagnostic artifact(s) were not produced". Since IsEnabledAsync guarantees at least one of --crashdump / --crash-report is set whenever this handler runs, reaching the (false, false) branch always means an expected artifact was not produced, so the more informative wording is always accurate. XLF files regenerated in the same commit.


// 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)))
{

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Already addressed in 8763b96 — the *.dmp fallback enumeration is now filtered with Path.GetExtension(f).Equals(".dmp", StringComparison.OrdinalIgnoreCase) so a customdumpname.dmp.crashreport.json (or any other extension that merely starts with .dmp) cannot leak through (CrashDumpProcessLifetimeHandler.cs:109-110).

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);
}
}
}
}
Expand Down
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.

Expand All @@ -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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Already addressed in 8763b96PACKAGE.md:18 and PACKAGE.md:24 both call out the runtime version requirement: "requires .NET 7+ when used alone or .NET 6+ when combined with --crashdump".

- **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

Expand Down
Loading