Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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,8 @@ namespace Microsoft.Testing.Extensions.Diagnostics;
internal static class CrashDumpCommandLineOptions
{
public const string CrashDumpOptionName = "crashdump";
public const string CrashReportOptionName = "crashreport";
public const string CrashReportOnlyOptionName = "crashreport-only";
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,14 @@ 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.CrashReportOnlyOptionName, CrashDumpResources.CrashReportOnlyOptionDescription, 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 +30,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 +47,12 @@ public Task<ValidationResult> ValidateOptionArgumentsAsync(CommandLineOption com
}

public Task<ValidationResult> ValidateCommandLineOptionsAsync(ICommandLineOptions commandLineOptions)
=> ValidationResult.ValidTask;
=> commandLineOptions.IsOptionSet(CrashDumpCommandLineOptions.CrashReportOptionName) &&
!commandLineOptions.IsOptionSet(CrashDumpCommandLineOptions.CrashDumpOptionName)
? ValidationResult.InvalidTask(CrashDumpResources.CrashReportRequiresCrashDumpErrorMessage)
: commandLineOptions.IsOptionSet(CrashDumpCommandLineOptions.CrashReportOnlyOptionName) &&
(commandLineOptions.IsOptionSet(CrashDumpCommandLineOptions.CrashDumpOptionName) ||
commandLineOptions.IsOptionSet(CrashDumpCommandLineOptions.CrashReportOptionName))
? ValidationResult.InvalidTask(CrashDumpResources.CrashReportOnlyCannotBeCombinedErrorMessage)
: ValidationResult.ValidTask;
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.

Done in e8b4818. Refactored into explicit if-statements (one per invalid combination) so future mutually-exclusive crash options are easy to add.

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.

Done in 64d9b6f (and the prior d68feac). Beyond the requested refactor: the entire validation method is now a no-op because the CLI surface was simplified to a single --crash-report flag (see #8191 (comment) / the rename discussion in PR comments). The two mutually-exclusive rules and their error messages no longer exist.

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

private static readonly string[] Prefixes = ["DOTNET_", "COMPlus_"];
Expand Down Expand Up @@ -54,17 +56,48 @@ 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) ||
_commandLineOptions.IsOptionSet(CrashDumpCommandLineOptions.CrashReportOnlyOptionName)) &&
_crashDumpGeneratorConfiguration.Enable);

public Task UpdateAsync(IEnvironmentVariables environmentVariables)
{
bool crashDumpEnabled = _commandLineOptions.IsOptionSet(CrashDumpCommandLineOptions.CrashDumpOptionName);
bool crashReportEnabled = _commandLineOptions.IsOptionSet(CrashDumpCommandLineOptions.CrashReportOptionName);
bool crashReportOnlyEnabled = _commandLineOptions.IsOptionSet(CrashDumpCommandLineOptions.CrashReportOnlyOptionName);

if (crashDumpEnabled || crashReportOnlyEnabled)
{
foreach (string prefix in Prefixes)
{
environmentVariables.SetVariable(new($"{prefix}{EnableMiniDumpVariable}", EnableMiniDumpValue, false, true));
}
Comment on lines +69 to +74
}
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.

Good catch. Addressed in d68feac (the condition now includes crashReportEnabled so MiniDump is enabled whenever any crash option is set, regardless of validator order). 64d9b6f keeps the same self-consistent wiring for the simplified single-flag design.


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));
}

if (crashReportEnabled)
{
foreach (string prefix in Prefixes)
{
environmentVariables.SetVariable(new($"{prefix}{EnableCrashReportVariable}", EnableMiniDumpValue, false, true));
}
}
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.

Done in d68feac. Renamed EnableMiniDumpValue to the neutral EnabledValue since it's reused by all three runtime variables.


if (crashReportOnlyEnabled)
{
foreach (string prefix in Prefixes)
{
environmentVariables.SetVariable(new($"{prefix}{EnableCrashReportOnlyVariable}", EnableMiniDumpValue, false, true));
}
}
Comment on lines +83 to +92
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.

Done in e8b4818. Renamed EnableMiniDumpValue to the neutral EnabledValue since it's now also used for DOTNET_EnableCrashReport and DOTNET_EnableCrashReportOnly.


string miniDumpTypeValue = "4";

if (_commandLineOptions.TryGetOptionArgumentList(CrashDumpCommandLineOptions.CrashDumpTypeOptionName, out string[]? dumpTypeString))
Expand Down Expand Up @@ -133,12 +166,16 @@ public Task<ValidationResult> ValidateTestHostEnvironmentVariablesAsync(IReadOnl
return ValidationResult.InvalidTask(CrashDumpResources.CrashDumpNotSupportedInNonNetCoreErrorMessage);
#else
StringBuilder errors = new();
foreach (string prefix in Prefixes)
if (_commandLineOptions.IsOptionSet(CrashDumpCommandLineOptions.CrashDumpOptionName) ||
_commandLineOptions.IsOptionSet(CrashDumpCommandLineOptions.CrashReportOnlyOptionName))
{
if (!environmentVariables.TryGetVariable($"{prefix}{EnableMiniDumpVariable}", out OwnedEnvironmentVariable? enableMiniDump)
|| enableMiniDump.Value != EnableMiniDumpValue)
foreach (string prefix in Prefixes)
{
AddError(errors, $"{prefix}{EnableMiniDumpVariable}", EnableMiniDumpValue, enableMiniDump?.Value);
if (!environmentVariables.TryGetVariable($"{prefix}{EnableMiniDumpVariable}", out OwnedEnvironmentVariable? enableMiniDump)
|| enableMiniDump.Value != EnableMiniDumpValue)
{
AddError(errors, $"{prefix}{EnableMiniDumpVariable}", EnableMiniDumpValue, enableMiniDump?.Value);
}
}
}

Expand All @@ -160,6 +197,30 @@ public Task<ValidationResult> ValidateTestHostEnvironmentVariablesAsync(IReadOnl
}
}

if (_commandLineOptions.IsOptionSet(CrashDumpCommandLineOptions.CrashReportOptionName))
{
foreach (string prefix in Prefixes)
{
if (!environmentVariables.TryGetVariable($"{prefix}{EnableCrashReportVariable}", out OwnedEnvironmentVariable? enableCrashReport)
|| enableCrashReport.Value != EnableMiniDumpValue)
{
AddError(errors, $"{prefix}{EnableCrashReportVariable}", EnableMiniDumpValue, enableCrashReport?.Value);
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.

Done in d68feac. Extracted a ValidateBothPrefixes(string variableName, string expectedValue) local function and replaced all three duplicated blocks with calls to it.

}
}
}

if (_commandLineOptions.IsOptionSet(CrashDumpCommandLineOptions.CrashReportOnlyOptionName))
{
foreach (string prefix in Prefixes)
{
if (!environmentVariables.TryGetVariable($"{prefix}{EnableCrashReportOnlyVariable}", out OwnedEnvironmentVariable? enableCrashReportOnly)
|| enableCrashReportOnly.Value != EnableMiniDumpValue)
{
AddError(errors, $"{prefix}{EnableCrashReportOnlyVariable}", EnableMiniDumpValue, enableCrashReportOnly?.Value);
}
}
}

foreach (string prefix in Prefixes)
{
if (!environmentVariables.TryGetVariable($"{prefix}{MiniDumpTypeVariable}", out OwnedEnvironmentVariable? miniDumpType))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,11 @@ 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) ||
_commandLineOptions.IsOptionSet(CrashDumpCommandLineOptions.CrashReportOnlyOptionName)) &&
_netCoreCrashDumpGeneratorConfiguration.Enable);

public Task BeforeTestHostProcessStartAsync(CancellationToken _) => Task.CompletedTask;

Expand All @@ -63,22 +66,51 @@ 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) ||
_commandLineOptions.IsOptionSet(CrashDumpCommandLineOptions.CrashReportOnlyOptionName);

string processCrashedMessage = generateDump && generateCrashReport
? string.Format(CultureInfo.InvariantCulture, CrashDumpResources.CrashDumpProcessCrashedDumpAndReportFileCreated, testHostProcessInformation.PID)
: generateCrashReport
? string.Format(CultureInfo.InvariantCulture, CrashDumpResources.CrashDumpProcessCrashedReportFileCreated, testHostProcessInformation.PID)
: string.Format(CultureInfo.InvariantCulture, CrashDumpResources.CrashDumpProcessCrashedDumpFileCreated, testHostProcessInformation.PID);
await _outputDisplay.DisplayAsync(this, new ErrorMessageOutputDeviceData(processCrashedMessage), cancellationToken).ConfigureAwait(false);

// 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))
if (generateDump)
{
await _messageBus.PublishAsync(this, new FileArtifact(new FileInfo(expectedDumpFile), CrashDumpResources.CrashDumpArtifactDisplayName, CrashDumpResources.CrashDumpArtifactDescription)).ConfigureAwait(false);
if (File.Exists(expectedDumpFile))
{
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);
foreach (string dumpFile in Directory.GetFiles(Path.GetDirectoryName(expectedDumpFile)!, "*.dmp"))
{
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"))
string expectedCrashReportFile = $"{expectedDumpFile}.crashreport.json";
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.

Done in d68feac. Extracted private const string CrashReportFileExtension = .crashreport.json; and private const string CrashReportFileSearchPattern = * + CrashReportFileExtension; so the suffix is defined in one place. The resource string is parameterized with {1} to receive the search pattern.

if (File.Exists(expectedCrashReportFile))
{
await _messageBus.PublishAsync(this, new FileArtifact(new FileInfo(expectedCrashReportFile), CrashDumpResources.CrashReportArtifactDisplayName, CrashDumpResources.CrashReportArtifactDescription)).ConfigureAwait(false);
}
else
{
await _messageBus.PublishAsync(this, new FileArtifact(new FileInfo(dumpFile), CrashDumpResources.CrashDumpDisplayName, CrashDumpResources.CrashDumpArtifactDescription)).ConfigureAwait(false);
await _outputDisplay.DisplayAsync(this, new ErrorMessageOutputDeviceData(string.Format(CultureInfo.InvariantCulture, CrashDumpResources.CannotFindExpectedCrashReportFile, expectedCrashReportFile)), cancellationToken).ConfigureAwait(false);
foreach (string crashReportFile in Directory.GetFiles(Path.GetDirectoryName(expectedCrashReportFile)!, "*.crashreport.json"))
{
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
- **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
- **Runtime behavior**: supported for .NET 6+; on .NET Framework this extension is ignored

Enable crash dump collection via the `--crashdump` command line option.
Add `--crashreport` to generate a dump and crash report together, or use `--crashreport-only` to generate only the crash report.

## Related packages

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,9 @@
<data name="CannotFindExpectedCrashDumpFile" xml:space="preserve">
<value>Expected crash dump file '{0}' could not be found, all files matching the '*.dmp' pattern will be copied to the result folder</value>
</data>
<data name="CannotFindExpectedCrashReportFile" xml:space="preserve">
<value>Expected crash report file '{0}' could not be found, all files matching the '*.crashreport.json' pattern will be copied to the result folder</value>
</data>
<data name="CrashDumpArtifactDescription" xml:space="preserve">
<value>The testhost process crash dump file</value>
</data>
Expand All @@ -144,9 +147,33 @@
<data name="CrashDumpOptionDescription" xml:space="preserve">
<value>[net6.0+ only] Generate a dump file if the test process crashes</value>
</data>
<data name="CrashReportArtifactDescription" xml:space="preserve">
<value>The testhost process crash report file</value>
</data>
<data name="CrashReportArtifactDisplayName" xml:space="preserve">
<value>Crash report file</value>
</data>
<data name="CrashReportOnlyCannotBeCombinedErrorMessage" xml:space="preserve">
<value>'--crashreport-only' can't be combined with '--crashdump' or '--crashreport'</value>
</data>
<data name="CrashReportOnlyOptionDescription" xml:space="preserve">
<value>[net7.0+ only] Generate only a crash report file if the test process crashes</value>
</data>
<data name="CrashReportOptionDescription" xml:space="preserve">
<value>[net6.0+ only] Generate a crash report file if the test process crashes. Requires '--crashdump'.</value>
</data>
<data name="CrashReportRequiresCrashDumpErrorMessage" xml:space="preserve">
<value>You specified '--crashreport' but did not enable crash dumps, add --crashdump to the command line</value>
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.

The CrashReportRequiresCrashDumpErrorMessage resource was already updated to quote both option names in d68feac, and as of 64d9b6f the resource was removed altogether (the validation it backed no longer exists since the CLI surface was simplified to a single --crash-report flag).

</data>
<data name="CrashDumpProcessCrashedDumpAndReportFileCreated" xml:space="preserve">
<value>Test host process with PID '{0}' crashed, a dump file and crash report were generated</value>
</data>
<data name="CrashDumpProcessCrashedDumpFileCreated" xml:space="preserve">
<value>Test host process with PID '{0}' crashed, a dump file was generated</value>
</data>
<data name="CrashDumpProcessCrashedReportFileCreated" xml:space="preserve">
<value>Test host process with PID '{0}' crashed, a crash report was generated</value>
</data>
Comment on lines +150 to +167
<data name="CrashDumpTypeOptionDescription" xml:space="preserve">
<value>Specify the type of the dump.
Valid values are 'Mini', 'Heap', 'Triage' or 'Full'. Default type is 'Full'.
Expand Down
Loading
Loading