Skip to content
Closed
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
106 changes: 106 additions & 0 deletions docs/ArtifactNamingService.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# Artifact Naming Service

The artifact naming service provides a standardized way to generate consistent names and paths for test artifacts across all extensions.

## Features

### Template-Based Naming
Comment thread
Evangelink marked this conversation as resolved.

Use placeholders in angle brackets to create dynamic file names:

```text
<pname>_<pid>_<id>_hang.dmp
```
Comment thread
Evangelink marked this conversation as resolved.

Resolves to: `MyTests_12345_a1b2c3d4_hang.dmp`

### Complex Path Templates
Comment thread
Evangelink marked this conversation as resolved.

Create structured directory layouts:

```text
<root>/artifacts/<os>/<asm>/dumps/<pname>_<pid>_<tfm>_<time>.dmp
```
Comment thread
Evangelink marked this conversation as resolved.

Resolves to: `c:/myproject/artifacts/linux/MyTests/dumps/my-child-process_10001_net9.0_2025-09-22T13:49:34.dmp`

### Available Placeholders

| Placeholder | Description | Example |
|-------------|-------------|---------|
| `<pname>` | Name of the process | `MyTests` |
| `<pid>` | Process ID | `12345` |
| `<id>` | Short random identifier (8 chars) | `a1b2c3d4` |
| `<os>` | Operating system | `windows`, `linux`, `macos` |
| `<asm>` | Assembly name | `MyTests` |
| `<tfm>` | Target framework moniker | `net9.0`, `net8.0` |
| `<time>` | Timestamp (1-second precision) | `2025-09-22T13:49:34` |
| `<root>` | Project root directory | Found via solution/git/working dir |

### Backward Compatibility
Comment thread
Evangelink marked this conversation as resolved.

Legacy patterns are still supported:

```csharp
// Old pattern
"myfile_%p.dmp"

// Works with legacy support
service.ResolveTemplateWithLegacySupport("myfile_%p.dmp",
legacyReplacements: new Dictionary<string, string> { ["%p"] = "12345" });
```

### Custom Replacements
Comment thread
Evangelink marked this conversation as resolved.

Override default values for specific scenarios:

```csharp
// When dumping a different process than the test host
var customReplacements = new Dictionary<string, string>
{
["pname"] = "Notepad",
["pid"] = "1111"
};

string result = service.ResolveTemplate("<pname>_<pid>.dmp", customReplacements);
// Result: "Notepad_1111.dmp"
```

## Usage in Extensions

Extensions can use the service through dependency injection:

```csharp
public class MyExtension
{
private readonly IArtifactNamingService _artifactNamingService;

public MyExtension(IServiceProvider serviceProvider)
{
_artifactNamingService = serviceProvider.GetArtifactNamingService();
}

public void CreateArtifact(string template)
{
string fileName = _artifactNamingService.ResolveTemplate(template);
// Use fileName for artifact creation
}
}
```

## Hang Dump Integration

The hang dump extension now uses the artifact naming service and supports both legacy and modern patterns:

```text
# Legacy pattern (still works)
--hangdump-filename "mydump_%p.dmp"

# New template pattern
--hangdump-filename "<pname>_<pid>_<id>_hang.dmp"

# Complex path template
--hangdump-filename "<root>/dumps/<os>/<pname>_<pid>_<time>.dmp"
```

This provides consistent artifact naming across all extensions while maintaining backward compatibility.
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ public static void AddHangDumpProvider(this ITestApplicationBuilder builder)
serviceProvider.GetLoggerFactory(),
serviceProvider.GetConfiguration(),
serviceProvider.GetProcessHandler(),
serviceProvider.GetClock()));
serviceProvider.GetClock(),
serviceProvider.GetArtifactNamingService()));

builder.TestHostControllers.AddEnvironmentVariableProvider(serviceProvider
=> new HangDumpEnvironmentVariableProvider(serviceProvider.GetCommandLineOptions(), hangDumpConfiguration));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
using Microsoft.Testing.Platform.Logging;
using Microsoft.Testing.Platform.Messages;
using Microsoft.Testing.Platform.OutputDevice;
using Microsoft.Testing.Platform.Services;

#if NETCOREAPP
using Microsoft.Diagnostics.NETCore.Client;
Expand Down Expand Up @@ -45,6 +46,7 @@ internal sealed class HangDumpProcessLifetimeHandler : ITestHostProcessLifetimeH
private readonly ILogger<HangDumpProcessLifetimeHandler> _logger;
private readonly ManualResetEventSlim _waitConsumerPipeName = new(false);
private readonly List<string> _dumpFiles = [];
private readonly IArtifactNamingService _artifactNamingService;

private TimeSpan? _activityTimerValue;
private Timer? _activityTimer;
Expand All @@ -66,7 +68,8 @@ public HangDumpProcessLifetimeHandler(
ILoggerFactory loggerFactory,
IConfiguration configuration,
IProcessHandler processHandler,
IClock clock)
IClock clock,
IArtifactNamingService artifactNamingService)
{
_logger = loggerFactory.CreateLogger<HangDumpProcessLifetimeHandler>();
_traceEnabled = _logger.IsEnabled(LogLevel.Trace);
Expand All @@ -79,6 +82,7 @@ public HangDumpProcessLifetimeHandler(
_configuration = configuration;
_processHandler = processHandler;
_clock = clock;
_artifactNamingService = artifactNamingService;
}

public string Uid => nameof(HangDumpProcessLifetimeHandler);
Expand Down Expand Up @@ -119,10 +123,10 @@ public async Task BeforeTestHostProcessStartAsync(CancellationToken cancellation

_waitConnectionTask = _task.Run(
async () =>
{
await _logger.LogDebugAsync($"Waiting for connection to {_singleConnectionNamedPipeServer.PipeName.Name}").ConfigureAwait(false);
await _singleConnectionNamedPipeServer.WaitConnectionAsync(cancellationToken).TimeoutAfterAsync(TimeoutHelper.DefaultHangTimeSpanTimeout, cancellationToken).ConfigureAwait(false);
}, cancellationToken);
{
await _logger.LogDebugAsync($"Waiting for connection to {_singleConnectionNamedPipeServer.PipeName.Name}").ConfigureAwait(false);
await _singleConnectionNamedPipeServer.WaitConnectionAsync(cancellationToken).TimeoutAfterAsync(TimeoutHelper.DefaultHangTimeSpanTimeout, cancellationToken).ConfigureAwait(false);
}, cancellationToken);
}

private async Task<IResponse> CallbackAsync(IRequest request)
Expand Down Expand Up @@ -325,7 +329,14 @@ private async Task TakeDumpAsync(IProcess process, CancellationToken cancellatio
ApplicationStateGuard.Ensure(_testHostProcessInformation is not null);
ApplicationStateGuard.Ensure(_dumpType is not null);

string finalDumpFileName = (_dumpFileNamePattern ?? $"{process.Name}_%p_hang.dmp").Replace("%p", process.Id.ToString(CultureInfo.InvariantCulture));
var customReplacements = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
["pname"] = process.Name,
["pid"] = process.Id.ToString(CultureInfo.InvariantCulture),
["%p"] = process.Id.ToString(CultureInfo.InvariantCulture),
};

string finalDumpFileName = _artifactNamingService.ResolveTemplate(_dumpFileNamePattern ?? $"{process.Name}_%p_hang.dmp", customReplacements);
finalDumpFileName = Path.Combine(_configuration.GetTestResultDirectory(), finalDumpFileName);

ApplicationStateGuard.Ensure(_namedPipeClient is not null);
Expand All @@ -349,7 +360,6 @@ private async Task TakeDumpAsync(IProcess process, CancellationToken cancellatio
}

await _logger.LogInformationAsync($"Creating dump filename {finalDumpFileName}").ConfigureAwait(false);

await _outputDisplay.DisplayAsync(new ErrorMessageOutputDeviceData(string.Format(CultureInfo.InvariantCulture, ExtensionResources.CreatingDumpFile, finalDumpFileName)), cancellationToken).ConfigureAwait(false);

#if NETCOREAPP
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ internal interface IEnvironment
string? ProcessPath { get; }
#endif

Version Version { get; }

string[] GetCommandLineArgs();

string? GetEnvironmentVariable(string name);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ internal sealed class SystemEnvironment : IEnvironment
public string? ProcessPath => Environment.ProcessPath;
#endif

public Version Version => Environment.Version;

public string[] GetCommandLineArgs() => Environment.GetCommandLineArgs();

public string? GetEnvironmentVariable(string name) => Environment.GetEnvironmentVariable(name);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,10 @@ public async Task<IHost> BuildAsync(
SystemMonitorAsyncFactory systemMonitorAsyncFactory = new();
serviceProvider.TryAddService(systemMonitorAsyncFactory);

// Add artifact naming service
ArtifactNamingService artifactNamingService = new(_testApplicationModuleInfo, systemEnvironment, systemClock, processHandler);
serviceProvider.TryAddService(artifactNamingService);

PlatformInformation platformInformation = new();
serviceProvider.AddService(platformInformation);

Expand Down
Loading
Loading