-
Notifications
You must be signed in to change notification settings - Fork 355
[fix] Fix HTML logger parallel file collision #15435
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 15 commits
b703a2b
a935a8e
20b08d7
7bc6553
2bd9d56
a64c258
ecf5ddb
3cb268d
1070912
6e26e63
ce0bf3e
4780e0e
87a4ba3
81df804
10bb750
2a4f047
4e7ba20
e2f99d1
4ab2a31
04aaf44
0b55ee4
8659640
9215fdd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -33,7 +33,6 @@ public class HtmlLogger : ITestLoggerWithParameters | |||||
| private readonly IFileHelper _fileHelper; | ||||||
| private readonly XmlObjectSerializer _xmlSerializer; | ||||||
| private readonly IHtmlTransformer _htmlTransformer; | ||||||
| private static readonly object FileCreateLockObject = new(); | ||||||
| private Dictionary<string, string?>? _parametersDictionary; | ||||||
|
|
||||||
| public HtmlLogger() | ||||||
|
|
@@ -345,18 +344,21 @@ private void PopulateHtmlFile() | |||||
|
|
||||||
| private string GenerateUniqueFilePath(string fileName, string fileExtension) | ||||||
| { | ||||||
| string fullFilePath; | ||||||
| for (short i = 0; i < short.MaxValue; i++) | ||||||
| { | ||||||
| var fileNameWithIter = i == 0 ? fileName : Path.GetFileNameWithoutExtension(fileName) + $"[{i}]"; | ||||||
| fullFilePath = Path.Combine(TestResultsDirPath!, $"TestResult_{fileNameWithIter}.{fileExtension}"); | ||||||
| lock (FileCreateLockObject) | ||||||
| var fullFilePath = Path.Combine(TestResultsDirPath!, $"TestResult_{fileNameWithIter}.{fileExtension}"); | ||||||
|
|
||||||
| try | ||||||
| { | ||||||
| if (!File.Exists(fullFilePath)) | ||||||
| { | ||||||
| using var _ = File.Create(fullFilePath); | ||||||
| return fullFilePath; | ||||||
| } | ||||||
| // Use FileMode.CreateNew for atomic "create if not exists" to avoid | ||||||
| // cross-process race conditions when multiple vstest processes run in parallel. | ||||||
| using var _ = new FileStream(fullFilePath, FileMode.CreateNew, FileAccess.Write, FileShare.None); | ||||||
| return fullFilePath; | ||||||
| } | ||||||
| catch (IOException) when (File.Exists(fullFilePath)) | ||||||
|
||||||
| catch (IOException) when (File.Exists(fullFilePath)) | |
| catch (IOException) when (_fileHelper.Exists(fullFilePath)) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -591,6 +591,45 @@ public void TestCompleteHandlerShouldNotDivideByZeroWhenThereAre0TestResults() | |
| Assert.AreEqual(0, _htmlLogger.TestRunDetails.Summary.PassPercentage); | ||
| } | ||
|
|
||
| [TestMethod] | ||
| public void XmlFilePathShouldContainMillisecondTimestampForCrossProcessUniqueness() | ||
| { | ||
| // Verifies fix for https://github.com/microsoft/vstest/issues/15404 | ||
| // The temp XML file name should use millisecond-precision timestamps to avoid | ||
| // collisions when multiple vstest processes run in parallel. | ||
| string? createdFilePath = null; | ||
| try | ||
| { | ||
| _htmlLogger.TestRunCompleteHandler(new object(), new TestRunCompleteEventArgs(null, false, true, null, null, null, TimeSpan.Zero)); | ||
|
|
||
| Assert.IsNotNull(_htmlLogger.XmlFilePath); | ||
| createdFilePath = _htmlLogger.XmlFilePath; | ||
|
||
|
|
||
| // The filename should match the pattern with milliseconds: yyyyMMdd_HHmmssfff | ||
| // e.g., TestResult_user_MACHINE_20260304_153012345.xml | ||
| // The time part (HHmmssfff) should be 9 chars; previously it was 6 chars (HHmmss). | ||
| var fileName = Path.GetFileNameWithoutExtension(_htmlLogger.XmlFilePath); | ||
| var parts = fileName.Split('_'); | ||
| // The last part is the time portion, possibly with an iteration suffix like [7] | ||
| var timePart = parts[parts.Length - 1]; | ||
| var bracketIdx = timePart.IndexOf('['); | ||
| if (bracketIdx >= 0) | ||
| { | ||
| timePart = timePart.Substring(0, bracketIdx); | ||
| } | ||
|
|
||
| Assert.AreEqual(9, timePart.Length, | ||
| $"Time portion of timestamp should be 9 chars (HHmmssfff) but was '{timePart}' (length {timePart.Length}) in path: '{_htmlLogger.XmlFilePath}'"); | ||
| } | ||
| finally | ||
| { | ||
| if (!string.IsNullOrEmpty(createdFilePath) && File.Exists(createdFilePath)) | ||
| { | ||
| File.Delete(createdFilePath); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private static TestCase CreateTestCase(string testCaseName) | ||
| { | ||
| return new TestCase(testCaseName, new Uri("some://uri"), "DummySourceFileName"); | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.