diff --git a/src/TestableIO.System.IO.Abstractions.TestingHelpers/MockFileStream.cs b/src/TestableIO.System.IO.Abstractions.TestingHelpers/MockFileStream.cs index c633dd8ef..fca762972 100644 --- a/src/TestableIO.System.IO.Abstractions.TestingHelpers/MockFileStream.cs +++ b/src/TestableIO.System.IO.Abstractions.TestingHelpers/MockFileStream.cs @@ -38,6 +38,8 @@ public NullFileSystemStream() : base(Null, ".", true) private readonly MockFileData fileData; private bool disposed; + private byte[] lastKnownContents; + /// public MockFileStream( IMockFileDataAccessor mockFileDataAccessor, @@ -104,6 +106,7 @@ public MockFileStream( mockFileDataAccessor.FileHandles.AddHandle(path, guid, access, share); this.access = access; this.share = share; + lastKnownContents = fileData.Contents; } private static void ThrowIfInvalidModeAccess(FileMode mode, FileAccess access) @@ -138,11 +141,46 @@ private static void ThrowIfInvalidModeAccess(FileMode mode, FileAccess access) /// public override int Read(byte[] buffer, int offset, int count) { + RefreshBufferFromSharedFileData(); mockFileDataAccessor.AdjustTimes(fileData, TimeAdjustments.LastAccessTime); return base.Read(buffer, offset, count); } +#if FEATURE_SPAN + /// + public override int Read(Span buffer) + { + RefreshBufferFromSharedFileData(); + return base.Read(buffer); + } +#endif + + /// + public override int ReadByte() + { + RefreshBufferFromSharedFileData(); + return base.ReadByte(); + } + + /// + public override Task ReadAsync(byte[] buffer, int offset, int count, + CancellationToken cancellationToken) + { + RefreshBufferFromSharedFileData(); + return base.ReadAsync(buffer, offset, count, cancellationToken); + } + +#if FEATURE_SPAN + /// + public override ValueTask ReadAsync(Memory buffer, + CancellationToken cancellationToken = new()) + { + RefreshBufferFromSharedFileData(); + return base.ReadAsync(buffer, cancellationToken); + } +#endif + /// protected override void Dispose(bool disposing) { @@ -296,16 +334,63 @@ private void InternalFlush() /* reset back to the beginning .. */ var position = Position; Seek(0, SeekOrigin.Begin); - /* .. read everything out */ + /* .. read everything out (bypassing the read-refresh, so we capture this handle's own buffer) */ var data = new byte[Length]; - _ = Read(data, 0, (int)Length); + _ = base.Read(data, 0, (int)Length); /* restore to original position */ Seek(position, SeekOrigin.Begin); - /* .. put it in the mock system */ - mockFileData.Contents = data; + /* .. put it in the mock system, but only if this handle actually changed the contents since it last synchronized. */ + if (!ContentsEqual(data, lastKnownContents)) + { + mockFileData.Contents = data; + lastKnownContents = data; + } } } + private static bool ContentsEqual(byte[] left, byte[] right) + { + if (ReferenceEquals(left, right)) + { + return true; + } + + if (left == null || right == null || left.Length != right.Length) + { + return false; + } + + for (var i = 0; i < left.Length; i++) + { + if (left[i] != right[i]) + { + return false; + } + } + + return true; + } + + private void RefreshBufferFromSharedFileData() + { + var sharedContents = fileData.Contents; + if (ReferenceEquals(sharedContents, lastKnownContents)) + { + return; + } + + var position = base.Position; + base.Position = 0; + base.SetLength(sharedContents.Length); + if (sharedContents.Length > 0) + { + base.Write(sharedContents, 0, sharedContents.Length); + } + + base.Position = position; + lastKnownContents = sharedContents; + } + private void OnClose() { if (options.HasFlag(FileOptions.DeleteOnClose) && mockFileDataAccessor.FileExists(path)) diff --git a/tests/TestableIO.System.IO.Abstractions.Api.Tests/Expected/TestableIO.System.IO.Abstractions.TestingHelpers_net10.0.txt b/tests/TestableIO.System.IO.Abstractions.Api.Tests/Expected/TestableIO.System.IO.Abstractions.TestingHelpers_net10.0.txt index 28283ec09..d7c082044 100644 --- a/tests/TestableIO.System.IO.Abstractions.Api.Tests/Expected/TestableIO.System.IO.Abstractions.TestingHelpers_net10.0.txt +++ b/tests/TestableIO.System.IO.Abstractions.Api.Tests/Expected/TestableIO.System.IO.Abstractions.TestingHelpers_net10.0.txt @@ -404,7 +404,11 @@ 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) { } + public override System.Threading.Tasks.ValueTask ReadAsync(System.Memory buffer, System.Threading.CancellationToken cancellationToken = default) { } + public override System.Threading.Tasks.Task ReadAsync(byte[] buffer, int offset, int count, System.Threading.CancellationToken cancellationToken) { } + public override int ReadByte() { } [System.Runtime.Versioning.SupportedOSPlatform("windows")] public void SetAccessControl(object value) { } public override void SetLength(long value) { } 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 bce734d4f..e64a25b2e 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 @@ -316,6 +316,8 @@ namespace System.IO.Abstractions.TestingHelpers public object GetAccessControl() { } public object GetAccessControl(System.IO.Abstractions.IFileSystemAclSupport.AccessControlSections includeSections) { } public override int Read(byte[] buffer, int offset, int count) { } + public override System.Threading.Tasks.Task ReadAsync(byte[] buffer, int offset, int count, System.Threading.CancellationToken cancellationToken) { } + public override int ReadByte() { } public void SetAccessControl(object value) { } public override void SetLength(long value) { } public override void Write(byte[] buffer, int offset, int count) { } 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 7fa0eecba..9aff85574 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 @@ -366,7 +366,11 @@ 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) { } + public override System.Threading.Tasks.ValueTask ReadAsync(System.Memory buffer, System.Threading.CancellationToken cancellationToken = default) { } + public override System.Threading.Tasks.Task ReadAsync(byte[] buffer, int offset, int count, System.Threading.CancellationToken cancellationToken) { } + public override int ReadByte() { } [System.Runtime.Versioning.SupportedOSPlatform("windows")] public void SetAccessControl(object value) { } public override void SetLength(long 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 e898230a1..e5788bd55 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 @@ -390,7 +390,11 @@ 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) { } + public override System.Threading.Tasks.ValueTask ReadAsync(System.Memory buffer, System.Threading.CancellationToken cancellationToken = default) { } + public override System.Threading.Tasks.Task ReadAsync(byte[] buffer, int offset, int count, System.Threading.CancellationToken cancellationToken) { } + public override int ReadByte() { } [System.Runtime.Versioning.SupportedOSPlatform("windows")] public void SetAccessControl(object value) { } public override void SetLength(long 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 dea869d1f..35cf35b2e 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 @@ -404,7 +404,11 @@ 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) { } + public override System.Threading.Tasks.ValueTask ReadAsync(System.Memory buffer, System.Threading.CancellationToken cancellationToken = default) { } + public override System.Threading.Tasks.Task ReadAsync(byte[] buffer, int offset, int count, System.Threading.CancellationToken cancellationToken) { } + public override int ReadByte() { } [System.Runtime.Versioning.SupportedOSPlatform("windows")] public void SetAccessControl(object value) { } public override void SetLength(long 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 fd8f7e4fc..1dc0ed02e 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 @@ -316,6 +316,8 @@ namespace System.IO.Abstractions.TestingHelpers public object GetAccessControl() { } public object GetAccessControl(System.IO.Abstractions.IFileSystemAclSupport.AccessControlSections includeSections) { } public override int Read(byte[] buffer, int offset, int count) { } + public override System.Threading.Tasks.Task ReadAsync(byte[] buffer, int offset, int count, System.Threading.CancellationToken cancellationToken) { } + public override int ReadByte() { } public void SetAccessControl(object value) { } public override void SetLength(long value) { } public override void Write(byte[] buffer, int offset, int count) { } 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 0bf9bd3e7..cba08bc11 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 @@ -341,7 +341,11 @@ 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 override System.Threading.Tasks.ValueTask ReadAsync(System.Memory buffer, System.Threading.CancellationToken cancellationToken = default) { } + public override System.Threading.Tasks.Task ReadAsync(byte[] buffer, int offset, int count, System.Threading.CancellationToken cancellationToken) { } + public override int ReadByte() { } public void SetAccessControl(object value) { } public override void SetLength(long value) { } public override void Write(System.ReadOnlySpan buffer) { } diff --git a/tests/TestableIO.System.IO.Abstractions.TestingHelpers.Tests/MockFileStreamTests.cs b/tests/TestableIO.System.IO.Abstractions.TestingHelpers.Tests/MockFileStreamTests.cs index a8f815a15..77fe218af 100644 --- a/tests/TestableIO.System.IO.Abstractions.TestingHelpers.Tests/MockFileStreamTests.cs +++ b/tests/TestableIO.System.IO.Abstractions.TestingHelpers.Tests/MockFileStreamTests.cs @@ -10,6 +10,39 @@ [TestFixture] public class MockFileStreamTests { + [Test] + public async Task MockFileStream_SharedReadWrite_SecondHandleSeesWritesFromFirstHandle() + { + // Arrange + var filename = XFS.Path(@"C:\temp\shared.bin"); + var fileSystem = new MockFileSystem(); + fileSystem.AddEmptyFile(filename); + var results = new List(); + + // Act + 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 < 10; ix++) + { + var bytes = BitConverter.GetBytes(ix); + file1.Position = 0; + file1.Write(bytes, 0, bytes.Length); + file1.Flush(); + + file2.Position = 0; + file2.Flush(); + var bytesRead = file2.Read(buffer, 0, buffer.Length); + await That(bytesRead).IsEqualTo(buffer.Length); + results.Add(BitConverter.ToInt32(buffer, 0)); + } + } + + // Assert + await That(results).IsEqualTo(new[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }); + } + [Test] public async Task MockFileStream_Flush_WritesByteToFile() {