Skip to content

Commit ec68061

Browse files
authored
fix: handle shared file contents across handles (#1507)
Each MockFileStream kept its own MemoryStream buffer that was only synced to the shared MockFileData.Contents on flush, so a second handle opened with FileShare.ReadWrite never saw writes made through another handle. On top of that, InternalFlush unconditionally wrote its buffer back, so a read-only handle's Flush could clobber another handle's writes. Track the last-synced Contents reference and reload the buffer from it on every read entry point (Read, ReadByte, ReadAsync, span overloads) when another handle has replaced it. InternalFlush now writes back only when the buffer actually differs from the last synced snapshot, matching the no-op behavior of a real FileStream.Flush with no pending writes.
1 parent 9cb6266 commit ec68061

9 files changed

Lines changed: 146 additions & 4 deletions

src/TestableIO.System.IO.Abstractions.TestingHelpers/MockFileStream.cs

Lines changed: 89 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ public NullFileSystemStream() : base(Null, ".", true)
3838
private readonly MockFileData fileData;
3939
private bool disposed;
4040

41+
private byte[] lastKnownContents;
42+
4143
/// <inheritdoc />
4244
public MockFileStream(
4345
IMockFileDataAccessor mockFileDataAccessor,
@@ -104,6 +106,7 @@ public MockFileStream(
104106
mockFileDataAccessor.FileHandles.AddHandle(path, guid, access, share);
105107
this.access = access;
106108
this.share = share;
109+
lastKnownContents = fileData.Contents;
107110
}
108111

109112
private static void ThrowIfInvalidModeAccess(FileMode mode, FileAccess access)
@@ -138,11 +141,46 @@ private static void ThrowIfInvalidModeAccess(FileMode mode, FileAccess access)
138141
/// <inheritdoc />
139142
public override int Read(byte[] buffer, int offset, int count)
140143
{
144+
RefreshBufferFromSharedFileData();
141145
mockFileDataAccessor.AdjustTimes(fileData,
142146
TimeAdjustments.LastAccessTime);
143147
return base.Read(buffer, offset, count);
144148
}
145149

150+
#if FEATURE_SPAN
151+
/// <inheritdoc />
152+
public override int Read(Span<byte> buffer)
153+
{
154+
RefreshBufferFromSharedFileData();
155+
return base.Read(buffer);
156+
}
157+
#endif
158+
159+
/// <inheritdoc />
160+
public override int ReadByte()
161+
{
162+
RefreshBufferFromSharedFileData();
163+
return base.ReadByte();
164+
}
165+
166+
/// <inheritdoc />
167+
public override Task<int> ReadAsync(byte[] buffer, int offset, int count,
168+
CancellationToken cancellationToken)
169+
{
170+
RefreshBufferFromSharedFileData();
171+
return base.ReadAsync(buffer, offset, count, cancellationToken);
172+
}
173+
174+
#if FEATURE_SPAN
175+
/// <inheritdoc />
176+
public override ValueTask<int> ReadAsync(Memory<byte> buffer,
177+
CancellationToken cancellationToken = new())
178+
{
179+
RefreshBufferFromSharedFileData();
180+
return base.ReadAsync(buffer, cancellationToken);
181+
}
182+
#endif
183+
146184
/// <inheritdoc />
147185
protected override void Dispose(bool disposing)
148186
{
@@ -296,16 +334,63 @@ private void InternalFlush()
296334
/* reset back to the beginning .. */
297335
var position = Position;
298336
Seek(0, SeekOrigin.Begin);
299-
/* .. read everything out */
337+
/* .. read everything out (bypassing the read-refresh, so we capture this handle's own buffer) */
300338
var data = new byte[Length];
301-
_ = Read(data, 0, (int)Length);
339+
_ = base.Read(data, 0, (int)Length);
302340
/* restore to original position */
303341
Seek(position, SeekOrigin.Begin);
304-
/* .. put it in the mock system */
305-
mockFileData.Contents = data;
342+
/* .. put it in the mock system, but only if this handle actually changed the contents since it last synchronized. */
343+
if (!ContentsEqual(data, lastKnownContents))
344+
{
345+
mockFileData.Contents = data;
346+
lastKnownContents = data;
347+
}
306348
}
307349
}
308350

351+
private static bool ContentsEqual(byte[] left, byte[] right)
352+
{
353+
if (ReferenceEquals(left, right))
354+
{
355+
return true;
356+
}
357+
358+
if (left == null || right == null || left.Length != right.Length)
359+
{
360+
return false;
361+
}
362+
363+
for (var i = 0; i < left.Length; i++)
364+
{
365+
if (left[i] != right[i])
366+
{
367+
return false;
368+
}
369+
}
370+
371+
return true;
372+
}
373+
374+
private void RefreshBufferFromSharedFileData()
375+
{
376+
var sharedContents = fileData.Contents;
377+
if (ReferenceEquals(sharedContents, lastKnownContents))
378+
{
379+
return;
380+
}
381+
382+
var position = base.Position;
383+
base.Position = 0;
384+
base.SetLength(sharedContents.Length);
385+
if (sharedContents.Length > 0)
386+
{
387+
base.Write(sharedContents, 0, sharedContents.Length);
388+
}
389+
390+
base.Position = position;
391+
lastKnownContents = sharedContents;
392+
}
393+
309394
private void OnClose()
310395
{
311396
if (options.HasFlag(FileOptions.DeleteOnClose) && mockFileDataAccessor.FileExists(path))

tests/TestableIO.System.IO.Abstractions.Api.Tests/Expected/TestableIO.System.IO.Abstractions.TestingHelpers_net10.0.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,7 +404,11 @@ namespace System.IO.Abstractions.TestingHelpers
404404
public object GetAccessControl() { }
405405
[System.Runtime.Versioning.SupportedOSPlatform("windows")]
406406
public object GetAccessControl(System.IO.Abstractions.IFileSystemAclSupport.AccessControlSections includeSections) { }
407+
public override int Read(System.Span<byte> buffer) { }
407408
public override int Read(byte[] buffer, int offset, int count) { }
409+
public override System.Threading.Tasks.ValueTask<int> ReadAsync(System.Memory<byte> buffer, System.Threading.CancellationToken cancellationToken = default) { }
410+
public override System.Threading.Tasks.Task<int> ReadAsync(byte[] buffer, int offset, int count, System.Threading.CancellationToken cancellationToken) { }
411+
public override int ReadByte() { }
408412
[System.Runtime.Versioning.SupportedOSPlatform("windows")]
409413
public void SetAccessControl(object value) { }
410414
public override void SetLength(long value) { }

tests/TestableIO.System.IO.Abstractions.Api.Tests/Expected/TestableIO.System.IO.Abstractions.TestingHelpers_net472.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,8 @@ namespace System.IO.Abstractions.TestingHelpers
316316
public object GetAccessControl() { }
317317
public object GetAccessControl(System.IO.Abstractions.IFileSystemAclSupport.AccessControlSections includeSections) { }
318318
public override int Read(byte[] buffer, int offset, int count) { }
319+
public override System.Threading.Tasks.Task<int> ReadAsync(byte[] buffer, int offset, int count, System.Threading.CancellationToken cancellationToken) { }
320+
public override int ReadByte() { }
319321
public void SetAccessControl(object value) { }
320322
public override void SetLength(long value) { }
321323
public override void Write(byte[] buffer, int offset, int count) { }

tests/TestableIO.System.IO.Abstractions.Api.Tests/Expected/TestableIO.System.IO.Abstractions.TestingHelpers_net6.0.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,7 +366,11 @@ namespace System.IO.Abstractions.TestingHelpers
366366
public object GetAccessControl() { }
367367
[System.Runtime.Versioning.SupportedOSPlatform("windows")]
368368
public object GetAccessControl(System.IO.Abstractions.IFileSystemAclSupport.AccessControlSections includeSections) { }
369+
public override int Read(System.Span<byte> buffer) { }
369370
public override int Read(byte[] buffer, int offset, int count) { }
371+
public override System.Threading.Tasks.ValueTask<int> ReadAsync(System.Memory<byte> buffer, System.Threading.CancellationToken cancellationToken = default) { }
372+
public override System.Threading.Tasks.Task<int> ReadAsync(byte[] buffer, int offset, int count, System.Threading.CancellationToken cancellationToken) { }
373+
public override int ReadByte() { }
370374
[System.Runtime.Versioning.SupportedOSPlatform("windows")]
371375
public void SetAccessControl(object value) { }
372376
public override void SetLength(long value) { }

tests/TestableIO.System.IO.Abstractions.Api.Tests/Expected/TestableIO.System.IO.Abstractions.TestingHelpers_net8.0.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,7 +390,11 @@ namespace System.IO.Abstractions.TestingHelpers
390390
public object GetAccessControl() { }
391391
[System.Runtime.Versioning.SupportedOSPlatform("windows")]
392392
public object GetAccessControl(System.IO.Abstractions.IFileSystemAclSupport.AccessControlSections includeSections) { }
393+
public override int Read(System.Span<byte> buffer) { }
393394
public override int Read(byte[] buffer, int offset, int count) { }
395+
public override System.Threading.Tasks.ValueTask<int> ReadAsync(System.Memory<byte> buffer, System.Threading.CancellationToken cancellationToken = default) { }
396+
public override System.Threading.Tasks.Task<int> ReadAsync(byte[] buffer, int offset, int count, System.Threading.CancellationToken cancellationToken) { }
397+
public override int ReadByte() { }
394398
[System.Runtime.Versioning.SupportedOSPlatform("windows")]
395399
public void SetAccessControl(object value) { }
396400
public override void SetLength(long value) { }

tests/TestableIO.System.IO.Abstractions.Api.Tests/Expected/TestableIO.System.IO.Abstractions.TestingHelpers_net9.0.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,7 +404,11 @@ namespace System.IO.Abstractions.TestingHelpers
404404
public object GetAccessControl() { }
405405
[System.Runtime.Versioning.SupportedOSPlatform("windows")]
406406
public object GetAccessControl(System.IO.Abstractions.IFileSystemAclSupport.AccessControlSections includeSections) { }
407+
public override int Read(System.Span<byte> buffer) { }
407408
public override int Read(byte[] buffer, int offset, int count) { }
409+
public override System.Threading.Tasks.ValueTask<int> ReadAsync(System.Memory<byte> buffer, System.Threading.CancellationToken cancellationToken = default) { }
410+
public override System.Threading.Tasks.Task<int> ReadAsync(byte[] buffer, int offset, int count, System.Threading.CancellationToken cancellationToken) { }
411+
public override int ReadByte() { }
408412
[System.Runtime.Versioning.SupportedOSPlatform("windows")]
409413
public void SetAccessControl(object value) { }
410414
public override void SetLength(long value) { }

tests/TestableIO.System.IO.Abstractions.Api.Tests/Expected/TestableIO.System.IO.Abstractions.TestingHelpers_netstandard2.0.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,8 @@ namespace System.IO.Abstractions.TestingHelpers
316316
public object GetAccessControl() { }
317317
public object GetAccessControl(System.IO.Abstractions.IFileSystemAclSupport.AccessControlSections includeSections) { }
318318
public override int Read(byte[] buffer, int offset, int count) { }
319+
public override System.Threading.Tasks.Task<int> ReadAsync(byte[] buffer, int offset, int count, System.Threading.CancellationToken cancellationToken) { }
320+
public override int ReadByte() { }
319321
public void SetAccessControl(object value) { }
320322
public override void SetLength(long value) { }
321323
public override void Write(byte[] buffer, int offset, int count) { }

tests/TestableIO.System.IO.Abstractions.Api.Tests/Expected/TestableIO.System.IO.Abstractions.TestingHelpers_netstandard2.1.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,11 @@ namespace System.IO.Abstractions.TestingHelpers
341341
public override System.Threading.Tasks.Task FlushAsync(System.Threading.CancellationToken cancellationToken) { }
342342
public object GetAccessControl() { }
343343
public object GetAccessControl(System.IO.Abstractions.IFileSystemAclSupport.AccessControlSections includeSections) { }
344+
public override int Read(System.Span<byte> buffer) { }
344345
public override int Read(byte[] buffer, int offset, int count) { }
346+
public override System.Threading.Tasks.ValueTask<int> ReadAsync(System.Memory<byte> buffer, System.Threading.CancellationToken cancellationToken = default) { }
347+
public override System.Threading.Tasks.Task<int> ReadAsync(byte[] buffer, int offset, int count, System.Threading.CancellationToken cancellationToken) { }
348+
public override int ReadByte() { }
345349
public void SetAccessControl(object value) { }
346350
public override void SetLength(long value) { }
347351
public override void Write(System.ReadOnlySpan<byte> buffer) { }

tests/TestableIO.System.IO.Abstractions.TestingHelpers.Tests/MockFileStreamTests.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,39 @@
1010
[TestFixture]
1111
public class MockFileStreamTests
1212
{
13+
[Test]
14+
public async Task MockFileStream_SharedReadWrite_SecondHandleSeesWritesFromFirstHandle()
15+
{
16+
// Arrange
17+
var filename = XFS.Path(@"C:\temp\shared.bin");
18+
var fileSystem = new MockFileSystem();
19+
fileSystem.AddEmptyFile(filename);
20+
var results = new List<int>();
21+
22+
// Act
23+
using (var file1 = fileSystem.FileStream.New(filename, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite))
24+
using (var file2 = fileSystem.FileStream.New(filename, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite))
25+
{
26+
var buffer = new byte[4];
27+
for (int ix = 0; ix < 10; ix++)
28+
{
29+
var bytes = BitConverter.GetBytes(ix);
30+
file1.Position = 0;
31+
file1.Write(bytes, 0, bytes.Length);
32+
file1.Flush();
33+
34+
file2.Position = 0;
35+
file2.Flush();
36+
var bytesRead = file2.Read(buffer, 0, buffer.Length);
37+
await That(bytesRead).IsEqualTo(buffer.Length);
38+
results.Add(BitConverter.ToInt32(buffer, 0));
39+
}
40+
}
41+
42+
// Assert
43+
await That(results).IsEqualTo(new[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 });
44+
}
45+
1346
[Test]
1447
public async Task MockFileStream_Flush_WritesByteToFile()
1548
{

0 commit comments

Comments
 (0)