Skip to content

Copy per-test result files to results directory in TRX reports#7449

Draft
Evangelink wants to merge 1 commit intomainfrom
dev/amauryleve/trx-attach-copy
Draft

Copy per-test result files to results directory in TRX reports#7449
Evangelink wants to merge 1 commit intomainfrom
dev/amauryleve/trx-attach-copy

Conversation

@Evangelink
Copy link
Member

When using MTP, files added via TestContext.AddResultFile were not copied to the results directory. The TRX file referenced them with absolute paths, making the results directory non-self-contained and causing warnings in Azure DevOps builds.

Changes:

  • Copy per-test FileArtifactProperty files into the results directory under {runDeploymentRoot}/In/{executionId}/{MachineName}/
  • Use relative paths in TRX elements instead of absolute paths
  • Replace unbounded while(true) loops with bounded for loops (max 10 attempts) when resolving duplicate file names, in both the new and existing artifact copy methods

Fixes #6782

When using MTP, files added via TestContext.AddResultFile were not copied to the results directory. The TRX file referenced them with absolute paths, making the results directory non-self-contained and causing warnings in Azure DevOps builds.

Changes:
- Copy per-test FileArtifactProperty files into the results directory under {runDeploymentRoot}/In/{executionId}/{MachineName}/
- Use relative paths in TRX <ResultFile> elements instead of absolute paths
- Replace unbounded while(true) loops with bounded for loops (max 10 attempts) when resolving duplicate file names, in both the new and existing artifact copy methods

Fixes #6782
Copilot AI review requested due to automatic review settings February 22, 2026 12:37
await AddArtifactsToCollectionAsync(_artifactsByExtension, collectorDataEntries, runDeploymentRoot).ConfigureAwait(false);
}

private async Task<string> CopyTestResultFileAndReturnRelativePathAsync(FileInfo artifact, string runDeploymentRoot, string executionId)
Copy link
Member Author

Choose a reason for hiding this comment

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

We could also consider the copy function to be part of the core platform (or vstest bridge) so that the feature is also available outside of trx being enabled

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Updates TRX generation in Microsoft.Testing.Extensions.TrxReport so per-test result files (from TestContext.AddResultFile / FileArtifactProperty) are copied into the test results directory and referenced via relative paths, making results self-contained.

Changes:

  • Copy per-test FileArtifactProperty files into {resultsDir}/{runDeploymentRoot}/In/{executionId}/{MachineName}/ and emit relative <ResultFile path=...> values.
  • Refactor result generation to an async tuple-returning AddResultsAsync(...) to support awaiting file-copy operations.
  • Replace unbounded filename-collision loops with bounded retry loops (max 10) when choosing destination filenames.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
test/UnitTests/Microsoft.Testing.Extensions.UnitTests/TrxTests.cs Updates TRX assertion to expect relative ResultFile paths (and includes TRX text on failure).
src/Platform/Microsoft.Testing.Extensions.TrxReport/TrxReportEngine.cs Implements per-test result file copying + relative paths; refactors result-building to async; bounds duplicate-name retry loops.
Comments suppressed due to low confidence (1)

src/Platform/Microsoft.Testing.Extensions.TrxReport/TrxReportEngine.cs:383

  • Same issue as above: with the 10-try loop, destination may still exist and CopyFileAsync will overwrite it (FileMode.Create). Ensure you either keep searching for a free name or fail deterministically once the retry budget is exhausted to prevent data loss.
        // If the file already exists, append a number to the end of the file name
        for (int nameCounter = 1; File.Exists(destination) && nameCounter <= 10; nameCounter++)
        {
            destination = Path.Combine(artifactDirectory, $"{Path.GetFileNameWithoutExtension(fileName)}_{nameCounter}{Path.GetExtension(fileName)}");
        }

Comment on lines +351 to +355
string testResultDirectory = Path.Combine(_configuration.GetTestResultDirectory(), runDeploymentRoot, "In", executionId, _environment.MachineName);
if (!Directory.Exists(testResultDirectory))
{
Directory.CreateDirectory(testResultDirectory);
}
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.

CopyTestResultFileAndReturnRelativePathAsync creates directories and probes the real filesystem even when _isCopyingFileAllowed is false. This makes “copy disabled” mode still have side effects (and can break unit tests / restricted environments). Consider short-circuiting early when copying is disabled (return the relative path without touching disk).

Copilot uses AI. Check for mistakes.
Comment on lines +552 to +556
string relativePath = await CopyTestResultFileAndReturnRelativePathAsync(
testFileArtifact.FileInfo, runDeploymentRoot, executionId).ConfigureAwait(false);
resultFiles.Add(new XElement(
"ResultFile",
new XAttribute("path", testFileArtifact.FileInfo.FullName)));
new XAttribute("path", relativePath)));
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.

New behavior copies per-test result files into the results directory and changes <ResultFile path=...> to be relative. There’s no unit test that exercises the real copy path (all tests construct TrxReportEngine with isCopyingFileAllowed: false), so regressions in directory layout / naming / copy failures won’t be caught. Add a test that enables copying and asserts the file is created under the expected {runDeploymentRoot}/In/{executionId}/{MachineName}/ layout and that the TRX references it correctly.

Copilot uses AI. Check for mistakes.
Comment on lines +361 to +365
// If the file already exists, append a number to the end of the file name
for (int nameCounter = 1; File.Exists(destination) && nameCounter <= 10; nameCounter++)
{
destination = Path.Combine(testResultDirectory, $"{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.

The bounded name-collision loop can still leave destination pointing to an existing file after 10 attempts. Because CopyFileAsync uses FileMode.Create, this will overwrite an existing artifact and silently lose data. After the loop, handle the “still exists” case explicitly (e.g., generate a GUID-based name or throw) and/or use an atomic CreateNew strategy to avoid overwrites.

This issue also appears on line 379 of the same file.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[MTP] Result files not added to results directory

2 participants