Skip to content
Open
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
2 changes: 1 addition & 1 deletion docs/glossary.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ An MTP struct (`ArgumentArity.cs`) that defines the minimum and maximum number o

### ArtifactNamingHelper

A shared static helper compiled into MTP extensions via file linking (no NuGet service registration or InternalsVisibleTo required) that provides template-based naming for test artifact files (dump files, report files, etc.). Templates are strings containing `{placeholder}` tokens (case-sensitive, lowercase): `{pname}` (process name), `{pid}` (process ID), `{asm}` (entry-assembly name), `{tfm}` (target framework moniker, best-effort runtime detection), and `{time}` (high-precision UTC timestamp). Legacy `%p` patterns from earlier hang-dump versions continue to work. Custom per-call overrides can replace default placeholder values via a `Dictionary<string, string>`. Used by the [HangDump](#hangdump) and [CrashDump](#crashdump) extensions.
A shared static helper compiled into MTP extensions via file linking (no NuGet service registration or InternalsVisibleTo required) that provides template-based naming for test artifact files (dump files, report files, etc.). Templates are strings containing `{placeholder}` tokens (case-sensitive, lowercase): `{pname}` (process name), `{pid}` (process ID), `{asm}` (entry-assembly name), `{tfm}` (target framework moniker, best-effort runtime detection), and `{time}` (high-precision UTC timestamp). Custom per-call overrides can replace default placeholder values via a `Dictionary<string, string>`. Used directly by the [HangDump](#hangdump) and [CrashDump](#crashdump) extensions, and indirectly by the report extensions ([HtmlReport](#htmlreport), [JUnitReport](#junitreport), and TrxReport) via the shared `ReportFileNameHelper`. The CrashDump consumer maps `{pid}` to the .NET runtime's `%p` and `{pname}` to `%e` so they expand at crash-write time (the testhost PID is not known when the environment variables are configured). HangDump preserves its legacy `%p` compatibility in its own option handling.

### AzureDevOpsReport

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Microsoft.Testing.Platform.Extensions.TestHostControllers;
using Microsoft.Testing.Platform.Helpers;
using Microsoft.Testing.Platform.Logging;
using Microsoft.Testing.Platform.Services;

namespace Microsoft.Testing.Extensions.Diagnostics;

Expand All @@ -27,6 +28,7 @@ internal sealed class CrashDumpEnvironmentVariableProvider : ITestHostEnvironmen
private readonly ICommandLineOptions _commandLineOptions;
private readonly CrashDumpConfiguration _crashDumpGeneratorConfiguration;
private readonly ILogger<CrashDumpEnvironmentVariableProvider> _logger;
private readonly IClock _clock;

private string? _miniDumpNameValue;
private string? _sequenceFileValue;
Expand All @@ -35,12 +37,14 @@ public CrashDumpEnvironmentVariableProvider(
IConfiguration configuration,
ICommandLineOptions commandLineOptions,
CrashDumpConfiguration crashDumpGeneratorConfiguration,
ILoggerFactory loggerFactory)
ILoggerFactory loggerFactory,
IClock clock)
{
_logger = loggerFactory.CreateLogger<CrashDumpEnvironmentVariableProvider>();
_configuration = configuration;
_commandLineOptions = commandLineOptions;
_crashDumpGeneratorConfiguration = crashDumpGeneratorConfiguration;
_clock = clock;
}

/// <inheritdoc />
Expand Down Expand Up @@ -137,9 +141,12 @@ public Task UpdateAsync(IEnvironmentVariables environmentVariables)
}

string testAppName = Assembly.GetEntryAssembly()?.GetName().Name ?? throw ApplicationStateGuard.Unreachable();
_miniDumpNameValue = _commandLineOptions.TryGetOptionArgumentList(CrashDumpCommandLineOptions.CrashDumpFileNameOptionName, out string[]? dumpFileName)
? Path.Combine(_configuration.GetTestResultDirectory(), dumpFileName[0])
: Path.Combine(_configuration.GetTestResultDirectory(), $"{testAppName}_%p_crash.dmp");
string? resolvedUserPattern = _commandLineOptions.TryGetOptionArgumentList(CrashDumpCommandLineOptions.CrashDumpFileNameOptionName, out string[]? dumpFileName)
? ResolveUserDumpFileNameTemplate(dumpFileName[0])
: null;
_miniDumpNameValue = Path.Combine(
_configuration.GetTestResultDirectory(),
resolvedUserPattern ?? $"{testAppName}_%p_crash.dmp");
_crashDumpGeneratorConfiguration.DumpFileNamePattern = _miniDumpNameValue;
foreach (string prefix in Prefixes)
{
Expand All @@ -157,9 +164,14 @@ public Task UpdateAsync(IEnvironmentVariables environmentVariables)
// launches targeting the same results directory cannot stomp on each other's sequence
// files (a write collision would otherwise be silently lost; a graceful exit of one host
// would also delete the sibling host's sequence file before it could be published).
//
// We base the sequence file on the *resolved* user pattern (after substituting {asm}/{tfm}/
// {time}) so the sequence file name stays in sync with the dump file name; only the
// runtime placeholders that we deferred to "createdump" (e.g. {pid} -> %p, {pname} -> %e)
// are stripped here.
string uniqueToken = Guid.NewGuid().ToString("N").Substring(0, 8);
string sequenceFileName = dumpFileName is not null
? $"{StripRuntimePlaceholders(dumpFileName[0])}_{uniqueToken}.sequence.log"
string sequenceFileName = resolvedUserPattern is not null
? $"{StripRuntimePlaceholders(resolvedUserPattern)}_{uniqueToken}.sequence.log"
: $"{testAppName}_{uniqueToken}_crash.sequence.log";
_sequenceFileValue = Path.Combine(_configuration.GetTestResultDirectory(), sequenceFileName);
_crashDumpGeneratorConfiguration.SequenceFileName = _sequenceFileValue;
Expand Down Expand Up @@ -190,6 +202,25 @@ private bool IsSequenceLoggingEnabled()
return !CommandLineOptionArgumentValidator.IsOffValue(arguments[0]);
}

private string ResolveUserDumpFileNameTemplate(string userTemplate)
{
// CrashDump's filename is consumed by the .NET runtime's "createdump" - not by our code -
// so any user-facing token whose value is only knowable at crash time (process id and
// executable name) must map to a runtime placeholder ("%p" / "%e") rather than a literal
// value resolved here. The remaining standard tokens (asm/tfm/time) are deterministic at
// configuration time and substituted straight away.
//
// Implementation note: GetStandardReplacements seeds the dictionary with pname=processName
// and pid=processId, so passing "%e" / "%p" as those arguments yields a dictionary whose
// {pname}/{pid} entries are the runtime placeholders themselves. This keeps the resolver
// generic and avoids a second post-processing pass.
Dictionary<string, string> replacements = ArtifactNamingHelper.GetStandardReplacements(
processName: "%e",
Comment thread
Evangelink marked this conversation as resolved.
processId: "%p",
_clock.UtcNow);
return ArtifactNamingHelper.ResolveTemplate(userTemplate, replacements);
}

private static string StripRuntimePlaceholders(string pattern)
{
// Drop the .NET runtime's "createdump" placeholders (%p, %e, %h, %t, ...) from a dump filename
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ public static void AddCrashDumpProvider(this ITestApplicationBuilder builder, bo
serviceProvider.GetConfiguration(),
serviceProvider.GetCommandLineOptions(),
crashDumpGeneratorConfiguration,
serviceProvider.GetLoggerFactory()));
serviceProvider.GetLoggerFactory(),
serviceProvider.GetClock()));

builder.TestHostControllers.AddProcessLifetimeHandler(serviceProvider
=> new CrashDumpProcessLifetimeHandler(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,39 @@ public async Task OnTestHostProcessExitedAsync(ITestHostProcessInformation testH
// want the banner to reflect that the testhost dump is, technically, present on disk.
testhostDumpProduced = generateDump && (testhostDumpProduced || File.Exists(expectedDumpFile));
bool dumpArtifactProduced = generateDump && (testhostDumpProduced || publishedAnyDump);
bool crashReportArtifactProduced = generateCrashReport && File.Exists(expectedCrashReportFile);

// The crash report file is written as "<dump file name>.crashreport.json" beside the dump.
// The dump file name pattern can contain runtime placeholders besides "%p" (e.g. "%e" when
// the user picks the {pname} token, "%h" or "%t" when configured directly). A plain
// File.Exists check on `expectedCrashReportFile` would miss those reports, so we apply the
// same testhost-dump-name regex to the prefix of each "*.crashreport.json" file in the dump
// directory: this preserves the literal-`%p`-baked PID match while expanding any remaining
// placeholders as wildcards, mirroring the dump-publication logic above.
List<string>? crashReportFiles = null;
bool matchedCrashReportFile = false;
if (generateCrashReport && Directory.Exists(dumpDirectory))
{
foreach (string crashReportFile in Directory.EnumerateFiles(dumpDirectory, CrashReportFileSearchPattern))
{
string crashReportFileName = Path.GetFileName(crashReportFile);
if (!crashReportFileName.EndsWith(CrashReportFileExtension, StringComparison.OrdinalIgnoreCase))
{
continue;
}

crashReportFiles ??= [];
crashReportFiles.Add(crashReportFile);

string dumpFileNamePart = crashReportFileName.Substring(0, crashReportFileName.Length - CrashReportFileExtension.Length);
if (testhostDumpRegex.IsMatch(dumpFileNamePart))
{
matchedCrashReportFile = true;
}
}
}

bool expectedCrashReportFileExists = File.Exists(expectedCrashReportFile);
bool crashReportArtifactProduced = expectedCrashReportFileExists || matchedCrashReportFile;

// Inspect the disk before emitting the crash banner so the message reflects
// what was actually produced, not what was requested. The runtime may fail
Expand Down Expand Up @@ -314,10 +346,17 @@ public async Task OnTestHostProcessExitedAsync(ITestHostProcessInformation testH

if (generateCrashReport)
{
if (crashReportArtifactProduced)
if (expectedCrashReportFileExists)
{
await _messageBus.PublishAsync(this, new FileArtifact(new FileInfo(expectedCrashReportFile), CrashDumpResources.CrashReportArtifactDisplayName, CrashDumpResources.CrashReportArtifactDescription)).ConfigureAwait(false);
}
else if (matchedCrashReportFile)
{
foreach (string crashReportFile in crashReportFiles!)
{
await _messageBus.PublishAsync(this, new FileArtifact(new FileInfo(crashReportFile), CrashDumpResources.CrashReportArtifactDisplayName, CrashDumpResources.CrashReportArtifactDescription)).ConfigureAwait(false);
}
}
else
{
await _outputDisplay.DisplayAsync(this, new ErrorMessageOutputDeviceData(string.Format(CultureInfo.InvariantCulture, CrashDumpResources.CannotFindExpectedCrashReportFile, expectedCrashReportFile, CrashReportFileSearchPattern)), cancellationToken).ConfigureAwait(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
<Compile Include="$(RepoRoot)src\Platform\Microsoft.Testing.Platform\Helpers\ApplicationStateGuard.cs" Link="Helpers\ApplicationStateGuard.cs" />
<Compile Include="$(RepoRoot)src\Platform\Microsoft.Testing.Platform\Helpers\RoslynString.cs" Link="Helpers\RoslynString.cs" />
<Compile Include="$(RepoRoot)src\Platform\Microsoft.Testing.Platform\Resources\PlatformResources.cs" Link="Resources\PlatformResources.cs" />
<Compile Include="$(RepoRoot)src\Platform\Microsoft.Testing.Platform\Services\ArtifactNamingHelper.cs" Link="Services\ArtifactNamingHelper.cs" />
<Compile Include="$(RepoRoot)src\Platform\Microsoft.Testing.Platform\OutputDevice\TargetFrameworkParser.cs" Link="Helpers\TargetFrameworkParser.cs" />
</ItemGroup>

<!-- NuGet properties -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,8 @@
<value>Crash dump</value>
</data>
<data name="CrashDumpFileNameOptionDescription" xml:space="preserve">
<value>Specify the name of the dump file</value>
<value>Specify the name of the dump file.
Supports the following placeholders: {pname} (process executable name, deferred to the .NET runtime as "%e"), {pid} (process ID, deferred to the .NET runtime as "%p"), {asm} (entry assembly name), {tfm} (target framework moniker), {time} (timestamp when the crashdump environment is configured). The legacy %p / %e / %h / %t tokens consumed directly by the .NET runtime's createdump are also supported.</value>
</data>
<data name="CrashDumpInvalidEnvironmentVariableValueErrorMessage" xml:space="preserve">
<value>Environment variable '{0}' should have been set to '{1}' but value is '{2}'</value>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,9 @@
<note />
</trans-unit>
<trans-unit id="CrashDumpFileNameOptionDescription">
<source>Specify the name of the dump file</source>
<target state="translated">Zadejte název souboru výpisu paměti.</target>
<source>Specify the name of the dump file.
Supports the following placeholders: {pname} (process executable name, deferred to the .NET runtime as "%e"), {pid} (process ID, deferred to the .NET runtime as "%p"), {asm} (entry assembly name), {tfm} (target framework moniker), {time} (timestamp when the crashdump environment is configured). The legacy %p / %e / %h / %t tokens consumed directly by the .NET runtime's createdump are also supported.</source>
<target state="needs-review-translation">Zadejte název souboru výpisu paměti.</target>
<note />
</trans-unit>
<trans-unit id="CrashDumpInvalidEnvironmentVariableValueErrorMessage">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,9 @@
<note />
</trans-unit>
<trans-unit id="CrashDumpFileNameOptionDescription">
<source>Specify the name of the dump file</source>
<target state="translated">Namen der Speicherabbilddatei angeben</target>
<source>Specify the name of the dump file.
Supports the following placeholders: {pname} (process executable name, deferred to the .NET runtime as "%e"), {pid} (process ID, deferred to the .NET runtime as "%p"), {asm} (entry assembly name), {tfm} (target framework moniker), {time} (timestamp when the crashdump environment is configured). The legacy %p / %e / %h / %t tokens consumed directly by the .NET runtime's createdump are also supported.</source>
<target state="needs-review-translation">Namen der Speicherabbilddatei angeben</target>
<note />
</trans-unit>
<trans-unit id="CrashDumpInvalidEnvironmentVariableValueErrorMessage">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,9 @@
<note />
</trans-unit>
<trans-unit id="CrashDumpFileNameOptionDescription">
<source>Specify the name of the dump file</source>
<target state="translated">Especificar el nombre del archivo de volcado</target>
<source>Specify the name of the dump file.
Supports the following placeholders: {pname} (process executable name, deferred to the .NET runtime as "%e"), {pid} (process ID, deferred to the .NET runtime as "%p"), {asm} (entry assembly name), {tfm} (target framework moniker), {time} (timestamp when the crashdump environment is configured). The legacy %p / %e / %h / %t tokens consumed directly by the .NET runtime's createdump are also supported.</source>
<target state="needs-review-translation">Especificar el nombre del archivo de volcado</target>
<note />
</trans-unit>
<trans-unit id="CrashDumpInvalidEnvironmentVariableValueErrorMessage">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,9 @@
<note />
</trans-unit>
<trans-unit id="CrashDumpFileNameOptionDescription">
<source>Specify the name of the dump file</source>
<target state="translated">Spécifier le nom du fichier de vidage</target>
<source>Specify the name of the dump file.
Supports the following placeholders: {pname} (process executable name, deferred to the .NET runtime as "%e"), {pid} (process ID, deferred to the .NET runtime as "%p"), {asm} (entry assembly name), {tfm} (target framework moniker), {time} (timestamp when the crashdump environment is configured). The legacy %p / %e / %h / %t tokens consumed directly by the .NET runtime's createdump are also supported.</source>
<target state="needs-review-translation">Spécifier le nom du fichier de vidage</target>
<note />
</trans-unit>
<trans-unit id="CrashDumpInvalidEnvironmentVariableValueErrorMessage">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,9 @@
<note />
</trans-unit>
<trans-unit id="CrashDumpFileNameOptionDescription">
<source>Specify the name of the dump file</source>
<target state="translated">Specificare il nome del file di dump</target>
<source>Specify the name of the dump file.
Supports the following placeholders: {pname} (process executable name, deferred to the .NET runtime as "%e"), {pid} (process ID, deferred to the .NET runtime as "%p"), {asm} (entry assembly name), {tfm} (target framework moniker), {time} (timestamp when the crashdump environment is configured). The legacy %p / %e / %h / %t tokens consumed directly by the .NET runtime's createdump are also supported.</source>
<target state="needs-review-translation">Specificare il nome del file di dump</target>
<note />
</trans-unit>
<trans-unit id="CrashDumpInvalidEnvironmentVariableValueErrorMessage">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,9 @@
<note />
</trans-unit>
<trans-unit id="CrashDumpFileNameOptionDescription">
<source>Specify the name of the dump file</source>
<target state="translated">ダンプ ファイルの名前を指定する</target>
<source>Specify the name of the dump file.
Supports the following placeholders: {pname} (process executable name, deferred to the .NET runtime as "%e"), {pid} (process ID, deferred to the .NET runtime as "%p"), {asm} (entry assembly name), {tfm} (target framework moniker), {time} (timestamp when the crashdump environment is configured). The legacy %p / %e / %h / %t tokens consumed directly by the .NET runtime's createdump are also supported.</source>
<target state="needs-review-translation">ダンプ ファイルの名前を指定する</target>
<note />
</trans-unit>
<trans-unit id="CrashDumpInvalidEnvironmentVariableValueErrorMessage">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,9 @@
<note />
</trans-unit>
<trans-unit id="CrashDumpFileNameOptionDescription">
<source>Specify the name of the dump file</source>
<target state="translated">덤프 파일의 이름 지정</target>
<source>Specify the name of the dump file.
Supports the following placeholders: {pname} (process executable name, deferred to the .NET runtime as "%e"), {pid} (process ID, deferred to the .NET runtime as "%p"), {asm} (entry assembly name), {tfm} (target framework moniker), {time} (timestamp when the crashdump environment is configured). The legacy %p / %e / %h / %t tokens consumed directly by the .NET runtime's createdump are also supported.</source>
<target state="needs-review-translation">덤프 파일의 이름 지정</target>
<note />
</trans-unit>
<trans-unit id="CrashDumpInvalidEnvironmentVariableValueErrorMessage">
Expand Down
Loading