Skip to content

Commit 0f67cc9

Browse files
committed
fix: handle shared file contents across handles
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 0f67cc9

2 files changed

Lines changed: 121 additions & 4 deletions

File tree

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.TestingHelpers.Tests/MockFileStreamTests.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,38 @@
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+
file2.Read(buffer, 0, buffer.Length);
37+
results.Add(BitConverter.ToInt32(buffer, 0));
38+
}
39+
}
40+
41+
// Assert
42+
await That(results).IsEqualTo(new[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 });
43+
}
44+
1345
[Test]
1446
public async Task MockFileStream_Flush_WritesByteToFile()
1547
{

0 commit comments

Comments
 (0)