Skip to content

[fix] Fix HTML logger parallel file collision#15435

Merged
nohwnd merged 23 commits into
mainfrom
fix/html-logger-parallel-file-collision
May 14, 2026
Merged

[fix] Fix HTML logger parallel file collision#15435
nohwnd merged 23 commits into
mainfrom
fix/html-logger-parallel-file-collision

Conversation

@nohwnd
Copy link
Copy Markdown
Member

@nohwnd nohwnd commented Mar 4, 2026

Summary

Fixes #15404

When multiple vstest.console processes run in parallel with the HTML logger, temporary XML result files can collide because:

  1. The timestamp format (yyyyMMdd_HHmmss) only has second-level precision
  2. The in-process lock (FileCreateLockObject) doesn't prevent cross-process races
  3. Two processes finishing in the same second generate identical filenames

Changes

src/Microsoft.TestPlatform.Extensions.HtmlLogger/HtmlLogger.cs

  • Add millisecond precision to timestamp - changes format from HHmmss to HHmmssfff, greatly reducing collision likelihood across parallel processes
  • Use FileMode.CreateNew for atomic file creation - replaces the File.Exists + File.Create pattern with an atomic create-if-not-exists operation that retries on IOException
  • Remove unused FileCreateLockObject - the cross-process lock was ineffective

test/Microsoft.TestPlatform.Extensions.HtmlLogger.UnitTests/HtmlLoggerTests.cs

  • Added XmlFilePathShouldContainMillisecondTimestampForCrossProcessUniqueness test

Documentation

  • Updated .github/copilot-instructions.md with build/test commands, repo structure, and logger architecture
  • Updated .github/skills/vstest-build-test/SKILL.md with logger-specific notes

Testing

All 34 HTML logger unit tests pass on both net9.0 and net48.

nohwnd and others added 7 commits March 4, 2026 11:41
Add pre-build environment setup section that detects when .dotnet
contains binaries for the wrong OS and cleans .dotnet, .packages,
and artifacts for a fresh bootstrap.

Update all build/test commands to show both Linux/macOS and Windows
equivalents with a quick-reference table.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…sification

- Add PowerShell OS detection alongside bash
- Add guidance for extracting OS-specific commands from tables
- Add placeholder substitution and cross-platform adaptation rules
- Use SQL tracking table instead of markdown table
- Distinguish ENV_ISSUE from ERROR in result classification
- Improve Fix or Flag section with actionable steps per status

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- eng/build.ps1: Forward exit code from Arcade build script using
  \�xit \0\ to ensure non-zero exit codes are propagated
  to the caller
- eng/common/tools.ps1: Add diagnostic log message before exiting so
  the exit code is visible in build output
- .github/skills/vstest-build-test/SKILL.md: Fix Windows test filter
  flag from \-p\ to \-projects\ since \-p\ is ambiguous in
  PowerShell and does not work as expected

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When multiple vstest processes run in parallel with the HTML logger, temporary
XML result files could collide because the timestamp-based filename only has
second-level precision, and the in-process lock doesn't prevent cross-process
races.

Changes:
- Include process ID in the temp XML filename for cross-process uniqueness
- Replace File.Exists + File.Create with atomic FileMode.CreateNew to handle
  any remaining race conditions gracefully via retry
- Remove unused FileCreateLockObject (cross-process lock was ineffective)

Fixes: #15404

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add build commands, test project mappings, repository structure, logger
architecture notes, and analyzer pitfalls to copilot-instructions.md.

Update vstest-build-test skill with logger-specific notes covering TRX and
HTML logger architecture, test patterns, and common analyzer issues.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings March 4, 2026 14:56
@nohwnd nohwnd marked this pull request as draft March 4, 2026 14:58
Copy link
Copy Markdown
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

Fixes cross-process temp XML filename collisions in the HTML logger when multiple vstest.console instances run in parallel.

Changes:

  • Adds PID to the temp XML filename and switches to FileMode.CreateNew for atomic “create if not exists”.
  • Adds a unit test to validate the PID is present in the generated XML path.
  • Updates build/doc tooling and authoring docs for repo build/test guidance.

Reviewed changes

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

Show a summary per file
File Description
test/Microsoft.TestPlatform.Extensions.HtmlLogger.UnitTests/HtmlLoggerTests.cs Adds a unit test asserting PID is included in the temp XML filename.
src/Microsoft.TestPlatform.Extensions.HtmlLogger/HtmlLogger.cs Adds PID to filename and uses FileMode.CreateNew for atomic file creation; removes ineffective in-proc lock.
eng/common/tools.ps1 Adds an extra console message when exiting with an exit code.
eng/build.ps1 Attempts to forward the exit code from the invoked Arcade build script.
.github/skills/vstest-build-test/SKILL.md Adds repo-specific build/test instructions and logger notes.
.github/skills/validate-skills/SKILL.md Adds a procedure for validating that documented skill commands work.
.github/skills/creating-skills/SKILL.md Adds guidance for creating skill files and frontmatter.
.github/copilot-instructions.md Adds build/test command guidance and repository structure notes.

Comment thread src/Microsoft.TestPlatform.Extensions.HtmlLogger/HtmlLogger.cs Outdated
Comment thread src/Microsoft.TestPlatform.Extensions.HtmlLogger/HtmlLogger.cs Outdated
Comment thread eng/common/tools.ps1 Outdated
When multiple vstest processes run in parallel with the HTML logger, temporary
XML result files could collide because the timestamp-based filename only has
second-level precision, and the in-process lock doesn't prevent cross-process
races.

Changes:
- Add millisecond precision to temp XML filename timestamp (HHmmss → HHmmssfff)
  to greatly reduce collision likelihood across parallel processes
- Replace File.Exists + File.Create with atomic FileMode.CreateNew to handle
  any remaining race conditions gracefully via retry
- Remove unused FileCreateLockObject (cross-process lock was ineffective)

Fixes: #15404

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@nohwnd nohwnd force-pushed the fix/html-logger-parallel-file-collision branch from 1d29994 to 3cb268d Compare March 4, 2026 15:04
@nohwnd
Copy link
Copy Markdown
Member Author

nohwnd commented Mar 4, 2026

needs merge of #15434

Copilot AI review requested due to automatic review settings March 5, 2026 14:39
Copy link
Copy Markdown
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

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

Comment thread .github/copilot-instructions.md Outdated
Comment thread .github/skills/vstest-build-test/SKILL.md Outdated
Comment thread .github/skills/vstest-build-test/SKILL.md Outdated
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings March 5, 2026 15:22
Copy link
Copy Markdown
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

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Comment on lines +160 to +161
- **Temp XML naming:** `TestResult_{user}_{machine}_{timestamp}_{pid}.xml` — PID ensures cross-process uniqueness
- **File creation:** Uses `FileMode.CreateNew` for atomic creation with retry on collision
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

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

Same issue as in copilot-instructions.md: the documented filename pattern includes {pid} but the actual code does not add a PID to the filename. The uniqueness strategy relies on millisecond timestamps plus FileMode.CreateNew with retry. This documentation should be corrected to match the actual implementation.

Suggested change
- **Temp XML naming:** `TestResult_{user}_{machine}_{timestamp}_{pid}.xml`PID ensures cross-process uniqueness
- **File creation:** Uses `FileMode.CreateNew` for atomic creation with retry on collision
- **Temp XML naming:** `TestResult_{user}_{machine}_{timestamp}.xml`timestamp-based name only (no PID)
- **File creation:** Uses `FileMode.CreateNew` for atomic creation with retry on collision; millisecond timestamps + `CreateNew` retries provide uniqueness

Copilot uses AI. Check for mistakes.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings March 5, 2026 15:42
nohwnd and others added 3 commits March 5, 2026 16:43
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Copy link
Copy Markdown
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

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

Comment thread test/Microsoft.TestPlatform.Extensions.HtmlLogger.UnitTests/HtmlLoggerTests.cs Outdated
Comment thread src/Microsoft.TestPlatform.Extensions.HtmlLogger/HtmlLogger.cs Outdated
Comment thread src/Microsoft.TestPlatform.Extensions.HtmlLogger/HtmlLogger.cs Outdated
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Youssef Victor <youssefvictor00@gmail.com>
Copilot AI review requested due to automatic review settings March 24, 2026 06:54
Copy link
Copy Markdown
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

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

Comment thread src/Microsoft.TestPlatform.Extensions.HtmlLogger/HtmlLogger.cs
Comment thread src/Microsoft.TestPlatform.Extensions.HtmlLogger/HtmlLogger.cs Outdated
nohwnd and others added 2 commits April 10, 2026 15:21
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings April 10, 2026 13:21
Copy link
Copy Markdown
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

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

Comments suppressed due to low confidence (1)

src/Microsoft.TestPlatform.Extensions.HtmlLogger/HtmlLogger.cs:366

  • GenerateUniqueFilePath uses File.Exists(fullFilePath) in the exception filter, bypassing the injected IFileHelper abstraction. This makes the behavior harder to test/mocks inconsistent (and couples the logger to the physical filesystem even when a custom IFileHelper is provided). Prefer using _fileHelper.Exists(fullFilePath) in the filter, and update the unit test setup accordingly.
                // Use FileMode.CreateNew for atomic "create if not exists" to avoid
                // cross-process race conditions when multiple vstest processes run in parallel.
                using var _ = _fileHelper.GetStream(fullFilePath, FileMode.CreateNew, FileAccess.Write, FileShare.None);
                return fullFilePath;
            }
            catch (IOException) when (File.Exists(fullFilePath))
            {

Copy link
Copy Markdown
Member Author

@nohwnd nohwnd left a comment

Choose a reason for hiding this comment

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

The FileMode.CreateNew + retry approach is the right fix for the cross-process collision. The timestamp precision improvement and removal of the in-process lock are both correct.

One issue to address: the exception filter calls File.Exists(fullFilePath) directly rather than _fileHelper.Exists(fullFilePath), bypassing the injected abstraction. See the inline comment for details.

Minor description inaccuracy: the PR description says the format changes "from HHmmss to HHmmssfff" but the actual code uses HHmmss.fffffff (7 fractional digits with a dot separator, giving 100-nanosecond precision). The description undersells the precision added.

🧠 Reviewed by claude-sonnet-4.6

🧠 Reviewed by Expert Code Reviewer 🧠

@nohwnd nohwnd changed the title Fix HTML logger parallel file collision [fix] Fix HTML logger parallel file collision May 13, 2026
@nohwnd
Copy link
Copy Markdown
Member Author

nohwnd commented May 13, 2026

Address the review feedback and drive this to green CI.

…ilePath

Address review feedback: the exception filter was calling File.Exists()
directly, bypassing the injected IFileHelper abstraction. Replace with
_fileHelper.Exists() for consistency and testability.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@nohwnd
Copy link
Copy Markdown
Member Author

nohwnd commented May 13, 2026

Commit pushed: 0b55ee4

🔧 Iterated by PR Iteration Agent 🔧

Copilot AI review requested due to automatic review settings May 13, 2026 15:12
@nohwnd
Copy link
Copy Markdown
Member Author

nohwnd commented May 13, 2026

Fixed the review feedback: changed File.Exists(fullFilePath) to _fileHelper.Exists(fullFilePath) in the GenerateUniqueFilePath exception filter so it uses the injected abstraction consistently.

CI is green. The PR description's precision description is a minor inaccuracy (the format is actually HHmmss.fffffff) but doesn't affect correctness — left as-is since the code is what matters.

🔧 Iterated by PR Iteration Agent 🔧

Copy link
Copy Markdown
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

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

Comment on lines +619 to +622
collidedPath = path;
File.WriteAllText(path, string.Empty);
throw new IOException("collision");
}
Copy link
Copy Markdown
Member Author

@nohwnd nohwnd left a comment

Choose a reason for hiding this comment

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

The FileMode.CreateNew + retry approach and the _fileHelper.Exists fix are both correct. One issue in the new test needs to be addressed before merging — see the inline comment.

🧠 Reviewed by claude-sonnet-4.6

🧠 Reviewed by Expert Code Reviewer 🧠

🧠 Reviewed by Expert Code Reviewer 🧠

Add _mockFileHelper.Exists setup returning true so the IOException
catch filter when (_fileHelper.Exists(fullFilePath)) evaluates to
true and the exception is swallowed, allowing the retry loop to
proceed. Remove real-filesystem File.WriteAllText side effect and
simplify cleanup.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@nohwnd
Copy link
Copy Markdown
Member Author

nohwnd commented May 13, 2026

Commit pushed: 8659640

🔧 Iterated by PR Iteration Agent 🔧

Copy link
Copy Markdown
Member Author

@nohwnd nohwnd left a comment

Choose a reason for hiding this comment

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

The two previously identified issues are resolved in this revision:

  1. _fileHelper.Exists abstraction — the catch filter now correctly calls _fileHelper.Exists(fullFilePath) instead of File.Exists, keeping the IFileHelper abstraction intact and testable.
  2. Test correctness_mockFileHelper.Setup(x => x.Exists(...)).Returns(true) is now set up before the IOException is thrown, so the catch filter evaluates to true, the retry loop advances, and the [1].xml assertion is actually exercised.

The FileMode.CreateNew atomic create-if-not-exists approach is the right fix for cross-process collision. LGTM.

🧠 Reviewed by claude-sonnet-4.6

🧠 Reviewed by Expert Code Reviewer 🧠

🧠 Reviewed by Expert Code Reviewer 🧠

Copilot AI review requested due to automatic review settings May 14, 2026 07:23
@nohwnd nohwnd enabled auto-merge (squash) May 14, 2026 07:25
@nohwnd nohwnd disabled auto-merge May 14, 2026 07:26
@nohwnd nohwnd merged commit 6c550f8 into main May 14, 2026
7 of 10 checks passed
@nohwnd nohwnd deleted the fix/html-logger-parallel-file-collision branch May 14, 2026 07:26
Copy link
Copy Markdown
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

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

Comment on lines +360 to 366
// Use FileMode.CreateNew for atomic "create if not exists" to avoid
// cross-process race conditions when multiple vstest processes run in parallel.
using var _ = _fileHelper.GetStream(fullFilePath, FileMode.CreateNew, FileAccess.Write, FileShare.None);
return fullFilePath;
}
catch (IOException) when (File.Exists(fullFilePath))
catch (IOException) when (_fileHelper.Exists(fullFilePath))
{
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.

HTML logger sometimes shows error when using multiple vstest runner in parallel

3 participants