From cb3d30952fe367711eb3bf015421ccb11b54336d Mon Sep 17 00:00:00 2001 From: Mistial Developer Date: Sun, 13 Jul 2025 03:42:44 -0400 Subject: [PATCH 1/6] Fixes TestableIO/System.IO.Abstractions#1131 --- .../MockFileData.cs | 29 +- .../MockFileStream.cs | 260 +++++++++++- .../MockFileStreamTests.cs | 394 ++++++++++++++++++ 3 files changed, 671 insertions(+), 12 deletions(-) diff --git a/src/TestableIO.System.IO.Abstractions.TestingHelpers/MockFileData.cs b/src/TestableIO.System.IO.Abstractions.TestingHelpers/MockFileData.cs index ea15019a1..30fb2ea29 100644 --- a/src/TestableIO.System.IO.Abstractions.TestingHelpers/MockFileData.cs +++ b/src/TestableIO.System.IO.Abstractions.TestingHelpers/MockFileData.cs @@ -57,6 +57,8 @@ private MockFileData() LastWriteTime = now; LastAccessTime = now; CreationTime = now; + contents = new byte[0]; + contentVersion = 0; } /// @@ -76,7 +78,8 @@ public MockFileData(string textContents) public MockFileData(string textContents, Encoding encoding) : this() { - Contents = encoding.GetPreamble().Concat(encoding.GetBytes(textContents)).ToArray(); + contents = encoding.GetPreamble().Concat(encoding.GetBytes(textContents)).ToArray(); + contentVersion = 1; } /// @@ -87,7 +90,8 @@ public MockFileData(string textContents, Encoding encoding) public MockFileData(byte[] contents) : this() { - Contents = contents ?? throw new ArgumentNullException(nameof(contents)); + this.contents = contents ?? throw new ArgumentNullException(nameof(contents)); + contentVersion = 1; } @@ -105,7 +109,8 @@ public MockFileData(MockFileData template) accessControl = template.accessControl; Attributes = template.Attributes; - Contents = template.Contents.ToArray(); + contents = template.contents?.ToArray(); + contentVersion = template.contentVersion; CreationTime = template.CreationTime; LastAccessTime = template.LastAccessTime; LastWriteTime = template.LastWriteTime; @@ -114,10 +119,26 @@ public MockFileData(MockFileData template) #endif } + private byte[] contents; + private long contentVersion = 0; + /// /// Gets or sets the byte contents of the . /// - public byte[] Contents { get; set; } + public byte[] Contents + { + get => contents; + set + { + contents = value; + contentVersion++; + } + } + + /// + /// Gets the current version of the file contents. This is incremented every time Contents is modified. + /// + internal long ContentVersion => contentVersion; /// /// Gets or sets the file version info of the diff --git a/src/TestableIO.System.IO.Abstractions.TestingHelpers/MockFileStream.cs b/src/TestableIO.System.IO.Abstractions.TestingHelpers/MockFileStream.cs index 1be2241af..32510b6dc 100644 --- a/src/TestableIO.System.IO.Abstractions.TestingHelpers/MockFileStream.cs +++ b/src/TestableIO.System.IO.Abstractions.TestingHelpers/MockFileStream.cs @@ -35,6 +35,15 @@ public NullFileSystemStream() : base(Null, ".", true) private readonly FileOptions options; private readonly MockFileData fileData; private bool disposed; + + // Version-based content synchronization to support shared file access + // Tracks the version of shared content we last synchronized with + private long lastKnownContentVersion; + + // Tracks whether this stream has local modifications that haven't been flushed to shared storage + // This prevents us from overwriting shared content unnecessarily and helps preserve unflushed changes during refresh + private bool hasUnflushedWrites; + /// public MockFileStream( @@ -77,6 +86,7 @@ public MockFileStream( ? SeekOrigin.End : SeekOrigin.Begin); } + lastKnownContentVersion = fileData.ContentVersion; } else { @@ -95,6 +105,7 @@ public MockFileStream( mockFileDataAccessor.AdjustTimes(fileData, TimeAdjustments.CreationTime | TimeAdjustments.LastAccessTime); mockFileDataAccessor.AddFile(path, fileData); + lastKnownContentVersion = fileData.ContentVersion; } this.access = access; @@ -129,13 +140,38 @@ private static void ThrowIfInvalidModeAccess(FileMode mode, FileAccess access) /// public override bool CanWrite => access.HasFlag(FileAccess.Write); + /// + public override long Length + { + get + { + // Only refresh if needed to see latest file size from other streams + RefreshFromSharedContentIfNeeded(); + return base.Length; + } + } + /// public override int Read(byte[] buffer, int offset, int count) { mockFileDataAccessor.AdjustTimes(fileData, TimeAdjustments.LastAccessTime); - return base.Read(buffer, offset, count); + RefreshFromSharedContentIfNeeded(); + var result = base.Read(buffer, offset, count); + return result; + } + +#if FEATURE_SPAN + /// + public override int Read(Span buffer) + { + mockFileDataAccessor.AdjustTimes(fileData, + TimeAdjustments.LastAccessTime); + RefreshFromSharedContentIfNeeded(); + var result = base.Read(buffer); + return result; } +#endif /// protected override void Dispose(bool disposing) @@ -168,6 +204,8 @@ public override void SetLength(long value) throw new NotSupportedException("Stream does not support writing."); } + // Mark that we have changes that need to be flushed to shared storage + hasUnflushedWrites = true; base.SetLength(value); } @@ -178,8 +216,11 @@ public override void Write(byte[] buffer, int offset, int count) { throw new NotSupportedException("Stream does not support writing."); } + RefreshFromSharedContentIfNeeded(); mockFileDataAccessor.AdjustTimes(fileData, TimeAdjustments.LastAccessTime | TimeAdjustments.LastWriteTime); + // Mark that we now have local changes that need to be flushed + hasUnflushedWrites = true; base.Write(buffer, offset, count); } @@ -191,8 +232,10 @@ public override void Write(ReadOnlySpan buffer) { throw new NotSupportedException("Stream does not support writing."); } + RefreshFromSharedContentIfNeeded(); mockFileDataAccessor.AdjustTimes(fileData, TimeAdjustments.LastAccessTime | TimeAdjustments.LastWriteTime); + hasUnflushedWrites = true; base.Write(buffer); } #endif @@ -205,8 +248,10 @@ public override Task WriteAsync(byte[] buffer, int offset, int count, { throw new NotSupportedException("Stream does not support writing."); } + RefreshFromSharedContentIfNeeded(); mockFileDataAccessor.AdjustTimes(fileData, TimeAdjustments.LastAccessTime | TimeAdjustments.LastWriteTime); + hasUnflushedWrites = true; return base.WriteAsync(buffer, offset, count, cancellationToken); } @@ -219,8 +264,10 @@ public override ValueTask WriteAsync(ReadOnlyMemory buffer, { throw new NotSupportedException("Stream does not support writing."); } + RefreshFromSharedContentIfNeeded(); mockFileDataAccessor.AdjustTimes(fileData, TimeAdjustments.LastAccessTime | TimeAdjustments.LastWriteTime); + hasUnflushedWrites = true; return base.WriteAsync(buffer, cancellationToken); } #endif @@ -232,8 +279,10 @@ public override void WriteByte(byte value) { throw new NotSupportedException("Stream does not support writing."); } + RefreshFromSharedContentIfNeeded(); mockFileDataAccessor.AdjustTimes(fileData, TimeAdjustments.LastAccessTime | TimeAdjustments.LastWriteTime); + hasUnflushedWrites = true; base.WriteByte(value); } @@ -281,21 +330,216 @@ private MockFileData GetMockFileData() ?? throw CommonExceptions.FileNotFound(path); } - private void InternalFlush() + /// + /// Fast path optimization: only refresh if the content version has actually changed. + /// This avoids expensive content synchronization when we're already up to date. + /// + private void RefreshFromSharedContentIfNeeded() { if (mockFileDataAccessor.FileExists(path)) { var mockFileData = mockFileDataAccessor.GetFile(path); - /* reset back to the beginning .. */ + + // Quick version check - if versions match, we're already synchronized + if (mockFileData.ContentVersion == lastKnownContentVersion) + { + return; // Fast exit - no work needed + } + + // Version changed, indicating another stream modified the file + RefreshFromSharedContent(mockFileData); + } + } + + /// + /// Synchronizes this stream's content with the shared file data to support FileShare.ReadWrite scenarios. + /// + /// When multiple MockFileStream instances open the same file with FileShare.ReadWrite, they need to see + /// each other's changes. This method implements a version-based synchronization mechanism where: + /// + /// 1. Each MockFileData has a ContentVersion that increments when content changes + /// 2. Each stream tracks the lastKnownContentVersion it has synchronized with + /// 3. Before reads/writes/length checks, streams refresh to get the latest shared content + /// 4. During refresh, any unflushed local changes are preserved and merged with shared content + /// + /// This solves GitHub issue #1131 where shared file content was not visible between streams. + /// + private void RefreshFromSharedContent(MockFileData mockFileData = null) + { + if (mockFileDataAccessor.FileExists(path)) + { + mockFileData ??= mockFileDataAccessor.GetFile(path); + + // Only refresh if the shared content has been modified since we last synced + // This prevents unnecessary work and maintains performance + if (mockFileData.ContentVersion != lastKnownContentVersion) + { + // If we have unflushed writes, we need to preserve them when refreshing + // This handles the case where: + // 1. Stream A writes data but hasn't flushed + // 2. Stream B writes and flushes, updating shared content + // 3. Stream A needs to refresh but preserve its unflushed changes + byte[] preservedContent = null; + long preservedLength = 0; + long currentPosition = Position; + + if (hasUnflushedWrites) + { + // Save our current stream content to preserve unflushed writes + preservedLength = base.Length; + preservedContent = new byte[preservedLength]; + var originalPosition = Position; + Position = 0; + var totalBytesRead = 0; + while (totalBytesRead < preservedLength) + { + var bytesRead = base.Read(preservedContent, totalBytesRead, (int)(preservedLength - totalBytesRead)); + if (bytesRead == 0) break; + totalBytesRead += bytesRead; + } + Position = originalPosition; + } + + var sharedContent = mockFileData.Contents; + + // Performance optimization: if we have no unflushed writes and the shared content + // is the same length as our current content, we might not need to do expensive copying + if (!hasUnflushedWrites && sharedContent?.Length == base.Length) + { + // Quick content comparison for common case where only metadata changed + bool contentSame = true; + if (sharedContent.Length > 0 && sharedContent.Length <= 4096) // Only check small files + { + var currentPos = Position; + Position = 0; + var currentContent = new byte[base.Length]; + var bytesRead = base.Read(currentContent, 0, (int)base.Length); + Position = currentPos; + + if (bytesRead == sharedContent.Length) + { + for (int i = 0; i < bytesRead; i++) + { + if (currentContent[i] != sharedContent[i]) + { + contentSame = false; + break; + } + } + } + else + { + contentSame = false; + } + } + else + { + contentSame = false; // Don't compare large files + } + + if (contentSame) + { + // Content is identical, just update version tracking and exit + lastKnownContentVersion = mockFileData.ContentVersion; + return; + } + } + + // Start with shared content as the base - this gives us the latest changes from other streams + base.SetLength(0); + Position = 0; + if (sharedContent != null && sharedContent.Length > 0) + { + base.Write(sharedContent, 0, sharedContent.Length); + } + + // If we had unflushed writes, we need to overlay them on the shared content + // This ensures our local changes take precedence over shared content + if (hasUnflushedWrites && preservedContent != null) + { + // Optimization: if preserved content is same length or longer, just use it directly + if (preservedLength >= (sharedContent?.Length ?? 0)) + { + base.SetLength(0); + Position = 0; + base.Write(preservedContent, 0, (int)preservedLength); + } + else + { + // Need to merge: ensure stream is large enough + if (base.Length < preservedLength) + { + base.SetLength(preservedLength); + } + + // Apply our preserved content on top of the shared content + Position = 0; + base.Write(preservedContent, 0, (int)preservedLength); + } + } + + // Restore position, but ensure it's within bounds of the new content + Position = Math.Min(currentPosition, base.Length); + + // Update our version to match what we just synchronized with + // This prevents unnecessary refreshes until the shared content changes again + lastKnownContentVersion = mockFileData.ContentVersion; + } + } + } + + /// + /// Flushes this stream's content to the shared file data, implementing proper synchronization for FileShare.ReadWrite. + /// + /// This method is called by Flush(), FlushAsync(), and Dispose() to ensure local changes are persisted + /// to the shared MockFileData that other streams can see. Key aspects: + /// + /// 1. Only flushes if we have unflushed writes (performance optimization) + /// 2. Refreshes from shared content first to merge any changes from other streams + /// 3. Reads the entire stream content and updates the shared MockFileData.Contents + /// 4. Updates version tracking to stay synchronized + /// 5. Clears the hasUnflushedWrites flag + /// + /// This ensures that when multiple streams share a file, all changes are properly coordinated. + /// + private void InternalFlush() + { + if (mockFileDataAccessor.FileExists(path) && hasUnflushedWrites) + { + var mockFileData = mockFileDataAccessor.GetFile(path); + + // Before flushing, ensure we have the latest shared content merged with our unflushed writes + // This is critical to prevent overwriting changes made by other streams + RefreshFromSharedContentIfNeeded(); + + // Save current position to restore later var position = Position; + + // Read the entire stream content to flush to shared storage Seek(0, SeekOrigin.Begin); - /* .. read everything out */ - var data = new byte[Length]; - _ = Read(data, 0, (int)Length); - /* restore to original position */ + var data = new byte[base.Length]; + var totalBytesRead = 0; + + // Use a loop to ensure we read everything (handles partial reads) + while (totalBytesRead < base.Length) + { + var bytesRead = base.Read(data, totalBytesRead, (int)(base.Length - totalBytesRead)); + if (bytesRead == 0) break; // End of stream + totalBytesRead += bytesRead; + } + + // Restore original position Seek(position, SeekOrigin.Begin); - /* .. put it in the mock system */ + + // Update the shared content - this is what makes changes visible to other streams + // Setting Contents will increment the ContentVersion, notifying other streams to refresh mockFileData.Contents = data; + + // Update our version tracking to match what we just wrote + lastKnownContentVersion = mockFileData.ContentVersion; + + // Clear the flag since we've now flushed all pending changes + hasUnflushedWrites = false; } } diff --git a/tests/TestableIO.System.IO.Abstractions.TestingHelpers.Tests/MockFileStreamTests.cs b/tests/TestableIO.System.IO.Abstractions.TestingHelpers.Tests/MockFileStreamTests.cs index b82566d9a..c649aad88 100644 --- a/tests/TestableIO.System.IO.Abstractions.TestingHelpers.Tests/MockFileStreamTests.cs +++ b/tests/TestableIO.System.IO.Abstractions.TestingHelpers.Tests/MockFileStreamTests.cs @@ -87,6 +87,400 @@ public async Task MockFileStream_Constructor_Reading_Nonexistent_File_Throws_Exc // Assert - expect an exception } + [Test] + public async Task MockFileStream_SharedFileContents_ShouldBeVisible() + { + // Reproduce issue #1131: The mock FileStream class does not handle shared file contents correctly + var fileSystem = new MockFileSystem(); + var filename = fileSystem.Path.GetTempFileName(); + + try + { + using var file1 = fileSystem.FileStream.New(filename, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite); + using var file2 = fileSystem.FileStream.New(filename, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite); + + var buffer = new byte[4]; + + for (int ix = 0; ix < 3; ix++) + { + file1.Position = 0; + file1.Write(BitConverter.GetBytes(ix)); + file1.Flush(); + + file2.Position = 0; + file2.Flush(); + var bytesRead = file2.Read(buffer); + int readValue = BitConverter.ToInt32(buffer); + + await That(readValue).IsEqualTo(ix) + .Because($"file2 should read the value {ix} that was written by file1, but got {readValue}"); + } + } + finally + { + fileSystem.File.Delete(filename); + } + } + + [Test] + public async Task MockFileStream_SharedContent_SetLengthTruncation_ShouldBeVisible() + { + var fileSystem = new MockFileSystem(); + var filename = fileSystem.Path.GetTempFileName(); + + try + { + using var file1 = fileSystem.FileStream.New(filename, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite); + using var file2 = fileSystem.FileStream.New(filename, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite); + + // Write initial data + file1.Write(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 }); + file1.Flush(); + + // Verify file2 can see the data + file2.Position = 0; + var buffer = new byte[8]; + var bytesRead = file2.Read(buffer); + await That(bytesRead).IsEqualTo(8); + + // Truncate file via file1 + file1.SetLength(4); + file1.Flush(); + + // Verify file2 sees the truncation + file2.Position = 0; + buffer = new byte[8]; + bytesRead = file2.Read(buffer); + await That(bytesRead).IsEqualTo(4) + .Because("file2 should see truncated length"); + await That(new byte[] { buffer[0], buffer[1], buffer[2], buffer[3] }).IsEquivalentTo(new byte[] { 1, 2, 3, 4 }); + } + finally + { + fileSystem.File.Delete(filename); + } + } + + [Test] + public async Task MockFileStream_SharedContent_PositionBeyondFileBounds_ShouldHandleGracefully() + { + var fileSystem = new MockFileSystem(); + var filename = fileSystem.Path.GetTempFileName(); + + try + { + using var file1 = fileSystem.FileStream.New(filename, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite); + using var file2 = fileSystem.FileStream.New(filename, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite); + + // Write some data and position file2 beyond it + file1.Write(new byte[] { 1, 2, 3, 4 }); + file1.Flush(); + + file2.Position = 10; // Beyond file end + + // Truncate file via file1 + file1.SetLength(2); + file1.Flush(); + + // file2 position should be adjusted + var buffer = new byte[4]; + var bytesRead = file2.Read(buffer); + await That(bytesRead).IsEqualTo(0) + .Because("reading beyond file end should return 0 bytes"); + await That(file2.Position).IsLessThanOrEqualTo(file2.Length) + .Because("position should be adjusted to file bounds"); + } + finally + { + fileSystem.File.Delete(filename); + } + } + + [Test] + public async Task MockFileStream_SharedContent_ConcurrentWritesToDifferentPositions() + { + var fileSystem = new MockFileSystem(); + var filename = fileSystem.Path.GetTempFileName(); + + try + { + using var file1 = fileSystem.FileStream.New(filename, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite); + using var file2 = fileSystem.FileStream.New(filename, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite); + + // Pre-allocate space + file1.SetLength(20); + file1.Flush(); + + // Write to different positions + file1.Position = 0; + file1.Write(new byte[] { 1, 1, 1, 1 }); + file1.Flush(); + + file2.Position = 10; + file2.Write(new byte[] { 2, 2, 2, 2 }); + file2.Flush(); + + // Verify both writes are visible + file1.Position = 10; + var buffer1 = new byte[4]; + var bytesRead1 = file1.Read(buffer1); + await That(bytesRead1).IsEqualTo(4); + await That(buffer1).IsEquivalentTo(new byte[] { 2, 2, 2, 2 }); + + file2.Position = 0; + var buffer2 = new byte[4]; + var bytesRead2 = file2.Read(buffer2); + await That(bytesRead2).IsEqualTo(4); + await That(buffer2).IsEquivalentTo(new byte[] { 1, 1, 1, 1 }); + } + finally + { + fileSystem.File.Delete(filename); + } + } + + [Test] + public async Task MockFileStream_SharedContent_ReadOnlyStreamShouldRefresh() + { + var fileSystem = new MockFileSystem(); + fileSystem.AddFile("test.txt", new MockFileData("initial")); + + using var writeStream = fileSystem.FileStream.New("test.txt", FileMode.Open, FileAccess.Write, FileShare.ReadWrite); + using var readStream = fileSystem.FileStream.New("test.txt", FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + + // Verify initial content + var buffer = new byte[7]; + var bytesRead = readStream.Read(buffer); + await That(bytesRead).IsEqualTo(7); + await That(System.Text.Encoding.UTF8.GetString(buffer)).IsEqualTo("initial"); + + // Write new content + writeStream.Position = 0; + writeStream.Write(System.Text.Encoding.UTF8.GetBytes("updated")); + writeStream.Flush(); + + // Read-only stream should see updated content + readStream.Position = 0; + buffer = new byte[7]; + bytesRead = readStream.Read(buffer); + await That(bytesRead).IsEqualTo(7); + await That(System.Text.Encoding.UTF8.GetString(buffer)).IsEqualTo("updated"); + } + + [Test] + public async Task MockFileStream_SharedContent_WriteOnlyStreamShouldNotUnnecessarilyRefresh() + { + var fileSystem = new MockFileSystem(); + fileSystem.AddFile("test.txt", new MockFileData("initial")); + + using var readStream = fileSystem.FileStream.New("test.txt", FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + using var writeStream = fileSystem.FileStream.New("test.txt", FileMode.Open, FileAccess.Write, FileShare.ReadWrite); + + // Read initial content + var buffer = new byte[7]; + var bytesRead = readStream.Read(buffer); + await That(bytesRead).IsEqualTo(7); + + // Write to write-only stream + writeStream.Position = 0; + writeStream.Write(System.Text.Encoding.UTF8.GetBytes("changed")); + writeStream.Flush(); + + // Read stream should see the change + readStream.Position = 0; + buffer = new byte[7]; + bytesRead = readStream.Read(buffer); + await That(bytesRead).IsEqualTo(7); + await That(System.Text.Encoding.UTF8.GetString(buffer)).IsEqualTo("changed"); + } + + [Test] + public async Task MockFileStream_SharedContent_PartialReadsAndWrites() + { + var fileSystem = new MockFileSystem(); + var filename = fileSystem.Path.GetTempFileName(); + + try + { + using var file1 = fileSystem.FileStream.New(filename, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite); + using var file2 = fileSystem.FileStream.New(filename, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite); + + // Write data in chunks + file1.Write(new byte[] { 1, 2, 3, 4 }); + file1.Write(new byte[] { 5, 6, 7, 8 }); + file1.Flush(); + + // Read data in different chunk sizes from file2 + var buffer = new byte[3]; + + // First partial read + var bytesRead1 = file2.Read(buffer); + await That(bytesRead1).IsEqualTo(3); + await That(buffer).IsEquivalentTo(new byte[] { 1, 2, 3 }); + + // Second partial read + var bytesRead2 = file2.Read(buffer); + await That(bytesRead2).IsEqualTo(3); + await That(buffer).IsEquivalentTo(new byte[] { 4, 5, 6 }); + + // Final partial read + buffer = new byte[5]; + var bytesRead3 = file2.Read(buffer); + await That(bytesRead3).IsEqualTo(2); + await That(new byte[] { buffer[0], buffer[1] }).IsEquivalentTo(new byte[] { 7, 8 }); + } + finally + { + fileSystem.File.Delete(filename); + } + } + + [Test] + public async Task MockFileStream_SharedContent_FileExtensionShouldBeVisible() + { + var fileSystem = new MockFileSystem(); + var filename = fileSystem.Path.GetTempFileName(); + + try + { + using var file1 = fileSystem.FileStream.New(filename, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite); + using var file2 = fileSystem.FileStream.New(filename, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite); + + // Write initial data + file1.Write(new byte[] { 1, 2, 3, 4 }); + file1.Flush(); + + // Verify file2 sees initial length + await That(file2.Length).IsEqualTo(4); + + // Extend file via file1 + file1.SetLength(10); + file1.Position = 8; + file1.Write(new byte[] { 9, 10 }); + file1.Flush(); + + // file2 should see extended file + await That(file2.Length).IsEqualTo(10); + + file2.Position = 8; + var buffer = new byte[2]; + var bytesRead = file2.Read(buffer); + await That(bytesRead).IsEqualTo(2); + await That(buffer).IsEquivalentTo(new byte[] { 9, 10 }); + } + finally + { + fileSystem.File.Delete(filename); + } + } + + [Test] + public async Task MockFileStream_SharedContent_DisposedStreamsShouldNotAffectVersioning() + { + var fileSystem = new MockFileSystem(); + var filename = fileSystem.Path.GetTempFileName(); + + try + { + using var persistentStream = fileSystem.FileStream.New(filename, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite); + + // Create and dispose a stream that writes data + using (var tempStream = fileSystem.FileStream.New(filename, FileMode.Open, FileAccess.Write, FileShare.ReadWrite)) + { + tempStream.Write(new byte[] { 1, 2, 3, 4 }); + tempStream.Flush(); + } // tempStream is disposed here + + // persistentStream should still see the data + persistentStream.Position = 0; + var buffer = new byte[4]; + var bytesRead = persistentStream.Read(buffer); + await That(bytesRead).IsEqualTo(4); + await That(buffer).IsEquivalentTo(new byte[] { 1, 2, 3, 4 }); + } + finally + { + fileSystem.File.Delete(filename); + } + } + + [Test] + public async Task MockFileStream_SharedContent_LargeFile_ShouldPerformWell() + { + var fileSystem = new MockFileSystem(); + var filename = fileSystem.Path.GetTempFileName(); + + try + { + using var file1 = fileSystem.FileStream.New(filename, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite); + using var file2 = fileSystem.FileStream.New(filename, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite); + + // Write a reasonably large amount of data + var largeData = new byte[1024 * 100]; // 100KB + for (int i = 0; i < largeData.Length; i++) + { + largeData[i] = (byte)(i % 256); + } + + var stopwatch = System.Diagnostics.Stopwatch.StartNew(); + + file1.Write(largeData); + file1.Flush(); + + // file2 should be able to read the large data efficiently + file2.Position = 0; + var readData = new byte[largeData.Length]; + var bytesRead = file2.Read(readData); + + stopwatch.Stop(); + + await That(bytesRead).IsEqualTo(largeData.Length); + await That(readData).IsEquivalentTo(largeData); + await That(stopwatch.ElapsedMilliseconds).IsLessThan(1000) + .Because("large file operations should be reasonably fast"); + } + finally + { + fileSystem.File.Delete(filename); + } + } + + [Test] + public async Task MockFileStream_SharedContent_VersionOverflowHandling() + { + var fileSystem = new MockFileSystem(); + var filename = fileSystem.Path.GetTempFileName(); + + try + { + using var file1 = fileSystem.FileStream.New(filename, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite); + using var file2 = fileSystem.FileStream.New(filename, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite); + + // Get reference to file data to manipulate version directly + var fileData = fileSystem.GetFile(filename); + + // Simulate near-overflow condition + var reflection = typeof(MockFileData).GetField("contentVersion", + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + reflection?.SetValue(fileData, long.MaxValue - 1); + + // Write data that will cause version to overflow + file1.Write(new byte[] { 1, 2, 3, 4 }); + file1.Flush(); + + // file2 should still be able to read the data despite version overflow + file2.Position = 0; + var buffer = new byte[4]; + var bytesRead = file2.Read(buffer); + await That(bytesRead).IsEqualTo(4); + await That(buffer).IsEquivalentTo(new byte[] { 1, 2, 3, 4 }); + } + finally + { + fileSystem.File.Delete(filename); + } + } + [Test] public async Task MockFileStream_Constructor_ReadTypeNotWritable() { From eb6b880a7c431505c3611099f931092bc6389f8b Mon Sep 17 00:00:00 2001 From: Mistial Developer Date: Sun, 13 Jul 2025 04:03:52 -0400 Subject: [PATCH 2/6] Fixed code sniffs (codacy). Made unit test deterministic for CI/CD --- .../MockFileData.cs | 2 +- .../MockFileStream.cs | 10 +++- .../MockFileStreamTests.cs | 47 ++++++++++--------- 3 files changed, 35 insertions(+), 24 deletions(-) diff --git a/src/TestableIO.System.IO.Abstractions.TestingHelpers/MockFileData.cs b/src/TestableIO.System.IO.Abstractions.TestingHelpers/MockFileData.cs index 30fb2ea29..e6f4cf7bc 100644 --- a/src/TestableIO.System.IO.Abstractions.TestingHelpers/MockFileData.cs +++ b/src/TestableIO.System.IO.Abstractions.TestingHelpers/MockFileData.cs @@ -120,7 +120,7 @@ public MockFileData(MockFileData template) } private byte[] contents; - private long contentVersion = 0; + private long contentVersion; /// /// Gets or sets the byte contents of the . diff --git a/src/TestableIO.System.IO.Abstractions.TestingHelpers/MockFileStream.cs b/src/TestableIO.System.IO.Abstractions.TestingHelpers/MockFileStream.cs index 32510b6dc..7b1676cc9 100644 --- a/src/TestableIO.System.IO.Abstractions.TestingHelpers/MockFileStream.cs +++ b/src/TestableIO.System.IO.Abstractions.TestingHelpers/MockFileStream.cs @@ -394,7 +394,10 @@ private void RefreshFromSharedContent(MockFileData mockFileData = null) while (totalBytesRead < preservedLength) { var bytesRead = base.Read(preservedContent, totalBytesRead, (int)(preservedLength - totalBytesRead)); - if (bytesRead == 0) break; + if (bytesRead == 0) + { + break; + } totalBytesRead += bytesRead; } Position = originalPosition; @@ -524,7 +527,10 @@ private void InternalFlush() while (totalBytesRead < base.Length) { var bytesRead = base.Read(data, totalBytesRead, (int)(base.Length - totalBytesRead)); - if (bytesRead == 0) break; // End of stream + if (bytesRead == 0) + { + break; // End of stream + } totalBytesRead += bytesRead; } diff --git a/tests/TestableIO.System.IO.Abstractions.TestingHelpers.Tests/MockFileStreamTests.cs b/tests/TestableIO.System.IO.Abstractions.TestingHelpers.Tests/MockFileStreamTests.cs index c649aad88..051745c3e 100644 --- a/tests/TestableIO.System.IO.Abstractions.TestingHelpers.Tests/MockFileStreamTests.cs +++ b/tests/TestableIO.System.IO.Abstractions.TestingHelpers.Tests/MockFileStreamTests.cs @@ -1,6 +1,7 @@ namespace System.IO.Abstractions.TestingHelpers.Tests; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using NUnit.Framework; @@ -109,7 +110,7 @@ public async Task MockFileStream_SharedFileContents_ShouldBeVisible() file2.Position = 0; file2.Flush(); - var bytesRead = file2.Read(buffer); + file2.Read(buffer); int readValue = BitConverter.ToInt32(buffer); await That(readValue).IsEqualTo(ix) @@ -153,7 +154,7 @@ public async Task MockFileStream_SharedContent_SetLengthTruncation_ShouldBeVisib bytesRead = file2.Read(buffer); await That(bytesRead).IsEqualTo(4) .Because("file2 should see truncated length"); - await That(new byte[] { buffer[0], buffer[1], buffer[2], buffer[3] }).IsEquivalentTo(new byte[] { 1, 2, 3, 4 }); + await That(buffer.Take(4).ToArray()).IsEquivalentTo(new byte[] { 1, 2, 3, 4 }); } finally { @@ -327,7 +328,7 @@ public async Task MockFileStream_SharedContent_PartialReadsAndWrites() buffer = new byte[5]; var bytesRead3 = file2.Read(buffer); await That(bytesRead3).IsEqualTo(2); - await That(new byte[] { buffer[0], buffer[1] }).IsEquivalentTo(new byte[] { 7, 8 }); + await That(buffer.Take(2).ToArray()).IsEquivalentTo(new byte[] { 7, 8 }); } finally { @@ -405,7 +406,7 @@ public async Task MockFileStream_SharedContent_DisposedStreamsShouldNotAffectVer } [Test] - public async Task MockFileStream_SharedContent_LargeFile_ShouldPerformWell() + public async Task MockFileStream_SharedContent_LargeFile_ShouldPerformCorrectly() { var fileSystem = new MockFileSystem(); var filename = fileSystem.Path.GetTempFileName(); @@ -415,29 +416,33 @@ public async Task MockFileStream_SharedContent_LargeFile_ShouldPerformWell() using var file1 = fileSystem.FileStream.New(filename, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite); using var file2 = fileSystem.FileStream.New(filename, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite); - // Write a reasonably large amount of data + // Test with a reasonably large amount of data to ensure synchronization works correctly var largeData = new byte[1024 * 100]; // 100KB for (int i = 0; i < largeData.Length; i++) { largeData[i] = (byte)(i % 256); } - var stopwatch = System.Diagnostics.Stopwatch.StartNew(); - - file1.Write(largeData); - file1.Flush(); - - // file2 should be able to read the large data efficiently - file2.Position = 0; - var readData = new byte[largeData.Length]; - var bytesRead = file2.Read(readData); - - stopwatch.Stop(); - - await That(bytesRead).IsEqualTo(largeData.Length); - await That(readData).IsEquivalentTo(largeData); - await That(stopwatch.ElapsedMilliseconds).IsLessThan(1000) - .Because("large file operations should be reasonably fast"); + // Test multiple write/read cycles to ensure synchronization is maintained + for (int cycle = 0; cycle < 3; cycle++) + { + // Modify some data for this cycle + largeData[cycle] = (byte)(cycle + 100); + + file1.Position = 0; + file1.Write(largeData); + file1.Flush(); + + // file2 should see the updated data + file2.Position = 0; + var readData = new byte[largeData.Length]; + var bytesRead = file2.Read(readData); + + await That(bytesRead).IsEqualTo(largeData.Length); + await That(readData).IsEquivalentTo(largeData); + await That(readData[cycle]).IsEqualTo((byte)(cycle + 100)) + .Because($"cycle {cycle} should see the updated data"); + } } finally { From 96baf92383864f15f8f4d6a4708a31287c19bc7b Mon Sep 17 00:00:00 2001 From: Mistial Developer Date: Sun, 13 Jul 2025 07:34:31 -0400 Subject: [PATCH 3/6] Refactored `MockFileStream` to simplify content synchronization logic and handle unflushed writes. Split a function to reduce complexity. Fixed code sniffs (especially insecure file path generation). Updated API baseline due to Read() and Length override. This should not be a breaking change. --- .../MockFileStream.cs | 218 ++++++++++-------- ....IO.Abstractions.TestingHelpers_net472.txt | 1 + ....IO.Abstractions.TestingHelpers_net6.0.txt | 2 + ....IO.Abstractions.TestingHelpers_net8.0.txt | 2 + ....IO.Abstractions.TestingHelpers_net9.0.txt | 2 + ...ractions.TestingHelpers_netstandard2.0.txt | 1 + ...ractions.TestingHelpers_netstandard2.1.txt | 2 + .../MockFileStreamTests.cs | 23 +- 8 files changed, 141 insertions(+), 110 deletions(-) diff --git a/src/TestableIO.System.IO.Abstractions.TestingHelpers/MockFileStream.cs b/src/TestableIO.System.IO.Abstractions.TestingHelpers/MockFileStream.cs index 7b1676cc9..d63b91975 100644 --- a/src/TestableIO.System.IO.Abstractions.TestingHelpers/MockFileStream.cs +++ b/src/TestableIO.System.IO.Abstractions.TestingHelpers/MockFileStream.cs @@ -2,6 +2,7 @@ using System.Threading; using System.Runtime.Versioning; using System.Security.AccessControl; +using System.Linq; namespace System.IO.Abstractions.TestingHelpers; @@ -75,6 +76,13 @@ public MockFileStream( var timeAdjustments = GetTimeAdjustmentsForFileStreamWhenFileExists(mode, access); mockFileDataAccessor.AdjustTimes(fileData, timeAdjustments); + + // For Truncate mode, clear the file contents first + if (mode == FileMode.Truncate) + { + fileData.Contents = new byte[0]; + } + var existingContents = fileData.Contents; var keepExistingContents = existingContents?.Length > 0 && @@ -145,7 +153,6 @@ public override long Length { get { - // Only refresh if needed to see latest file size from other streams RefreshFromSharedContentIfNeeded(); return base.Length; } @@ -374,112 +381,22 @@ private void RefreshFromSharedContent(MockFileData mockFileData = null) // This prevents unnecessary work and maintains performance if (mockFileData.ContentVersion != lastKnownContentVersion) { - // If we have unflushed writes, we need to preserve them when refreshing - // This handles the case where: - // 1. Stream A writes data but hasn't flushed - // 2. Stream B writes and flushes, updating shared content - // 3. Stream A needs to refresh but preserve its unflushed changes - byte[] preservedContent = null; - long preservedLength = 0; long currentPosition = Position; - if (hasUnflushedWrites) - { - // Save our current stream content to preserve unflushed writes - preservedLength = base.Length; - preservedContent = new byte[preservedLength]; - var originalPosition = Position; - Position = 0; - var totalBytesRead = 0; - while (totalBytesRead < preservedLength) - { - var bytesRead = base.Read(preservedContent, totalBytesRead, (int)(preservedLength - totalBytesRead)); - if (bytesRead == 0) - { - break; - } - totalBytesRead += bytesRead; - } - Position = originalPosition; - } + // Preserve unflushed content if necessary + var (preservedContent, preservedLength) = PreserveUnflushedContent(); var sharedContent = mockFileData.Contents; - // Performance optimization: if we have no unflushed writes and the shared content - // is the same length as our current content, we might not need to do expensive copying - if (!hasUnflushedWrites && sharedContent?.Length == base.Length) - { - // Quick content comparison for common case where only metadata changed - bool contentSame = true; - if (sharedContent.Length > 0 && sharedContent.Length <= 4096) // Only check small files - { - var currentPos = Position; - Position = 0; - var currentContent = new byte[base.Length]; - var bytesRead = base.Read(currentContent, 0, (int)base.Length); - Position = currentPos; - - if (bytesRead == sharedContent.Length) - { - for (int i = 0; i < bytesRead; i++) - { - if (currentContent[i] != sharedContent[i]) - { - contentSame = false; - break; - } - } - } - else - { - contentSame = false; - } - } - else - { - contentSame = false; // Don't compare large files - } - - if (contentSame) - { - // Content is identical, just update version tracking and exit - lastKnownContentVersion = mockFileData.ContentVersion; - return; - } - } - - // Start with shared content as the base - this gives us the latest changes from other streams - base.SetLength(0); - Position = 0; - if (sharedContent != null && sharedContent.Length > 0) + // Check if content is already the same (optimization for metadata-only changes) + if (IsContentIdentical(sharedContent)) { - base.Write(sharedContent, 0, sharedContent.Length); + lastKnownContentVersion = mockFileData.ContentVersion; + return; } - // If we had unflushed writes, we need to overlay them on the shared content - // This ensures our local changes take precedence over shared content - if (hasUnflushedWrites && preservedContent != null) - { - // Optimization: if preserved content is same length or longer, just use it directly - if (preservedLength >= (sharedContent?.Length ?? 0)) - { - base.SetLength(0); - Position = 0; - base.Write(preservedContent, 0, (int)preservedLength); - } - else - { - // Need to merge: ensure stream is large enough - if (base.Length < preservedLength) - { - base.SetLength(preservedLength); - } - - // Apply our preserved content on top of the shared content - Position = 0; - base.Write(preservedContent, 0, (int)preservedLength); - } - } + // Merge shared content with any preserved unflushed writes + MergeWithSharedContent(sharedContent, preservedContent, preservedLength); // Restore position, but ensure it's within bounds of the new content Position = Math.Min(currentPosition, base.Length); @@ -491,6 +408,109 @@ private void RefreshFromSharedContent(MockFileData mockFileData = null) } } + /// + /// Preserves unflushed content from the current stream before refreshing from shared content. + /// + /// A tuple containing the preserved content and its length. + private (byte[] content, long length) PreserveUnflushedContent() + { + if (!hasUnflushedWrites) + { + return (null, 0); + } + + // Save our current stream content to preserve unflushed writes + var preservedLength = base.Length; + var preservedContent = new byte[preservedLength]; + var originalPosition = Position; + Position = 0; + var totalBytesRead = 0; + while (totalBytesRead < preservedLength) + { + var bytesRead = base.Read(preservedContent, totalBytesRead, (int)(preservedLength - totalBytesRead)); + if (bytesRead == 0) + { + break; + } + totalBytesRead += bytesRead; + } + Position = originalPosition; + + return (preservedContent, preservedLength); + } + + /// + /// Checks if the current content is identical to the shared content (optimization). + /// + /// The shared content to compare against. + /// True if the content is identical, false otherwise. + private bool IsContentIdentical(byte[] sharedContent) + { + // Performance optimization: if we have no unflushed writes and the shared content + // is the same length as our current content, we might not need to do expensive copying + if (hasUnflushedWrites || sharedContent?.Length != base.Length) + { + return false; + } + + // Quick content comparison for common case where only metadata changed + if (sharedContent.Length > 0 && sharedContent.Length <= 4096) // Only check small files + { + var currentPos = Position; + Position = 0; + var currentContent = new byte[base.Length]; + var bytesRead = base.Read(currentContent, 0, (int)base.Length); + Position = currentPos; + + return bytesRead == sharedContent.Length && + currentContent.Take(bytesRead).SequenceEqual(sharedContent); + } + + return false; // Don't compare large files + } + + /// + /// Merges the shared content with any preserved unflushed writes. + /// + /// The shared content from MockFileData. + /// Any preserved unflushed content. + /// The length of the preserved content. + private void MergeWithSharedContent(byte[] sharedContent, byte[] preservedContent, long preservedLength) + { + // Start with shared content as the base - this gives us the latest changes from other streams + base.SetLength(0); + Position = 0; + if (sharedContent != null && sharedContent.Length > 0) + { + base.Write(sharedContent, 0, sharedContent.Length); + } + + // If we had unflushed writes, we need to overlay them on the shared content + // This ensures our local changes take precedence over shared content + if (hasUnflushedWrites && preservedContent != null) + { + // Optimization: if preserved content is same length or longer, just use it directly + if (preservedLength >= (sharedContent?.Length ?? 0)) + { + base.SetLength(0); + Position = 0; + base.Write(preservedContent, 0, (int)preservedLength); + } + else + { + // Need to merge: ensure stream is large enough + if (base.Length < preservedLength) + { + base.SetLength(preservedLength); + } + + // Apply our preserved content on top of the shared content + Position = 0; + base.Write(preservedContent, 0, (int)preservedLength); + } + } + } + /// /// Flushes this stream's content to the shared file data, implementing proper synchronization for FileShare.ReadWrite. /// diff --git a/tests/TestableIO.System.IO.Abstractions.Api.Tests/Expected/TestableIO.System.IO.Abstractions.TestingHelpers_net472.txt b/tests/TestableIO.System.IO.Abstractions.Api.Tests/Expected/TestableIO.System.IO.Abstractions.TestingHelpers_net472.txt index 788ddb703..7e85f8543 100644 --- a/tests/TestableIO.System.IO.Abstractions.Api.Tests/Expected/TestableIO.System.IO.Abstractions.TestingHelpers_net472.txt +++ b/tests/TestableIO.System.IO.Abstractions.Api.Tests/Expected/TestableIO.System.IO.Abstractions.TestingHelpers_net472.txt @@ -300,6 +300,7 @@ namespace System.IO.Abstractions.TestingHelpers public MockFileStream(System.IO.Abstractions.TestingHelpers.IMockFileDataAccessor mockFileDataAccessor, string path, System.IO.FileMode mode, System.IO.FileAccess access = 3, System.IO.FileOptions options = 0) { } public override bool CanRead { get; } public override bool CanWrite { get; } + public override long Length { get; } public static System.IO.Abstractions.FileSystemStream Null { get; } protected override void Dispose(bool disposing) { } public override void EndWrite(System.IAsyncResult asyncResult) { } diff --git a/tests/TestableIO.System.IO.Abstractions.Api.Tests/Expected/TestableIO.System.IO.Abstractions.TestingHelpers_net6.0.txt b/tests/TestableIO.System.IO.Abstractions.Api.Tests/Expected/TestableIO.System.IO.Abstractions.TestingHelpers_net6.0.txt index afd2ce3c4..32cfc2d63 100644 --- a/tests/TestableIO.System.IO.Abstractions.Api.Tests/Expected/TestableIO.System.IO.Abstractions.TestingHelpers_net6.0.txt +++ b/tests/TestableIO.System.IO.Abstractions.Api.Tests/Expected/TestableIO.System.IO.Abstractions.TestingHelpers_net6.0.txt @@ -349,6 +349,7 @@ namespace System.IO.Abstractions.TestingHelpers public MockFileStream(System.IO.Abstractions.TestingHelpers.IMockFileDataAccessor mockFileDataAccessor, string path, System.IO.FileMode mode, System.IO.FileAccess access = 3, System.IO.FileOptions options = 0) { } public override bool CanRead { get; } public override bool CanWrite { get; } + public override long Length { get; } public static System.IO.Abstractions.FileSystemStream Null { get; } protected override void Dispose(bool disposing) { } public override void EndWrite(System.IAsyncResult asyncResult) { } @@ -359,6 +360,7 @@ namespace System.IO.Abstractions.TestingHelpers public object GetAccessControl() { } [System.Runtime.Versioning.SupportedOSPlatform("windows")] public object GetAccessControl(System.IO.Abstractions.IFileSystemAclSupport.AccessControlSections includeSections) { } + public override int Read(System.Span buffer) { } public override int Read(byte[] buffer, int offset, int count) { } [System.Runtime.Versioning.SupportedOSPlatform("windows")] public void SetAccessControl(object value) { } diff --git a/tests/TestableIO.System.IO.Abstractions.Api.Tests/Expected/TestableIO.System.IO.Abstractions.TestingHelpers_net8.0.txt b/tests/TestableIO.System.IO.Abstractions.Api.Tests/Expected/TestableIO.System.IO.Abstractions.TestingHelpers_net8.0.txt index 1c581cab8..62e75e444 100644 --- a/tests/TestableIO.System.IO.Abstractions.Api.Tests/Expected/TestableIO.System.IO.Abstractions.TestingHelpers_net8.0.txt +++ b/tests/TestableIO.System.IO.Abstractions.Api.Tests/Expected/TestableIO.System.IO.Abstractions.TestingHelpers_net8.0.txt @@ -373,6 +373,7 @@ namespace System.IO.Abstractions.TestingHelpers public MockFileStream(System.IO.Abstractions.TestingHelpers.IMockFileDataAccessor mockFileDataAccessor, string path, System.IO.FileMode mode, System.IO.FileAccess access = 3, System.IO.FileOptions options = 0) { } public override bool CanRead { get; } public override bool CanWrite { get; } + public override long Length { get; } public static System.IO.Abstractions.FileSystemStream Null { get; } protected override void Dispose(bool disposing) { } public override void EndWrite(System.IAsyncResult asyncResult) { } @@ -383,6 +384,7 @@ namespace System.IO.Abstractions.TestingHelpers public object GetAccessControl() { } [System.Runtime.Versioning.SupportedOSPlatform("windows")] public object GetAccessControl(System.IO.Abstractions.IFileSystemAclSupport.AccessControlSections includeSections) { } + public override int Read(System.Span buffer) { } public override int Read(byte[] buffer, int offset, int count) { } [System.Runtime.Versioning.SupportedOSPlatform("windows")] public void SetAccessControl(object value) { } diff --git a/tests/TestableIO.System.IO.Abstractions.Api.Tests/Expected/TestableIO.System.IO.Abstractions.TestingHelpers_net9.0.txt b/tests/TestableIO.System.IO.Abstractions.Api.Tests/Expected/TestableIO.System.IO.Abstractions.TestingHelpers_net9.0.txt index 6c78cf677..06af4a047 100644 --- a/tests/TestableIO.System.IO.Abstractions.Api.Tests/Expected/TestableIO.System.IO.Abstractions.TestingHelpers_net9.0.txt +++ b/tests/TestableIO.System.IO.Abstractions.Api.Tests/Expected/TestableIO.System.IO.Abstractions.TestingHelpers_net9.0.txt @@ -387,6 +387,7 @@ namespace System.IO.Abstractions.TestingHelpers public MockFileStream(System.IO.Abstractions.TestingHelpers.IMockFileDataAccessor mockFileDataAccessor, string path, System.IO.FileMode mode, System.IO.FileAccess access = 3, System.IO.FileOptions options = 0) { } public override bool CanRead { get; } public override bool CanWrite { get; } + public override long Length { get; } public static System.IO.Abstractions.FileSystemStream Null { get; } protected override void Dispose(bool disposing) { } public override void EndWrite(System.IAsyncResult asyncResult) { } @@ -397,6 +398,7 @@ namespace System.IO.Abstractions.TestingHelpers public object GetAccessControl() { } [System.Runtime.Versioning.SupportedOSPlatform("windows")] public object GetAccessControl(System.IO.Abstractions.IFileSystemAclSupport.AccessControlSections includeSections) { } + public override int Read(System.Span buffer) { } public override int Read(byte[] buffer, int offset, int count) { } [System.Runtime.Versioning.SupportedOSPlatform("windows")] public void SetAccessControl(object value) { } diff --git a/tests/TestableIO.System.IO.Abstractions.Api.Tests/Expected/TestableIO.System.IO.Abstractions.TestingHelpers_netstandard2.0.txt b/tests/TestableIO.System.IO.Abstractions.Api.Tests/Expected/TestableIO.System.IO.Abstractions.TestingHelpers_netstandard2.0.txt index e0880b9f8..76974f94c 100644 --- a/tests/TestableIO.System.IO.Abstractions.Api.Tests/Expected/TestableIO.System.IO.Abstractions.TestingHelpers_netstandard2.0.txt +++ b/tests/TestableIO.System.IO.Abstractions.Api.Tests/Expected/TestableIO.System.IO.Abstractions.TestingHelpers_netstandard2.0.txt @@ -300,6 +300,7 @@ namespace System.IO.Abstractions.TestingHelpers public MockFileStream(System.IO.Abstractions.TestingHelpers.IMockFileDataAccessor mockFileDataAccessor, string path, System.IO.FileMode mode, System.IO.FileAccess access = 3, System.IO.FileOptions options = 0) { } public override bool CanRead { get; } public override bool CanWrite { get; } + public override long Length { get; } public static System.IO.Abstractions.FileSystemStream Null { get; } protected override void Dispose(bool disposing) { } public override void EndWrite(System.IAsyncResult asyncResult) { } diff --git a/tests/TestableIO.System.IO.Abstractions.Api.Tests/Expected/TestableIO.System.IO.Abstractions.TestingHelpers_netstandard2.1.txt b/tests/TestableIO.System.IO.Abstractions.Api.Tests/Expected/TestableIO.System.IO.Abstractions.TestingHelpers_netstandard2.1.txt index 80f876c0c..683213478 100644 --- a/tests/TestableIO.System.IO.Abstractions.Api.Tests/Expected/TestableIO.System.IO.Abstractions.TestingHelpers_netstandard2.1.txt +++ b/tests/TestableIO.System.IO.Abstractions.Api.Tests/Expected/TestableIO.System.IO.Abstractions.TestingHelpers_netstandard2.1.txt @@ -326,6 +326,7 @@ namespace System.IO.Abstractions.TestingHelpers public MockFileStream(System.IO.Abstractions.TestingHelpers.IMockFileDataAccessor mockFileDataAccessor, string path, System.IO.FileMode mode, System.IO.FileAccess access = 3, System.IO.FileOptions options = 0) { } public override bool CanRead { get; } public override bool CanWrite { get; } + public override long Length { get; } public static System.IO.Abstractions.FileSystemStream Null { get; } protected override void Dispose(bool disposing) { } public override void EndWrite(System.IAsyncResult asyncResult) { } @@ -334,6 +335,7 @@ namespace System.IO.Abstractions.TestingHelpers public override System.Threading.Tasks.Task FlushAsync(System.Threading.CancellationToken cancellationToken) { } public object GetAccessControl() { } public object GetAccessControl(System.IO.Abstractions.IFileSystemAclSupport.AccessControlSections includeSections) { } + public override int Read(System.Span buffer) { } public override int Read(byte[] buffer, int offset, int count) { } public void SetAccessControl(object value) { } public override void SetLength(long value) { } diff --git a/tests/TestableIO.System.IO.Abstractions.TestingHelpers.Tests/MockFileStreamTests.cs b/tests/TestableIO.System.IO.Abstractions.TestingHelpers.Tests/MockFileStreamTests.cs index 051745c3e..4b19df626 100644 --- a/tests/TestableIO.System.IO.Abstractions.TestingHelpers.Tests/MockFileStreamTests.cs +++ b/tests/TestableIO.System.IO.Abstractions.TestingHelpers.Tests/MockFileStreamTests.cs @@ -93,7 +93,7 @@ public async Task MockFileStream_SharedFileContents_ShouldBeVisible() { // Reproduce issue #1131: The mock FileStream class does not handle shared file contents correctly var fileSystem = new MockFileSystem(); - var filename = fileSystem.Path.GetTempFileName(); + var filename = fileSystem.Path.Combine(fileSystem.Path.GetTempPath(), fileSystem.Path.GetRandomFileName()); try { @@ -110,7 +110,8 @@ public async Task MockFileStream_SharedFileContents_ShouldBeVisible() file2.Position = 0; file2.Flush(); - file2.Read(buffer); + var bytesRead = file2.Read(buffer); + await That(bytesRead).IsEqualTo(4).Because("should read exactly 4 bytes"); int readValue = BitConverter.ToInt32(buffer); await That(readValue).IsEqualTo(ix) @@ -127,7 +128,7 @@ await That(readValue).IsEqualTo(ix) public async Task MockFileStream_SharedContent_SetLengthTruncation_ShouldBeVisible() { var fileSystem = new MockFileSystem(); - var filename = fileSystem.Path.GetTempFileName(); + var filename = fileSystem.Path.Combine(fileSystem.Path.GetTempPath(), fileSystem.Path.GetRandomFileName()); try { @@ -166,7 +167,7 @@ await That(bytesRead).IsEqualTo(4) public async Task MockFileStream_SharedContent_PositionBeyondFileBounds_ShouldHandleGracefully() { var fileSystem = new MockFileSystem(); - var filename = fileSystem.Path.GetTempFileName(); + var filename = fileSystem.Path.Combine(fileSystem.Path.GetTempPath(), fileSystem.Path.GetRandomFileName()); try { @@ -201,7 +202,7 @@ await That(file2.Position).IsLessThanOrEqualTo(file2.Length) public async Task MockFileStream_SharedContent_ConcurrentWritesToDifferentPositions() { var fileSystem = new MockFileSystem(); - var filename = fileSystem.Path.GetTempFileName(); + var filename = fileSystem.Path.Combine(fileSystem.Path.GetTempPath(), fileSystem.Path.GetRandomFileName()); try { @@ -299,7 +300,7 @@ public async Task MockFileStream_SharedContent_WriteOnlyStreamShouldNotUnnecessa public async Task MockFileStream_SharedContent_PartialReadsAndWrites() { var fileSystem = new MockFileSystem(); - var filename = fileSystem.Path.GetTempFileName(); + var filename = fileSystem.Path.Combine(fileSystem.Path.GetTempPath(), fileSystem.Path.GetRandomFileName()); try { @@ -337,10 +338,10 @@ public async Task MockFileStream_SharedContent_PartialReadsAndWrites() } [Test] - public async Task MockFileStream_SharedContent_FileExtensionShouldBeVisible() + public async Task MockFileStream_SharedContent_FileLengthExtensionShouldBeVisible() { var fileSystem = new MockFileSystem(); - var filename = fileSystem.Path.GetTempFileName(); + var filename = fileSystem.Path.Combine(fileSystem.Path.GetTempPath(), fileSystem.Path.GetRandomFileName()); try { @@ -379,7 +380,7 @@ public async Task MockFileStream_SharedContent_FileExtensionShouldBeVisible() public async Task MockFileStream_SharedContent_DisposedStreamsShouldNotAffectVersioning() { var fileSystem = new MockFileSystem(); - var filename = fileSystem.Path.GetTempFileName(); + var filename = fileSystem.Path.Combine(fileSystem.Path.GetTempPath(), fileSystem.Path.GetRandomFileName()); try { @@ -409,7 +410,7 @@ public async Task MockFileStream_SharedContent_DisposedStreamsShouldNotAffectVer public async Task MockFileStream_SharedContent_LargeFile_ShouldPerformCorrectly() { var fileSystem = new MockFileSystem(); - var filename = fileSystem.Path.GetTempFileName(); + var filename = fileSystem.Path.Combine(fileSystem.Path.GetTempPath(), fileSystem.Path.GetRandomFileName()); try { @@ -454,7 +455,7 @@ await That(readData[cycle]).IsEqualTo((byte)(cycle + 100)) public async Task MockFileStream_SharedContent_VersionOverflowHandling() { var fileSystem = new MockFileSystem(); - var filename = fileSystem.Path.GetTempFileName(); + var filename = fileSystem.Path.Combine(fileSystem.Path.GetTempPath(), fileSystem.Path.GetRandomFileName()); try { From fe15c80f843c92ddbcc601e80c14bd2433e6d612 Mon Sep 17 00:00:00 2001 From: Mistial Developer Date: Sun, 13 Jul 2025 07:50:28 -0400 Subject: [PATCH 4/6] Removed unnecessary unit test and introduced `MaxContentComparisonSize` constant for cleaner comparison logic in `MockFileStream`. --- .../MockFileStream.cs | 5 ++- .../MockFileStreamTests.cs | 35 ------------------- 2 files changed, 4 insertions(+), 36 deletions(-) diff --git a/src/TestableIO.System.IO.Abstractions.TestingHelpers/MockFileStream.cs b/src/TestableIO.System.IO.Abstractions.TestingHelpers/MockFileStream.cs index d63b91975..1e8b74655 100644 --- a/src/TestableIO.System.IO.Abstractions.TestingHelpers/MockFileStream.cs +++ b/src/TestableIO.System.IO.Abstractions.TestingHelpers/MockFileStream.cs @@ -45,6 +45,9 @@ public NullFileSystemStream() : base(Null, ".", true) // This prevents us from overwriting shared content unnecessarily and helps preserve unflushed changes during refresh private bool hasUnflushedWrites; + // Maximum file size for content comparison optimization + private const int MaxContentComparisonSize = 4096; + /// public MockFileStream( @@ -454,7 +457,7 @@ private bool IsContentIdentical(byte[] sharedContent) } // Quick content comparison for common case where only metadata changed - if (sharedContent.Length > 0 && sharedContent.Length <= 4096) // Only check small files + if (sharedContent.Length > 0 && sharedContent.Length <= MaxContentComparisonSize) // Only check small files { var currentPos = Position; Position = 0; diff --git a/tests/TestableIO.System.IO.Abstractions.TestingHelpers.Tests/MockFileStreamTests.cs b/tests/TestableIO.System.IO.Abstractions.TestingHelpers.Tests/MockFileStreamTests.cs index 4b19df626..f23d6ea56 100644 --- a/tests/TestableIO.System.IO.Abstractions.TestingHelpers.Tests/MockFileStreamTests.cs +++ b/tests/TestableIO.System.IO.Abstractions.TestingHelpers.Tests/MockFileStreamTests.cs @@ -451,41 +451,6 @@ await That(readData[cycle]).IsEqualTo((byte)(cycle + 100)) } } - [Test] - public async Task MockFileStream_SharedContent_VersionOverflowHandling() - { - var fileSystem = new MockFileSystem(); - var filename = fileSystem.Path.Combine(fileSystem.Path.GetTempPath(), fileSystem.Path.GetRandomFileName()); - - try - { - using var file1 = fileSystem.FileStream.New(filename, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite); - using var file2 = fileSystem.FileStream.New(filename, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite); - - // Get reference to file data to manipulate version directly - var fileData = fileSystem.GetFile(filename); - - // Simulate near-overflow condition - var reflection = typeof(MockFileData).GetField("contentVersion", - System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); - reflection?.SetValue(fileData, long.MaxValue - 1); - - // Write data that will cause version to overflow - file1.Write(new byte[] { 1, 2, 3, 4 }); - file1.Flush(); - - // file2 should still be able to read the data despite version overflow - file2.Position = 0; - var buffer = new byte[4]; - var bytesRead = file2.Read(buffer); - await That(bytesRead).IsEqualTo(4); - await That(buffer).IsEquivalentTo(new byte[] { 1, 2, 3, 4 }); - } - finally - { - fileSystem.File.Delete(filename); - } - } [Test] public async Task MockFileStream_Constructor_ReadTypeNotWritable() From cb0409a71b41db8f1108961aed9e7513daa15c8f Mon Sep 17 00:00:00 2001 From: Mistial Developer Date: Sun, 13 Jul 2025 09:23:56 -0400 Subject: [PATCH 5/6] Add .net 472 support --- .../MockFileStreamTests.cs | 60 +++++++++---------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/tests/TestableIO.System.IO.Abstractions.TestingHelpers.Tests/MockFileStreamTests.cs b/tests/TestableIO.System.IO.Abstractions.TestingHelpers.Tests/MockFileStreamTests.cs index f23d6ea56..a80bd9d68 100644 --- a/tests/TestableIO.System.IO.Abstractions.TestingHelpers.Tests/MockFileStreamTests.cs +++ b/tests/TestableIO.System.IO.Abstractions.TestingHelpers.Tests/MockFileStreamTests.cs @@ -105,14 +105,14 @@ public async Task MockFileStream_SharedFileContents_ShouldBeVisible() for (int ix = 0; ix < 3; ix++) { file1.Position = 0; - file1.Write(BitConverter.GetBytes(ix)); + file1.Write(BitConverter.GetBytes(ix), 0, 4); file1.Flush(); file2.Position = 0; file2.Flush(); - var bytesRead = file2.Read(buffer); + var bytesRead = file2.Read(buffer, 0, buffer.Length); await That(bytesRead).IsEqualTo(4).Because("should read exactly 4 bytes"); - int readValue = BitConverter.ToInt32(buffer); + int readValue = BitConverter.ToInt32(buffer, 0); await That(readValue).IsEqualTo(ix) .Because($"file2 should read the value {ix} that was written by file1, but got {readValue}"); @@ -136,13 +136,13 @@ public async Task MockFileStream_SharedContent_SetLengthTruncation_ShouldBeVisib using var file2 = fileSystem.FileStream.New(filename, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite); // Write initial data - file1.Write(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 }); + file1.Write(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 }, 0, 8); file1.Flush(); // Verify file2 can see the data file2.Position = 0; var buffer = new byte[8]; - var bytesRead = file2.Read(buffer); + var bytesRead = file2.Read(buffer, 0, buffer.Length); await That(bytesRead).IsEqualTo(8); // Truncate file via file1 @@ -152,7 +152,7 @@ public async Task MockFileStream_SharedContent_SetLengthTruncation_ShouldBeVisib // Verify file2 sees the truncation file2.Position = 0; buffer = new byte[8]; - bytesRead = file2.Read(buffer); + bytesRead = file2.Read(buffer, 0, buffer.Length); await That(bytesRead).IsEqualTo(4) .Because("file2 should see truncated length"); await That(buffer.Take(4).ToArray()).IsEquivalentTo(new byte[] { 1, 2, 3, 4 }); @@ -175,7 +175,7 @@ public async Task MockFileStream_SharedContent_PositionBeyondFileBounds_ShouldHa using var file2 = fileSystem.FileStream.New(filename, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite); // Write some data and position file2 beyond it - file1.Write(new byte[] { 1, 2, 3, 4 }); + file1.Write(new byte[] { 1, 2, 3, 4 }, 0, 4); file1.Flush(); file2.Position = 10; // Beyond file end @@ -186,7 +186,7 @@ public async Task MockFileStream_SharedContent_PositionBeyondFileBounds_ShouldHa // file2 position should be adjusted var buffer = new byte[4]; - var bytesRead = file2.Read(buffer); + var bytesRead = file2.Read(buffer, 0, buffer.Length); await That(bytesRead).IsEqualTo(0) .Because("reading beyond file end should return 0 bytes"); await That(file2.Position).IsLessThanOrEqualTo(file2.Length) @@ -215,23 +215,23 @@ public async Task MockFileStream_SharedContent_ConcurrentWritesToDifferentPositi // Write to different positions file1.Position = 0; - file1.Write(new byte[] { 1, 1, 1, 1 }); + file1.Write(new byte[] { 1, 1, 1, 1 }, 0, 4); file1.Flush(); file2.Position = 10; - file2.Write(new byte[] { 2, 2, 2, 2 }); + file2.Write(new byte[] { 2, 2, 2, 2 }, 0, 4); file2.Flush(); // Verify both writes are visible file1.Position = 10; var buffer1 = new byte[4]; - var bytesRead1 = file1.Read(buffer1); + var bytesRead1 = file1.Read(buffer1, 0, buffer1.Length); await That(bytesRead1).IsEqualTo(4); await That(buffer1).IsEquivalentTo(new byte[] { 2, 2, 2, 2 }); file2.Position = 0; var buffer2 = new byte[4]; - var bytesRead2 = file2.Read(buffer2); + var bytesRead2 = file2.Read(buffer2, 0, buffer2.Length); await That(bytesRead2).IsEqualTo(4); await That(buffer2).IsEquivalentTo(new byte[] { 1, 1, 1, 1 }); } @@ -252,19 +252,19 @@ public async Task MockFileStream_SharedContent_ReadOnlyStreamShouldRefresh() // Verify initial content var buffer = new byte[7]; - var bytesRead = readStream.Read(buffer); + var bytesRead = readStream.Read(buffer, 0, buffer.Length); await That(bytesRead).IsEqualTo(7); await That(System.Text.Encoding.UTF8.GetString(buffer)).IsEqualTo("initial"); // Write new content writeStream.Position = 0; - writeStream.Write(System.Text.Encoding.UTF8.GetBytes("updated")); + var updatedBytes = System.Text.Encoding.UTF8.GetBytes("updated"); writeStream.Write(updatedBytes, 0, updatedBytes.Length); writeStream.Flush(); // Read-only stream should see updated content readStream.Position = 0; buffer = new byte[7]; - bytesRead = readStream.Read(buffer); + bytesRead = readStream.Read(buffer, 0, buffer.Length); await That(bytesRead).IsEqualTo(7); await That(System.Text.Encoding.UTF8.GetString(buffer)).IsEqualTo("updated"); } @@ -280,18 +280,18 @@ public async Task MockFileStream_SharedContent_WriteOnlyStreamShouldNotUnnecessa // Read initial content var buffer = new byte[7]; - var bytesRead = readStream.Read(buffer); + var bytesRead = readStream.Read(buffer, 0, buffer.Length); await That(bytesRead).IsEqualTo(7); // Write to write-only stream writeStream.Position = 0; - writeStream.Write(System.Text.Encoding.UTF8.GetBytes("changed")); + var changedBytes = System.Text.Encoding.UTF8.GetBytes("changed"); writeStream.Write(changedBytes, 0, changedBytes.Length); writeStream.Flush(); // Read stream should see the change readStream.Position = 0; buffer = new byte[7]; - bytesRead = readStream.Read(buffer); + bytesRead = readStream.Read(buffer, 0, buffer.Length); await That(bytesRead).IsEqualTo(7); await That(System.Text.Encoding.UTF8.GetString(buffer)).IsEqualTo("changed"); } @@ -308,26 +308,26 @@ public async Task MockFileStream_SharedContent_PartialReadsAndWrites() using var file2 = fileSystem.FileStream.New(filename, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite); // Write data in chunks - file1.Write(new byte[] { 1, 2, 3, 4 }); - file1.Write(new byte[] { 5, 6, 7, 8 }); + file1.Write(new byte[] { 1, 2, 3, 4 }, 0, 4); + file1.Write(new byte[] { 5, 6, 7, 8 }, 0, 4); file1.Flush(); // Read data in different chunk sizes from file2 var buffer = new byte[3]; // First partial read - var bytesRead1 = file2.Read(buffer); + var bytesRead1 = file2.Read(buffer, 0, buffer.Length); await That(bytesRead1).IsEqualTo(3); await That(buffer).IsEquivalentTo(new byte[] { 1, 2, 3 }); // Second partial read - var bytesRead2 = file2.Read(buffer); + var bytesRead2 = file2.Read(buffer, 0, buffer.Length); await That(bytesRead2).IsEqualTo(3); await That(buffer).IsEquivalentTo(new byte[] { 4, 5, 6 }); // Final partial read buffer = new byte[5]; - var bytesRead3 = file2.Read(buffer); + var bytesRead3 = file2.Read(buffer, 0, buffer.Length); await That(bytesRead3).IsEqualTo(2); await That(buffer.Take(2).ToArray()).IsEquivalentTo(new byte[] { 7, 8 }); } @@ -349,7 +349,7 @@ public async Task MockFileStream_SharedContent_FileLengthExtensionShouldBeVisibl using var file2 = fileSystem.FileStream.New(filename, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite); // Write initial data - file1.Write(new byte[] { 1, 2, 3, 4 }); + file1.Write(new byte[] { 1, 2, 3, 4 }, 0, 4); file1.Flush(); // Verify file2 sees initial length @@ -358,7 +358,7 @@ public async Task MockFileStream_SharedContent_FileLengthExtensionShouldBeVisibl // Extend file via file1 file1.SetLength(10); file1.Position = 8; - file1.Write(new byte[] { 9, 10 }); + file1.Write(new byte[] { 9, 10 }, 0, 2); file1.Flush(); // file2 should see extended file @@ -366,7 +366,7 @@ public async Task MockFileStream_SharedContent_FileLengthExtensionShouldBeVisibl file2.Position = 8; var buffer = new byte[2]; - var bytesRead = file2.Read(buffer); + var bytesRead = file2.Read(buffer, 0, buffer.Length); await That(bytesRead).IsEqualTo(2); await That(buffer).IsEquivalentTo(new byte[] { 9, 10 }); } @@ -389,14 +389,14 @@ public async Task MockFileStream_SharedContent_DisposedStreamsShouldNotAffectVer // Create and dispose a stream that writes data using (var tempStream = fileSystem.FileStream.New(filename, FileMode.Open, FileAccess.Write, FileShare.ReadWrite)) { - tempStream.Write(new byte[] { 1, 2, 3, 4 }); + tempStream.Write(new byte[] { 1, 2, 3, 4 }, 0, 4); tempStream.Flush(); } // tempStream is disposed here // persistentStream should still see the data persistentStream.Position = 0; var buffer = new byte[4]; - var bytesRead = persistentStream.Read(buffer); + var bytesRead = persistentStream.Read(buffer, 0, buffer.Length); await That(bytesRead).IsEqualTo(4); await That(buffer).IsEquivalentTo(new byte[] { 1, 2, 3, 4 }); } @@ -431,13 +431,13 @@ public async Task MockFileStream_SharedContent_LargeFile_ShouldPerformCorrectly( largeData[cycle] = (byte)(cycle + 100); file1.Position = 0; - file1.Write(largeData); + file1.Write(largeData, 0, largeData.Length); file1.Flush(); // file2 should see the updated data file2.Position = 0; var readData = new byte[largeData.Length]; - var bytesRead = file2.Read(readData); + var bytesRead = file2.Read(readData, 0, readData.Length); await That(bytesRead).IsEqualTo(largeData.Length); await That(readData).IsEquivalentTo(largeData); From bcbf8c3c56fea2e4af3e4b42fa95cb01bc68b820 Mon Sep 17 00:00:00 2001 From: Mistial Developer Date: Sun, 13 Jul 2025 09:40:11 -0400 Subject: [PATCH 6/6] Formatting fixes --- .../MockFileStreamTests.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/tests/TestableIO.System.IO.Abstractions.TestingHelpers.Tests/MockFileStreamTests.cs b/tests/TestableIO.System.IO.Abstractions.TestingHelpers.Tests/MockFileStreamTests.cs index a80bd9d68..be6afcad7 100644 --- a/tests/TestableIO.System.IO.Abstractions.TestingHelpers.Tests/MockFileStreamTests.cs +++ b/tests/TestableIO.System.IO.Abstractions.TestingHelpers.Tests/MockFileStreamTests.cs @@ -1,8 +1,8 @@ namespace System.IO.Abstractions.TestingHelpers.Tests; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; +using Collections.Generic; +using Linq; +using Threading.Tasks; using NUnit.Framework; @@ -258,7 +258,8 @@ public async Task MockFileStream_SharedContent_ReadOnlyStreamShouldRefresh() // Write new content writeStream.Position = 0; - var updatedBytes = System.Text.Encoding.UTF8.GetBytes("updated"); writeStream.Write(updatedBytes, 0, updatedBytes.Length); + var updatedBytes = "updated"u8.ToArray(); + writeStream.Write(updatedBytes, 0, updatedBytes.Length); writeStream.Flush(); // Read-only stream should see updated content @@ -285,7 +286,8 @@ public async Task MockFileStream_SharedContent_WriteOnlyStreamShouldNotUnnecessa // Write to write-only stream writeStream.Position = 0; - var changedBytes = System.Text.Encoding.UTF8.GetBytes("changed"); writeStream.Write(changedBytes, 0, changedBytes.Length); + var changedBytes = "changed"u8.ToArray(); + writeStream.Write(changedBytes, 0, changedBytes.Length); writeStream.Flush(); // Read stream should see the change