[fix] Fix HTML logger parallel file collision#15435
Conversation
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>
There was a problem hiding this comment.
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.CreateNewfor 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. |
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>
1d29994 to
3cb268d
Compare
|
needs merge of #15434 |
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
| - **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 |
There was a problem hiding this comment.
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.
| - **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 |
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Youssef Victor <youssefvictor00@gmail.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
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
GenerateUniqueFilePathusesFile.Exists(fullFilePath)in the exception filter, bypassing the injectedIFileHelperabstraction. This makes the behavior harder to test/mocks inconsistent (and couples the logger to the physical filesystem even when a customIFileHelperis 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))
{
…lel-file-collision
nohwnd
left a comment
There was a problem hiding this comment.
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 🧠
|
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>
|
Commit pushed:
|
|
Fixed the review feedback: changed CI is green. The PR description's precision description is a minor inaccuracy (the format is actually
|
| collidedPath = path; | ||
| File.WriteAllText(path, string.Empty); | ||
| throw new IOException("collision"); | ||
| } |
nohwnd
left a comment
There was a problem hiding this comment.
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>
|
Commit pushed:
|
nohwnd
left a comment
There was a problem hiding this comment.
The two previously identified issues are resolved in this revision:
_fileHelper.Existsabstraction — the catch filter now correctly calls_fileHelper.Exists(fullFilePath)instead ofFile.Exists, keeping the IFileHelper abstraction intact and testable.- Test correctness —
_mockFileHelper.Setup(x => x.Exists(...)).Returns(true)is now set up before the IOException is thrown, so the catch filter evaluates totrue, the retry loop advances, and the[1].xmlassertion 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 🧠
| // 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)) | ||
| { |
Summary
Fixes #15404
When multiple vstest.console processes run in parallel with the HTML logger, temporary XML result files can collide because:
yyyyMMdd_HHmmss) only has second-level precisionlock (FileCreateLockObject)doesn't prevent cross-process racesChanges
src/Microsoft.TestPlatform.Extensions.HtmlLogger/HtmlLogger.csHHmmsstoHHmmssfff, greatly reducing collision likelihood across parallel processesFileMode.CreateNewfor atomic file creation - replaces theFile.Exists+File.Createpattern with an atomic create-if-not-exists operation that retries onIOExceptionFileCreateLockObject- the cross-process lock was ineffectivetest/Microsoft.TestPlatform.Extensions.HtmlLogger.UnitTests/HtmlLoggerTests.csXmlFilePathShouldContainMillisecondTimestampForCrossProcessUniquenesstestDocumentation
.github/copilot-instructions.mdwith build/test commands, repo structure, and logger architecture.github/skills/vstest-build-test/SKILL.mdwith logger-specific notesTesting
All 34 HTML logger unit tests pass on both net9.0 and net48.