Skip to content
Draft
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 @@ -352,19 +352,11 @@ private async Task<string> CopyArtifactIntoTrxDirectoryAndReturnHrefValueAsync(F
string fileName = artifact.Name;

string destination = Path.Combine(artifactDirectory, fileName);
int nameCounter = 0;

// If the file already exists, append a number to the end of the file name
while (true)
for (int nameCounter = 1; File.Exists(destination) && nameCounter <= 10; nameCounter++)
{
if (File.Exists(destination))
{
nameCounter++;
destination = Path.Combine(artifactDirectory, $"{Path.GetFileNameWithoutExtension(fileName)}_{nameCounter}{Path.GetExtension(fileName)}");
continue;
}

break;
destination = Path.Combine(artifactDirectory, $"{Path.GetFileNameWithoutExtension(fileName)}_{nameCounter}{Path.GetExtension(fileName)}");
}

Copy link

Copilot AI Feb 22, 2026

Choose a reason for hiding this comment

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

After the bounded loop completes (when nameCounter exceeds 10), the code calls CopyFileAsync even if a file still exists at the destination path. While CopyFileAsync creates a new file with FileMode.Create (which will overwrite), it would be better to verify that a unique filename was found or handle the case explicitly for clarity and consistency with error handling.

Suggested change
if (File.Exists(destination))
{
throw new IOException($"Could not generate a unique file name for artifact '{fileName}' in directory '{artifactDirectory}'.");
}

Copilot uses AI. Check for mistakes.
await CopyFileAsync(artifact, new FileInfo(destination)).ConfigureAwait(false);
Expand Down Expand Up @@ -536,7 +528,7 @@ private void AddResults(string testAppModule, XElement testRun, out XElement tes
resultFiles ??= new XElement("ResultFiles");
resultFiles.Add(new XElement(
"ResultFile",
new XAttribute("path", testFileArtifact.FileInfo.FullName)));
new XAttribute("path", testFileArtifact.FileInfo.Name)));
}

if (resultFiles is not null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -720,6 +720,9 @@ private static async Task<ITestFramework> BuildTestFrameworkAsync(TestFrameworkB
await RegisterAsServiceOrConsumerOrBothAsync(testFrameworkBuilderData.TestExecutionRequestInvoker, serviceProvider, dataConsumersBuilder).ConfigureAwait(false);
await RegisterAsServiceOrConsumerOrBothAsync(testFrameworkBuilderData.TestExecutionFilterFactory, serviceProvider, dataConsumersBuilder).ConfigureAwait(false);

// Register consumer that copies per-test file artifacts to the results directory.
dataConsumersBuilder.Add(new FileArtifactCopyDataConsumer(serviceProvider.GetConfiguration()));

// Create the test framework adapter
ITestFrameworkCapabilities testFrameworkCapabilities = serviceProvider.GetTestFrameworkCapabilities();
ITestFramework testFramework = testFrameworkBuilderData.TestFrameworkManager.TestFrameworkFactory(testFrameworkCapabilities, serviceProvider);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -746,4 +746,10 @@ Takes one argument as string in the format &lt;value&gt;[h|m|s] where 'value' is
<data name="WaitDebuggerAttachNotSupportedInBrowserErrorMessage" xml:space="preserve">
<value>Waiting for debugger to attach (TESTINGPLATFORM_WAIT_ATTACH_DEBUGGER=1) is not supported in browser environments.</value>
</data>
<data name="FileArtifactCopyDataConsumerDisplayName" xml:space="preserve">
<value>File artifact copy</value>
</data>
<data name="FileArtifactCopyDataConsumerDescription" xml:space="preserve">
<value>Copies per-test file artifacts to the test results directory.</value>
</data>
</root>
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,16 @@
<target state="translated">selhalo s {0} upozorněním(i).</target>
<note />
</trans-unit>
<trans-unit id="FileArtifactCopyDataConsumerDescription">
<source>Copies per-test file artifacts to the test results directory.</source>
<target state="new">Copies per-test file artifacts to the test results directory.</target>
<note />
</trans-unit>
<trans-unit id="FileArtifactCopyDataConsumerDisplayName">
<source>File artifact copy</source>
<target state="new">File artifact copy</target>
<note />
</trans-unit>
<trans-unit id="FinishedTestSession">
<source>Finished test session.</source>
<target state="translated">Testovací relace byla dokončena.</target>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,16 @@
<target state="translated">fehlerhaft mit {0} Warnung(en)</target>
<note />
</trans-unit>
<trans-unit id="FileArtifactCopyDataConsumerDescription">
<source>Copies per-test file artifacts to the test results directory.</source>
<target state="new">Copies per-test file artifacts to the test results directory.</target>
<note />
</trans-unit>
<trans-unit id="FileArtifactCopyDataConsumerDisplayName">
<source>File artifact copy</source>
<target state="new">File artifact copy</target>
<note />
</trans-unit>
<trans-unit id="FinishedTestSession">
<source>Finished test session.</source>
<target state="translated">Die Testsitzung wurde beendet.</target>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,16 @@
<target state="translated">error con {0} advertencias</target>
<note />
</trans-unit>
<trans-unit id="FileArtifactCopyDataConsumerDescription">
<source>Copies per-test file artifacts to the test results directory.</source>
<target state="new">Copies per-test file artifacts to the test results directory.</target>
<note />
</trans-unit>
<trans-unit id="FileArtifactCopyDataConsumerDisplayName">
<source>File artifact copy</source>
<target state="new">File artifact copy</target>
<note />
</trans-unit>
<trans-unit id="FinishedTestSession">
<source>Finished test session.</source>
<target state="translated">Finalizó la sesión de prueba.</target>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,16 @@
<target state="translated">a échoué avec {0} avertissement(s)</target>
<note />
</trans-unit>
<trans-unit id="FileArtifactCopyDataConsumerDescription">
<source>Copies per-test file artifacts to the test results directory.</source>
<target state="new">Copies per-test file artifacts to the test results directory.</target>
<note />
</trans-unit>
<trans-unit id="FileArtifactCopyDataConsumerDisplayName">
<source>File artifact copy</source>
<target state="new">File artifact copy</target>
<note />
</trans-unit>
<trans-unit id="FinishedTestSession">
<source>Finished test session.</source>
<target state="translated">Session de test terminée.</target>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,16 @@
<target state="translated">non riuscito con {0} avvisi</target>
<note />
</trans-unit>
<trans-unit id="FileArtifactCopyDataConsumerDescription">
<source>Copies per-test file artifacts to the test results directory.</source>
<target state="new">Copies per-test file artifacts to the test results directory.</target>
<note />
</trans-unit>
<trans-unit id="FileArtifactCopyDataConsumerDisplayName">
<source>File artifact copy</source>
<target state="new">File artifact copy</target>
<note />
</trans-unit>
<trans-unit id="FinishedTestSession">
<source>Finished test session.</source>
<target state="translated">Sessione di test completata.</target>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,16 @@
<target state="translated">{0} 件の警告付きで失敗しました</target>
<note />
</trans-unit>
<trans-unit id="FileArtifactCopyDataConsumerDescription">
<source>Copies per-test file artifacts to the test results directory.</source>
<target state="new">Copies per-test file artifacts to the test results directory.</target>
<note />
</trans-unit>
<trans-unit id="FileArtifactCopyDataConsumerDisplayName">
<source>File artifact copy</source>
<target state="new">File artifact copy</target>
<note />
</trans-unit>
<trans-unit id="FinishedTestSession">
<source>Finished test session.</source>
<target state="translated">テスト セッションを終了しました。</target>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,16 @@
<target state="translated">{0} 경고와 함께 실패</target>
<note />
</trans-unit>
<trans-unit id="FileArtifactCopyDataConsumerDescription">
<source>Copies per-test file artifacts to the test results directory.</source>
<target state="new">Copies per-test file artifacts to the test results directory.</target>
<note />
</trans-unit>
<trans-unit id="FileArtifactCopyDataConsumerDisplayName">
<source>File artifact copy</source>
<target state="new">File artifact copy</target>
<note />
</trans-unit>
<trans-unit id="FinishedTestSession">
<source>Finished test session.</source>
<target state="translated">테스트 세션을 마쳤습니다.</target>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,16 @@
<target state="translated">zakończono niepowodzeniem, z ostrzeżeniami w liczbie: {0}</target>
<note />
</trans-unit>
<trans-unit id="FileArtifactCopyDataConsumerDescription">
<source>Copies per-test file artifacts to the test results directory.</source>
<target state="new">Copies per-test file artifacts to the test results directory.</target>
<note />
</trans-unit>
<trans-unit id="FileArtifactCopyDataConsumerDisplayName">
<source>File artifact copy</source>
<target state="new">File artifact copy</target>
<note />
</trans-unit>
<trans-unit id="FinishedTestSession">
<source>Finished test session.</source>
<target state="translated">Zakończono sesję testą.</target>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,16 @@
<target state="translated">falhou com {0} aviso(s)</target>
<note />
</trans-unit>
<trans-unit id="FileArtifactCopyDataConsumerDescription">
<source>Copies per-test file artifacts to the test results directory.</source>
<target state="new">Copies per-test file artifacts to the test results directory.</target>
<note />
</trans-unit>
<trans-unit id="FileArtifactCopyDataConsumerDisplayName">
<source>File artifact copy</source>
<target state="new">File artifact copy</target>
<note />
</trans-unit>
<trans-unit id="FinishedTestSession">
<source>Finished test session.</source>
<target state="translated">Sessão de teste concluída.</target>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,16 @@
<target state="translated">сбой с предупреждениями ({0})</target>
<note />
</trans-unit>
<trans-unit id="FileArtifactCopyDataConsumerDescription">
<source>Copies per-test file artifacts to the test results directory.</source>
<target state="new">Copies per-test file artifacts to the test results directory.</target>
<note />
</trans-unit>
<trans-unit id="FileArtifactCopyDataConsumerDisplayName">
<source>File artifact copy</source>
<target state="new">File artifact copy</target>
<note />
</trans-unit>
<trans-unit id="FinishedTestSession">
<source>Finished test session.</source>
<target state="translated">Тестовый сеанс завершен.</target>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,16 @@
<target state="translated">{0} uyarıyla başarısız oldu</target>
<note />
</trans-unit>
<trans-unit id="FileArtifactCopyDataConsumerDescription">
<source>Copies per-test file artifacts to the test results directory.</source>
<target state="new">Copies per-test file artifacts to the test results directory.</target>
<note />
</trans-unit>
<trans-unit id="FileArtifactCopyDataConsumerDisplayName">
<source>File artifact copy</source>
<target state="new">File artifact copy</target>
<note />
</trans-unit>
<trans-unit id="FinishedTestSession">
<source>Finished test session.</source>
<target state="translated">Test oturumu bitti.</target>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,16 @@
<target state="translated">失败,出现 {0} 警告</target>
<note />
</trans-unit>
<trans-unit id="FileArtifactCopyDataConsumerDescription">
<source>Copies per-test file artifacts to the test results directory.</source>
<target state="new">Copies per-test file artifacts to the test results directory.</target>
<note />
</trans-unit>
<trans-unit id="FileArtifactCopyDataConsumerDisplayName">
<source>File artifact copy</source>
<target state="new">File artifact copy</target>
<note />
</trans-unit>
<trans-unit id="FinishedTestSession">
<source>Finished test session.</source>
<target state="translated">已完成测试会话。</target>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,16 @@
<target state="translated">失敗,有 {0} 個警告</target>
<note />
</trans-unit>
<trans-unit id="FileArtifactCopyDataConsumerDescription">
<source>Copies per-test file artifacts to the test results directory.</source>
<target state="new">Copies per-test file artifacts to the test results directory.</target>
<note />
</trans-unit>
<trans-unit id="FileArtifactCopyDataConsumerDisplayName">
<source>File artifact copy</source>
<target state="new">File artifact copy</target>
<note />
</trans-unit>
<trans-unit id="FinishedTestSession">
<source>Finished test session.</source>
<target state="translated">已完成測試會話。</target>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// 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.Testing.Platform.Configurations;
using Microsoft.Testing.Platform.Extensions;
using Microsoft.Testing.Platform.Extensions.Messages;
using Microsoft.Testing.Platform.Helpers;
using Microsoft.Testing.Platform.Resources;

namespace Microsoft.Testing.Platform.TestHost;

/// <summary>
/// Copies per-test file artifacts to the test results directory so that the
/// results directory is self-contained.
/// </summary>
internal sealed class FileArtifactCopyDataConsumer : IDataConsumer
Copy link
Member Author

Choose a reason for hiding this comment

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

Maybe this should be enabled/disabled via CLI or option?

{
private readonly IConfiguration _configuration;

public FileArtifactCopyDataConsumer(IConfiguration configuration)
=> _configuration = configuration;

public Type[] DataTypesConsumed => [typeof(TestNodeUpdateMessage)];

public string Uid => nameof(FileArtifactCopyDataConsumer);

public string Version => AppVersion.DefaultSemVer;

public string DisplayName => PlatformResources.FileArtifactCopyDataConsumerDisplayName;

public string Description => PlatformResources.FileArtifactCopyDataConsumerDescription;

public Task<bool> IsEnabledAsync() => Task.FromResult(true);

public Task ConsumeAsync(IDataProducer dataProducer, IData value, CancellationToken cancellationToken)
{
if (value is not TestNodeUpdateMessage message)
{
return Task.CompletedTask;
}

FileArtifactProperty[] artifacts = message.TestNode.Properties.OfType<FileArtifactProperty>();
if (artifacts.Length == 0)
{
return Task.CompletedTask;
}

string resultsDirectory = _configuration.GetTestResultDirectory();

foreach (FileArtifactProperty artifact in artifacts)
{
CopyFileToResultsDirectory(artifact.FileInfo, resultsDirectory);
}

return Task.CompletedTask;
}

private static void CopyFileToResultsDirectory(FileInfo file, string resultsDirectory)
{
// If the file is already under the results directory, skip.
string fullResultsDirectory = Path.GetFullPath(resultsDirectory);
if (file.FullName.StartsWith(fullResultsDirectory, StringComparison.OrdinalIgnoreCase))
Copy link

Copilot AI Feb 22, 2026

Choose a reason for hiding this comment

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

The path comparison using StartsWith with StringComparison.OrdinalIgnoreCase may not correctly handle directory separators on all platforms. For example, if resultsDirectory is "C:\results" and file.FullName is "C:\results_backup\file.txt", the StartsWith check will incorrectly consider the file to be under the results directory.

The codebase has a FileUtilities.PathComparison utility (src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/FileUtilities.cs) that properly handles case-sensitivity based on the file system. Additionally, similar code in AnsiTerminal.cs checks for directory separators after the path prefix to ensure it's a true subdirectory.

Consider using FileUtilities.PathComparison for the string comparison and adding a check for directory separators after the prefix, similar to the pattern in AnsiTerminal.cs lines 209-213.

Copilot uses AI. Check for mistakes.
{
return;
}

if (!Directory.Exists(resultsDirectory))
Copy link
Member Author

Choose a reason for hiding this comment

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

If we go with this solution, I'll use file system wrapper instead to ease testing

{
Directory.CreateDirectory(resultsDirectory);
}

string destination = Path.Combine(resultsDirectory, file.Name);
for (int nameCounter = 1; File.Exists(destination) && nameCounter <= 10; nameCounter++)
{
destination = Path.Combine(resultsDirectory, $"{Path.GetFileNameWithoutExtension(file.Name)}_{nameCounter}{Path.GetExtension(file.Name)}");
}

Copy link

Copilot AI Feb 22, 2026

Choose a reason for hiding this comment

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

After the bounded loop completes (when nameCounter exceeds 10), the code calls File.Copy even if a file still exists at the destination path. This will throw an IOException when overwrite is false and the destination file exists.

The code should check if a unique filename was found before attempting the copy, or handle the potential exception. Consider either throwing a more informative exception when all 10 attempts are exhausted, or checking File.Exists(destination) after the loop and handling that case explicitly.

Suggested change
if (File.Exists(destination))
{
throw new IOException($"Failed to copy file '{file.FullName}' to results directory '{resultsDirectory}' because a unique destination filename could not be determined after 10 attempts.");
}

Copilot uses AI. Check for mistakes.
File.Copy(file.FullName, destination, overwrite: false);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -473,11 +473,11 @@ public async Task TrxReportEngine_GenerateReportAsync_WithArtifactsByTestNode_Tr
string trxContentsPattern = @"
<UnitTestResult .* testName=""TestMethod"" .* outcome=""Passed"" .*>
<ResultFiles>
<ResultFile path=.*fileName"" />
<ResultFile path=""fileName"" />
</ResultFiles>
</UnitTestResult>
";
Assert.IsTrue(Regex.IsMatch(trxContent, trxContentsPattern));
Assert.IsTrue(Regex.IsMatch(trxContent, trxContentsPattern), trxContent);
}

[TestMethod]
Expand Down
Loading
Loading