Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
118 commits
Select commit Hold shift + click to select a range
d51897b
Initial plan
Copilot Sep 22, 2025
eb60a65
Initial exploration and plan for artifact naming service
Copilot Sep 22, 2025
32a3424
Add artifact naming service implementation with backward compatibility
Copilot Sep 22, 2025
3f373e1
Add integration test for artifact naming service templates and fix mi…
Copilot Sep 22, 2025
4d2d2f3
Fix process name retrieval and improve integration test robustness
Copilot Sep 22, 2025
ca27413
Change process-name placeholder to pname for consistency with short n…
Copilot Dec 7, 2025
3ee129e
Change assembly placeholder to asm for consistency with short names
Copilot Dec 7, 2025
042d74d
Merge branch 'main' into copilot/fix-6586
Evangelink Dec 15, 2025
63a96be
Fixes
Evangelink Dec 15, 2025
9afa902
Apply suggestions from code review
Evangelink Dec 16, 2025
1de7408
Merge branch 'main' into copilot/fix-6586
Evangelink Dec 17, 2025
f934d44
Fixes
Evangelink Dec 17, 2025
8e9c2a7
More fixes
Evangelink Dec 17, 2025
ef9e0ea
Merge branch 'main' into copilot/fix-6586
Evangelink Dec 18, 2025
a15fe1f
Merge remote-tracking branch 'origin/main' into dev/amauryleve/artifa…
Evangelink Apr 26, 2026
0f47404
Merge main and fix code review issues in artifact naming service
Evangelink Apr 26, 2026
8307cf1
Change regex pattern from [^>]+ to .+? for better readability (equiva…
Copilot Apr 27, 2026
f00ad73
Make placeholder matching case-sensitive and change time format to yy…
Copilot Apr 27, 2026
20967d0
Improve test clarity: rename IGNORED to WRONG_CASE in case-sensitivit…
Copilot Apr 27, 2026
04ba2db
Merge remote-tracking branch 'origin/main' into dev/amauryleve/artifa…
Evangelink Apr 29, 2026
a6e2766
Remove <id> placeholder, reuse TargetFrameworkParser for TFM, use cas…
Evangelink Apr 29, 2026
d163cb6
Replace IArtifactNamingService DI with static ArtifactNamingHelper
Evangelink Apr 29, 2026
e6080f7
Fix review issues: restore dropped field declarations, remove dead Ve…
Evangelink Apr 29, 2026
c4856f3
Move ArtifactNamingHelper docs to microsoft.testing.platform folder
Evangelink Apr 29, 2026
20f6617
Address review comments: guard whitespace templates, strengthen integ…
Evangelink May 11, 2026
707cd22
Merge remote-tracking branch 'origin/dev/amauryleve/artifact-naming-s…
Evangelink May 11, 2026
2f52acd
Use real TFM from TargetFrameworkAttribute for <tfm> placeholder
Evangelink May 11, 2026
ef66b16
Make HangDump_TemplateFileName_CreateDump data-driven with <tfm>
Evangelink May 11, 2026
f4db2b2
Apply suggestion from @Evangelink
Evangelink May 11, 2026
ab86bcf
Address PR review comments
Evangelink May 11, 2026
302a7e8
Address PR review comments
Evangelink May 11, 2026
cc693ff
Ensure dump destination directory exists for template paths
Evangelink May 11, 2026
d23cb1d
Normalize dump path with GetFullPath and add subdirectory template in…
Evangelink May 11, 2026
8e0b53b
Apply suggestions from code review
Evangelink May 11, 2026
48f5f6e
Fix log accumulation across DynamicData test invocations (#7925)
Evangelink Apr 29, 2026
8dfae36
[Test Improver] Add unit tests for LoggerFactoryProxy (#7916)
Evangelink Apr 29, 2026
667ce70
[Lean Squad] feat(ci): Task 9 — fix elan SHA256, cache ordering, cond…
Evangelink Apr 29, 2026
7cb2ac1
Add code fix for MSTEST0042 — DuplicateDataRow (#7896)
Copilot Apr 29, 2026
67e75a9
[Test Improver] Add unit tests for PasteArguments.AppendArgument (#7888)
Evangelink Apr 29, 2026
98f2402
Add code duplication analysis workflow (#7874)
Evangelink Apr 29, 2026
6954767
[Lean Squad] feat(fv): Task 2 — informal spec for CommandLineParseRes…
Evangelink Apr 29, 2026
ae80d6d
File Diet: Split CollectionAssert.cs into focused partial-class files…
Copilot Apr 29, 2026
76e55bd
Add Repo Historian workflow and wire reviewers to history data (#7873)
Evangelink Apr 29, 2026
0cdc09d
Convert ExitCodes from static class to enum (#7876)
Evangelink Apr 29, 2026
05e25c1
Fix minor inconsistencies in analyzer/fixer naming and documentation …
Copilot Apr 29, 2026
eb26b5b
Eliminate delegate allocation in AbortForMaxFailedTestsExtension (#7923)
Copilot Apr 29, 2026
30276ff
Harden Copilot workflow integrity, lockdown, and toolset configuratio…
Evangelink Apr 29, 2026
f625873
[Efficiency Improver] perf: eliminate yield-iterator state machine in…
Evangelink Apr 29, 2026
f0927bd
[Efficiency Improver] perf: skip PropertyBag construction in BFSTestN…
Evangelink Apr 29, 2026
24acd67
[Lean Squad] feat(fv): Task 2 — informal spec for CommandLineParser.P…
Evangelink Apr 29, 2026
6e63de7
[main] Update dependencies from microsoft/testfx (#7945)
dotnet-maestro[bot] Apr 30, 2026
6077e9d
[main] Update dependencies from dotnet/arcade (#7944)
dotnet-maestro[bot] Apr 30, 2026
82c0c24
[main] Update dependencies from devdiv/DevDiv/vs-code-coverage (#7946)
dotnet-maestro[bot] Apr 30, 2026
db698b8
Localized file check-in by OneLocBuild Task: Build definition ID 1218…
dotnet-bot Apr 30, 2026
63e7509
Refactor `TestNodeProperties.cs` into focused single-responsibility f…
Copilot Apr 30, 2026
6d1fe5f
[Lean Squad] feat(ci): Task 9 — FV docs validation workflow + updated…
Evangelink Apr 30, 2026
3d6d158
[docs] Update glossary - daily scan (#7960)
Evangelink Apr 30, 2026
5b44fae
[Lean Squad] feat(fv): Task 2 — informal spec for TreeNodeFilter.Matc…
Evangelink Apr 30, 2026
b248c13
[Perf Improver] perf: eliminate LINQ iterator allocations in GetTestC…
Evangelink Apr 30, 2026
bc1372e
Add code fix for MSTEST0060 — DuplicateTestMethodAttribute (#7894)
Copilot Apr 30, 2026
3ca1d11
Localized file check-in by OneLocBuild Task: Build definition ID 1218…
dotnet-bot Apr 30, 2026
8ffe187
[main] Update dependencies from microsoft/testfx (#7972)
dotnet-maestro[bot] May 1, 2026
1704a3f
[main] Update dependencies from dotnet/arcade (#7971)
dotnet-maestro[bot] May 1, 2026
437b705
[main] Update dependencies from microsoft/testfx (#7989)
dotnet-maestro[bot] May 2, 2026
5c516dc
[main] Update dependencies from dotnet/arcade (#7988)
dotnet-maestro[bot] May 2, 2026
1bc2d40
[main] Update dependencies from microsoft/testfx (#7997)
dotnet-maestro[bot] May 3, 2026
0df1667
[main] Update dependencies from microsoft/testfx (#8005)
dotnet-maestro[bot] May 4, 2026
1526cff
[main] Update dependencies from devdiv/DevDiv/vs-code-coverage (#8022)
dotnet-maestro[bot] May 5, 2026
876d97f
[main] Bump Microsoft.CodeAnalysis.BannedApiAnalyzers from 5.5.0-2.26…
dependabot[bot] May 5, 2026
625ca0b
[main] Bump AwesomeAssertions from 9.3.0 to 9.4.0 (#8008)
dependabot[bot] May 5, 2026
eaacf8f
[main] Update dependencies from devdiv/DevDiv/vs-code-coverage (#8038)
dotnet-maestro[bot] May 6, 2026
1bf932e
[main] Update dependencies from microsoft/testfx (#8039)
dotnet-maestro[bot] May 6, 2026
7e67493
[main] Update dependencies from dotnet/arcade (#8049)
dotnet-maestro[bot] May 7, 2026
209b625
[main] Update dependencies from microsoft/testfx (#8050)
dotnet-maestro[bot] May 7, 2026
5583b45
[main] Update dependencies from devdiv/DevDiv/vs-code-coverage (#8051)
dotnet-maestro[bot] May 7, 2026
7b7caf6
[main] Update dependencies from dotnet/arcade (#8061)
dotnet-maestro[bot] May 8, 2026
64a1e75
[main] Update dependencies from microsoft/testfx (#8062)
dotnet-maestro[bot] May 8, 2026
cb15a07
[main] Update dependencies from microsoft/testfx (#8072)
dotnet-maestro[bot] May 9, 2026
97d0f53
[main] Bump dotnet-coverage from 17.14.2 to 18.7.0 (#8009)
dependabot[bot] May 11, 2026
18ff01c
Recompile GH workflows (#8087)
Evangelink May 11, 2026
311babc
[docs] Update glossary - daily scan (#8089)
Evangelink May 11, 2026
b652a7f
[main] Bump Microsoft.CodeAnalysis.Analyzers from 3.11.0 to 5.3.0 (#8…
dependabot[bot] May 11, 2026
3ce41dc
Fix typos, formatting, and class names in AnalyzerReleases.Shipped.md…
Copilot May 11, 2026
b8fe52c
[docs] Update glossary - weekly full scan (#8090)
Evangelink May 11, 2026
17f6a80
Fix O(n²) output accumulation in data-driven tests causing large TRX …
Evangelink May 11, 2026
810ac6b
Refactor TestMethodInfo.cs into focused partial class files (#7985)
Copilot May 11, 2026
4a99647
[code-simplifier] Remove redundant `.ToString()` in `TimingInfo.ToStr…
Evangelink May 11, 2026
e110ba1
[main] Bump FSharp.Core from 9.0.202 to 11.0.100 (#8010)
dependabot[bot] May 11, 2026
b19e79e
Localized file check-in by OneLocBuild Task: Build definition ID 1218…
dotnet-bot May 11, 2026
89ae441
[Lean Squad] feat: [Lean Squad] informal spec for EnvironmentVariable…
Evangelink May 11, 2026
f7f6f85
Bump nerdbank deps (#8096)
Evangelink May 11, 2026
a4818f0
MSTEST0007: report concrete condition attribute instead of ConditionB…
Copilot May 11, 2026
b5f3b3e
Fix typo: 'CompositeExtensonFactory' -> 'CompositeExtensionFactory' (…
Evangelink May 11, 2026
8c20a2f
Analyzer catalog hygiene: retire MSTEST0039 constant and replace unac…
Copilot May 11, 2026
b366cc4
Bump OTel (#8099)
Evangelink May 11, 2026
3615c41
[Lean Squad] feat(fv): Task 2 + Task 9 — CommandLineOptionsValidator …
Evangelink May 11, 2026
77f0258
Localized file check-in by OneLocBuild Task: Build definition ID 1218…
dotnet-bot May 11, 2026
90699de
Fix Hash input in _GenerateVersionSourceFileCache target (#8102)
Evangelink May 11, 2026
9ad9a0c
[Lean Squad] feat(fv): Task 3+9 — Lean 4 formal spec for TreeNodeFilt…
Evangelink May 11, 2026
009132b
[Lean Squad] feat(fv): Task 1 — expand TARGETS.md to 17 targets; add …
Evangelink May 11, 2026
2404223
[Lean Squad] feat(fv): Task 2 — informal spec for PasteArguments.Appe…
Evangelink May 11, 2026
beb7aa5
[Lean Squad] Task 9: CI automation improvements — TARGETS.md expansio…
Evangelink May 11, 2026
5f88e21
Remove Lean formal verification agentic workflow (#8121)
Evangelink May 11, 2026
37f2673
[Efficiency Improver] perf: avoid iterator allocation in GetRetryAttr…
Evangelink May 11, 2026
167522a
Update changelogs for v4.2.2 release (#8132)
Evangelink May 11, 2026
0b832eb
[Test Improver] Add unit tests for LoggingManager.BuildAsync (#8124)
Evangelink May 11, 2026
d51f424
Bump test deps (#8110)
Evangelink May 11, 2026
4724fcc
[Test Improver] Add unit tests for ExtensionValidationHelper.Validate…
Evangelink May 11, 2026
a297469
Add code fix for MSTEST0031 — DoNotUseSystemDescriptionAttribute (#7898)
Copilot May 11, 2026
0aa8d87
Merge branch 'main' into dev/amauryleve/artifact-naming-service
Evangelink May 12, 2026
5f8bb21
Address PR review comments: improve tfm resolution and null handling
Evangelink May 12, 2026
a3256e5
Merge branch 'main' into dev/amauryleve/artifact-naming-service
Evangelink May 12, 2026
50f8d99
Address second round of PR review comments
Evangelink May 12, 2026
c77ef7d
Merge branch 'dev/amauryleve/artifact-naming-service' of https://gith…
Evangelink May 12, 2026
446c2af
Address third round of PR review comments
Evangelink May 12, 2026
369256b
Use OS-appropriate path comparison for traversal guard
Evangelink May 12, 2026
7f02e33
Use syntax suggested by nohwnd
Evangelink May 12, 2026
67401d9
Address review feedback: drop {os}, source-gen regex, centralize valu…
Evangelink May 13, 2026
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 .config/dotnet-tools.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@
"rollForward": false
}
}
}
}
54 changes: 54 additions & 0 deletions docs/microsoft.testing.platform/ArtifactNamingHelper.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Artifact Naming Helper

The `ArtifactNamingHelper` is a shared static helper that provides a standardized way to generate consistent names and paths for test artifacts. It is compiled into each extension that needs it via file linking (no service registration or IVT required).

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

Use placeholders in curly braces to create dynamic file names. Placeholder matching is **case-sensitive** — use lowercase placeholder names (e.g., `{pname}`, not `{PName}`).

```text
{pname}_{pid}_{time}_hang.dmp
```

Resolves to: `MyTests_12345_2025-09-22_13-49-34.0000000_hang.dmp`

## Available Placeholders

| Placeholder | Description | Example |
|-------------|-------------|---------|
| `{pname}` | Name of the process | `MyTests` |
| `{pid}` | Process ID | `12345` |
| `{asm}` | Assembly name (entry assembly, or `unknown` if unavailable) | `MyTests` |
| `{tfm}` | Target framework moniker (best-effort, detected at runtime — may differ from the build-time TFM, e.g. a `net462` assembly running on .NET Framework 4.8 reports `net481`) | `net9.0`, `net8.0` |
| `{time}` | Timestamp (high precision) | `2025-09-22_13-49-34.0000000` |

## Backward Compatibility

Legacy patterns like `%p` continue to work in the hang dump extension.

## Custom Replacements

Override default values for specific scenarios:

```csharp
var replacements = new Dictionary<string, string>
{
["pname"] = "Notepad",
["pid"] = "1111"
};

string result = ArtifactNamingHelper.ResolveTemplate("{pname}_{pid}.dmp", replacements);
// Result: "Notepad_1111.dmp"
```

## Hang Dump Integration

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

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

# New template pattern
--hangdump-filename "{pname}_{pid}_{time}_hang.dmp"
```
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 @@ -118,10 +119,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 @@ -307,14 +308,40 @@ 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));
finalDumpFileName = Path.Combine(_configuration.GetTestResultDirectory(), finalDumpFileName);
string processId = process.Id.ToString(CultureInfo.InvariantCulture);
Dictionary<string, string> replacements = ArtifactNamingHelper.GetStandardReplacements(process.Name, processId, _clock.UtcNow);

string pattern = _dumpFileNamePattern ?? $"{process.Name}_%p_hang.dmp";

// First resolve {placeholder} templates, then handle legacy %p pattern for backward compatibility.
string finalDumpFileName = ArtifactNamingHelper.ResolveTemplate(pattern, replacements)
.Replace("%p", processId);
string resultsDirectory = Path.GetFullPath(_configuration.GetTestResultDirectory());
finalDumpFileName = Path.GetFullPath(Path.Combine(resultsDirectory, finalDumpFileName));

// Reject resolved paths that escape the results directory (e.g. rooted paths or ".." segments).
// Append a trailing separator to prevent sibling-directory bypass (e.g. "/tmp/results" vs "/tmp/results-evil").
// Use case-insensitive comparison on Windows where paths are case-insensitive.
StringComparison pathComparison = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? StringComparison.OrdinalIgnoreCase
: StringComparison.Ordinal;
string separatorStr = Path.DirectorySeparatorChar.ToString();
string resultsDirectoryGuard = resultsDirectory.EndsWith(separatorStr, StringComparison.Ordinal)
? resultsDirectory
: resultsDirectory + separatorStr;
if (!finalDumpFileName.StartsWith(resultsDirectoryGuard, pathComparison))
{
throw new InvalidOperationException($"The resolved dump file path '{finalDumpFileName}' is outside the results directory '{resultsDirectory}'. Ensure --hangdump-filename is a relative path without '..' segments.");
Comment thread
Evangelink marked this conversation as resolved.
}

// Ensure the destination directory exists (templates may include directory separators, e.g. {asm}/{pname}).
Directory.CreateDirectory(Path.GetDirectoryName(finalDumpFileName)!);
Comment thread
Evangelink marked this conversation as resolved.
Comment thread
Evangelink marked this conversation as resolved.

Comment thread
Evangelink marked this conversation as resolved.
ApplicationStateGuard.Ensure(_namedPipeClient is not null);
GetInProgressTestsResponse tests = await _namedPipeClient.RequestReplyAsync<GetInProgressTestsRequest, GetInProgressTestsResponse>(new GetInProgressTestsRequest(), cancellationToken).ConfigureAwait(false);
if (tests.Tests.Length > 0)
{
string hangTestsFileName = Path.Combine(_configuration.GetTestResultDirectory(), Path.ChangeExtension(Path.GetFileName(finalDumpFileName), ".log"));
string hangTestsFileName = Path.ChangeExtension(finalDumpFileName, ".log");
using (FileStream fs = File.OpenWrite(hangTestsFileName))
using (StreamWriter sw = new(fs))
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ This package extends Microsoft Testing Platform to provide an implementation of
<Compile Include="$(RepoRoot)src\Platform\Microsoft.Testing.Platform\Helpers\ExitCodes.cs" Link="Helpers\ExitCodes.cs" />
<Compile Include="$(RepoRoot)src\Platform\Microsoft.Testing.Platform\Helpers\TimeoutHelper.cs" Link="Helpers\TimeoutHelper.cs" />
<Compile Include="$(RepoRoot)src\Platform\Microsoft.Testing.Platform\Helpers\TimeSpanParser.cs" Link="Helpers\TimeSpanParser.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 package layout -->
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Microsoft.CodeAnalysis;
using Microsoft.Testing.Platform.OutputDevice;

namespace Microsoft.Testing.Platform.Services;

[Embedded]
internal static partial class ArtifactNamingHelper
{
#if NET
private static readonly Regex TemplateFieldRegex = GetTemplateFieldRegex();

[GeneratedRegex(@"\{([^}]+)\}")]
private static partial Regex GetTemplateFieldRegex();
#else
private static readonly Regex TemplateFieldRegex = new(@"\{([^}]+)\}", RegexOptions.Compiled);
Comment thread
Evangelink marked this conversation as resolved.
#endif

/// <summary>
/// Builds the standard set of placeholder replacements for artifact naming.
/// Consumers pass process-specific values; the helper resolves the rest (asm, tfm).
/// </summary>
/// <param name="processName">The name of the process (resolves <c>{pname}</c>).</param>
/// <param name="processId">The process ID (resolves <c>{pid}</c>).</param>
/// <param name="timestamp">The timestamp to use (resolves <c>{time}</c>).</param>
public static Dictionary<string, string> GetStandardReplacements(string processName, string processId, DateTimeOffset timestamp)
{
var replacements = new Dictionary<string, string>(StringComparer.Ordinal)
{
["pname"] = processName,
["pid"] = processId,
["time"] = timestamp.ToString("yyyy-MM-dd_HH-mm-ss.fffffff", CultureInfo.InvariantCulture),
};

string? asmName = Assembly.GetEntryAssembly()?.GetName().Name;
replacements["asm"] = asmName ?? "unknown";

string? tfm = TargetFrameworkParser.GetShortTargetFramework(Assembly.GetEntryAssembly()?.GetCustomAttribute<TargetFrameworkAttribute>()?.FrameworkDisplayName)
?? TargetFrameworkParser.GetShortTargetFramework(RuntimeInformation.FrameworkDescription);
replacements["tfm"] = tfm ?? "unknown";

return replacements;
}

/// <summary>
/// Resolves a template pattern by replacing {placeholder} tokens with values from the provided dictionary.
/// Unknown placeholders are preserved as-is. Placeholder matching is always case-sensitive (ordinal).
/// </summary>
Comment thread
Evangelink marked this conversation as resolved.
public static string ResolveTemplate(string template, IDictionary<string, string>? replacements = null)
{
if (RoslynString.IsNullOrWhiteSpace(template))
Comment thread
Evangelink marked this conversation as resolved.
{
throw new ArgumentException("Template cannot be null, empty, or whitespace.", nameof(template));
}

if (replacements is null || replacements.Count == 0)
{
return template;
}

// Ensure ordinal (case-sensitive) comparison regardless of the caller-provided dictionary comparer.
Dictionary<string, string> ordinalReplacements = replacements is Dictionary<string, string> dict && dict.Comparer == StringComparer.Ordinal
? dict
: new Dictionary<string, string>(replacements, StringComparer.Ordinal);

return TemplateFieldRegex.Replace(template, match =>
{
string fieldName = match.Groups[1].Value;
return ordinalReplacements.TryGetValue(fieldName, out string? value) ? value : match.Value;
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ public async Task HangDump_DefaultSetting_CreateDump(string tfm)
$"--hangdump --hangdump-timeout 8s --results-directory {resultDirectory}",
new Dictionary<string, string?>
{
{ "SLEEPTIMEMS1", "4000" },
{ "SLEEPTIMEMS2", "600000" },
{ "SLEEPTIMEMS1", "4000" },
{ "SLEEPTIMEMS2", "600000" },
},
cancellationToken: TestContext.CancellationToken);
testHostResult.AssertExitCodeIs(ExitCode.TestHostProcessExitedNonGracefully);
Expand Down Expand Up @@ -104,6 +104,88 @@ public async Task HangDump_PathWithSpaces_CreateDump()
Assert.IsNotNull(dumpFile, $"Dump file not found '{TargetFrameworks.NetCurrent}'\n{testHostResult}'");
}

[DynamicData(nameof(TargetFrameworks.AllForDynamicData), typeof(TargetFrameworks))]
[TestMethod]
public async Task HangDump_TemplateFileName_CreateDump(string tfm)
Comment thread
Evangelink marked this conversation as resolved.
{
string resultDirectory = Path.Combine(AssetFixture.TargetAssetPath, Guid.NewGuid().ToString("N"), tfm);
var testHost = TestInfrastructure.TestHost.LocateFrom(AssetFixture.TargetAssetPath, "HangDump", tfm);
TestHostResult testHostResult = await testHost.ExecuteAsync(
$"--hangdump --hangdump-timeout 8s --hangdump-filename {{pname}}_{{pid}}_{{tfm}}_{{time}}_hang.dmp --results-directory {resultDirectory}",
new Dictionary<string, string?>
{
["SLEEPTIMEMS1"] = "4000",
["SLEEPTIMEMS2"] = "20000",
},
cancellationToken: TestContext.CancellationToken);
testHostResult.AssertExitCodeIs(ExitCode.TestHostProcessExitedNonGracefully);

// Verify the dump file uses the template format
string[] dumpFiles = Directory.GetFiles(resultDirectory, "*_hang.dmp", SearchOption.AllDirectories);
string dumpFile = Assert.ContainsSingle(
dumpFiles,
$"Expected single dump file in '{resultDirectory}'\n{testHostResult}'");
string fileName = Path.GetFileNameWithoutExtension(dumpFile);

// File should match pattern: {pname}_{pid}_{tfm}_{time}_hang
// where {tfm} is e.g. net10.0, net8.0, net462
// and {time} is yyyy-MM-dd_HH-mm-ss.fffffff
Assert.MatchesRegex(@"^.+_\d+_net[\w.]+_\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}\.\d{7}_hang$", fileName,
$"File name should match '{{pname}}_{{pid}}_{{tfm}}_{{time}}_hang' pattern. Actual: {fileName}");

// Verify the TFM segment matches the expected target framework
Assert.Contains($"_{tfm}_", fileName, $"File name should contain the TFM '{tfm}'. Actual: {fileName}");
}

[DynamicData(nameof(TargetFrameworks.AllForDynamicData), typeof(TargetFrameworks))]
[TestMethod]
public async Task HangDump_TemplateFileNameWithSubdirectory_CreateDump(string tfm)
{
string resultDirectory = Path.Combine(AssetFixture.TargetAssetPath, Guid.NewGuid().ToString("N"), tfm);
var testHost = TestInfrastructure.TestHost.LocateFrom(AssetFixture.TargetAssetPath, "HangDump", tfm);
TestHostResult testHostResult = await testHost.ExecuteAsync(
$"--hangdump --hangdump-timeout 8s --hangdump-filename {{asm}}/{{pname}}_{{pid}}_hang.dmp --results-directory {resultDirectory}",
new Dictionary<string, string?>
{
["SLEEPTIMEMS1"] = "4000",
["SLEEPTIMEMS2"] = "20000",
},
cancellationToken: TestContext.CancellationToken);
testHostResult.AssertExitCodeIs(ExitCode.TestHostProcessExitedNonGracefully);

// Verify the dump file was created inside a subdirectory named after the assembly
string[] dumpFiles = Directory.GetFiles(resultDirectory, "*_hang.dmp", SearchOption.AllDirectories);
string dumpFile = Assert.ContainsSingle(
dumpFiles,
$"Expected single dump file in '{resultDirectory}'\n{testHostResult}'");

// The dump file should be in a subdirectory named after the assembly
string? parentDir = Path.GetDirectoryName(dumpFile);
Assert.IsNotNull(parentDir);
Assert.AreNotEqual(resultDirectory, parentDir, "Dump file should be in a subdirectory created from the {asm} placeholder");
Assert.AreEqual("HangDump", Path.GetFileName(parentDir),
$"Subdirectory should be named after the assembly ('HangDump'). Actual: {Path.GetFileName(parentDir)}");
}
Comment thread
Evangelink marked this conversation as resolved.

[TestMethod]
public async Task HangDump_TemplateWithPathTraversal_RejectsAndFails()
{
string resultDirectory = Path.Combine(AssetFixture.TargetAssetPath, Guid.NewGuid().ToString("N"), TargetFrameworks.NetCurrent);
var testHost = TestInfrastructure.TestHost.LocateFrom(AssetFixture.TargetAssetPath, "HangDump", TargetFrameworks.NetCurrent);
TestHostResult testHostResult = await testHost.ExecuteAsync(
$"--hangdump --hangdump-timeout 8s --hangdump-filename ../../outside/{{pname}}_hang.dmp --results-directory {resultDirectory}",
new Dictionary<string, string?>
{
["SLEEPTIMEMS1"] = "4000",
["SLEEPTIMEMS2"] = "20000",
},
cancellationToken: TestContext.CancellationToken);

// The path-traversal guard should cause a non-graceful exit and no dump file should be created outside the results directory.
testHostResult.AssertExitCodeIs(ExitCode.TestHostProcessExitedNonGracefully);
Assert.Contains("outside the results directory", testHostResult.StandardOutput);
}

[DataRow("Mini")]
[DataRow("Heap")]
[DataRow("Triage")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
<Compile Include="$(RepoRoot)src\Platform\Microsoft.Testing.Platform\Helpers\TimeoutHelper.cs" Link="Helpers\TimeoutHelper.cs" />
<Compile Include="$(RepoRoot)src\Platform\Microsoft.Testing.Platform\Helpers\TimeSpanParser.cs" Link="Helpers\TimeSpanParser.cs" />
<Compile Include="$(RepoRoot)src\Platform\Microsoft.Testing.Platform\OutputDevice\TargetFrameworkParser.cs" Link="Helpers\TargetFrameworkParser.cs" />
<Compile Include="$(RepoRoot)src\Platform\Microsoft.Testing.Platform\Services\ArtifactNamingHelper.cs" Link="Services\ArtifactNamingHelper.cs" />
</ItemGroup>

<ItemGroup>
Expand Down
Loading
Loading