diff --git a/src/AI/tests/Essentials.AI.UnitTests/TestHelpers/DataStreamsHelper.cs b/src/AI/tests/Essentials.AI.UnitTests/TestHelpers/DataStreamsHelper.cs index 36da3a6332cc..148af442b1c2 100644 --- a/src/AI/tests/Essentials.AI.UnitTests/TestHelpers/DataStreamsHelper.cs +++ b/src/AI/tests/Essentials.AI.UnitTests/TestHelpers/DataStreamsHelper.cs @@ -19,7 +19,27 @@ public static string GetTxtItinerary(string fileName) public static string[] GetFileLines(string fileName) { var path = GetFile(fileName); - return File.ReadAllLines(path); + return ReadAllLinesShared(path); + } + + // Reads all lines from a file while allowing other readers/writers to keep the + // file open concurrently. Test classes run in parallel and multiple theories read + // the same shared TestData files, which can otherwise race and throw an IOException + // ("The process cannot access the file because it is being used by another process") + // on Windows. Opening with FileShare.ReadWrite makes the shared reads deterministic. + public static string[] ReadAllLinesShared(string path) + { + using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + using var reader = new StreamReader(stream); + + var lines = new List(); + string? line; + while ((line = reader.ReadLine()) is not null) + { + lines.Add(line); + } + + return lines.ToArray(); } public static IEnumerable JsonlItineraries diff --git a/src/AI/tests/Essentials.AI.UnitTests/Tests/JsonStreamChunkerTests/Integration.cs b/src/AI/tests/Essentials.AI.UnitTests/Tests/JsonStreamChunkerTests/Integration.cs index f366913eefd8..0e4d02e1e505 100644 --- a/src/AI/tests/Essentials.AI.UnitTests/Tests/JsonStreamChunkerTests/Integration.cs +++ b/src/AI/tests/Essentials.AI.UnitTests/Tests/JsonStreamChunkerTests/Integration.cs @@ -20,7 +20,7 @@ public void Process_FromJsonlFile_ProducesValidJsonMatchingFinalLine(string file // Arrange var chunker = new JsonStreamChunker(); - var lines = File.ReadAllLines(filePath); + var lines = DataStreamsHelper.ReadAllLinesShared(filePath); // Act var chunks = new List(); diff --git a/src/AI/tests/Essentials.AI.UnitTests/Tests/JsonStreamingRoundtripTests/FileBasedTests.cs b/src/AI/tests/Essentials.AI.UnitTests/Tests/JsonStreamingRoundtripTests/FileBasedTests.cs index c5b43a9a60ae..226a68e7beec 100644 --- a/src/AI/tests/Essentials.AI.UnitTests/Tests/JsonStreamingRoundtripTests/FileBasedTests.cs +++ b/src/AI/tests/Essentials.AI.UnitTests/Tests/JsonStreamingRoundtripTests/FileBasedTests.cs @@ -19,7 +19,7 @@ public void Roundtrip_FromJsonlFile_DeserializerProducesEquivalentResult(string _ = fileName; // We are not using the parameter here // Arrange - var lines = File.ReadAllLines(filePath); + var lines = DataStreamsHelper.ReadAllLinesShared(filePath); var finalLine = lines[^1]; // Act - pass each line through chunker and deserializer @@ -48,7 +48,7 @@ public void Roundtrip_FromJsonlFile_EachLineDeserializesProgressively(string fil _ = fileName; // We are not using the parameter here // This test validates that each intermediate line produces a valid partial object - var lines = File.ReadAllLines(filePath); + var lines = DataStreamsHelper.ReadAllLinesShared(filePath); var models = ProcessLines(lines); diff --git a/src/AI/tests/Essentials.AI.UnitTests/Tests/StreamingJsonDeserializerTests/FileBasedTests.cs b/src/AI/tests/Essentials.AI.UnitTests/Tests/StreamingJsonDeserializerTests/FileBasedTests.cs index b60de7522523..7d3a69a2ca21 100644 --- a/src/AI/tests/Essentials.AI.UnitTests/Tests/StreamingJsonDeserializerTests/FileBasedTests.cs +++ b/src/AI/tests/Essentials.AI.UnitTests/Tests/StreamingJsonDeserializerTests/FileBasedTests.cs @@ -18,7 +18,7 @@ public void ProcessChunk_TxtFile_DeserializesProgressively(string fileName, stri { _ = fileName; // Used for test display name var deserializer = new StreamingJsonDeserializer(DeserializationOptions); - var chunks = File.ReadAllLines(filePath); + var chunks = DataStreamsHelper.ReadAllLinesShared(filePath); Itinerary? lastItinerary = null; @@ -41,7 +41,7 @@ public void ProcessChunk_TxtFile_FinalResultMatchesDirectDeserialization(string { _ = fileName; // Used for test display name var deserializer = new StreamingJsonDeserializer(DeserializationOptions); - var chunks = File.ReadAllLines(filePath); + var chunks = DataStreamsHelper.ReadAllLinesShared(filePath); Itinerary? finalItinerary = null; @@ -65,7 +65,7 @@ public void ProcessChunk_TxtFile_FinalResultMatchesDirectDeserialization(string public void ProcessChunk_TxtFile_ShouldProgressDaysArray(string fileName, string filePath) { var deserializer = new StreamingJsonDeserializer(DeserializationOptions); - var chunks = File.ReadAllLines(filePath); + var chunks = DataStreamsHelper.ReadAllLinesShared(filePath); var daysProgression = new List<(int ChunkNumber, int? DaysCount, int? ActivitiesInFirstDay)>(); @@ -98,7 +98,7 @@ public void ProcessChunk_TxtFile_ShouldProgressDaysArray(string fileName, string public void ProcessChunk_TxtFile_ShouldProgressTitleField(string fileName, string filePath) { var deserializer = new StreamingJsonDeserializer(DeserializationOptions); - var chunks = File.ReadAllLines(filePath); + var chunks = DataStreamsHelper.ReadAllLinesShared(filePath); var titleProgression = new List(); @@ -129,7 +129,7 @@ public void ProcessChunk_TxtFile_EachChunkProducesNonEmptyObject(string fileName { _ = fileName; // Used for test display name var deserializer = new StreamingJsonDeserializer(DeserializationOptions); - var chunks = File.ReadAllLines(filePath); + var chunks = DataStreamsHelper.ReadAllLinesShared(filePath); foreach (var chunk in chunks) { @@ -152,7 +152,7 @@ public void ProcessChunk_TxtFile_UpdateRate(string fileName, string filePath) // partial property values, or unchanged content - 70% is a reasonable threshold. var deserializer = new StreamingJsonDeserializer(DeserializationOptions); - var chunks = File.ReadAllLines(filePath); + var chunks = DataStreamsHelper.ReadAllLinesShared(filePath); string? previousJson = null; int totalChunks = 0;