diff --git a/Source/Testably.Abstractions.Testing/FileSystem/FileStreamMock.cs b/Source/Testably.Abstractions.Testing/FileSystem/FileStreamMock.cs index 6b6ff8460..31d6f2874 100644 --- a/Source/Testably.Abstractions.Testing/FileSystem/FileStreamMock.cs +++ b/Source/Testably.Abstractions.Testing/FileSystem/FileStreamMock.cs @@ -177,6 +177,8 @@ public override int WriteTimeout private bool _isContentChanged; private bool _isDisposed; private readonly IStorageLocation _location; + private long _maxWrite; + private long _minWrite = long.MaxValue; private readonly FileMode _mode; private readonly FileOptions _options; private readonly MemoryStream _stream; @@ -248,7 +250,8 @@ private FileStreamMock(MemoryStream stream, throw ExceptionFactory.FileAlreadyExists( _fileSystem.Execute.Path.GetFullPath(base.Name), 17); - } else if (_mode.Equals(FileMode.CreateNew)) + } + else if (_mode.Equals(FileMode.CreateNew)) { throw ExceptionFactory.FileAlreadyExists( _fileSystem.Execute.Path.GetFullPath(Name), @@ -321,6 +324,8 @@ public override IAsyncResult BeginWrite(byte[] buffer, throw ExceptionFactory.StreamDoesNotSupportWriting(); } + _minWrite = Position; + _maxWrite = Position + count; return base.BeginWrite(buffer, offset, count, callback, state); } @@ -575,6 +580,8 @@ public override void Write(byte[] buffer, int offset, int count) } _isContentChanged = true; + _minWrite = Position; + _maxWrite = Position + count; base.Write(buffer, offset, count); } @@ -592,6 +599,8 @@ public override void Write(ReadOnlySpan buffer) } _isContentChanged = true; + _minWrite = Position; + _maxWrite = Position + buffer.Length; base.Write(buffer); } #endif @@ -610,6 +619,8 @@ public override async Task WriteAsync(byte[] buffer, int offset, int count, } _isContentChanged = true; + _minWrite = Position; + _maxWrite = Position + count; await base.WriteAsync(buffer, offset, count, cancellationToken); } @@ -628,6 +639,8 @@ public override async ValueTask WriteAsync(ReadOnlyMemory buffer, } _isContentChanged = true; + _minWrite = Position; + _maxWrite = Position + buffer.Length; await base.WriteAsync(buffer, cancellationToken); } #endif @@ -645,6 +658,8 @@ public override void WriteByte(byte value) } _isContentChanged = true; + _minWrite = Position; + _maxWrite = Position + 1L; base.WriteByte(value); } @@ -679,6 +694,8 @@ private void InitializeStream() else { _isContentChanged = true; + _minWrite = Position; + _maxWrite = Position; } } @@ -696,12 +713,20 @@ private void InternalFlush() _ = _stream.Read(data, 0, (int)Length); _stream.Seek(position, SeekOrigin.Begin); _container.WriteBytes(data); + _minWrite = long.MaxValue; + _maxWrite = 0; } private void OnBytesChanged(object? sender, EventArgs e) { byte[] existingContents = _container.GetBytes(); long position = _stream.Position; + if (_minWrite < _maxWrite) + { + _stream.Position = _minWrite; + _ = _stream.Read(existingContents, (int)_minWrite, (int)(_maxWrite - _minWrite)); + } + _stream.Position = 0; _stream.Write(existingContents, 0, existingContents.Length); _stream.Position = position; diff --git a/Tests/Testably.Abstractions.Tests/FileSystem/FileStream/ParallelTests.cs b/Tests/Testably.Abstractions.Tests/FileSystem/FileStream/ParallelTests.cs new file mode 100644 index 000000000..07891aade --- /dev/null +++ b/Tests/Testably.Abstractions.Tests/FileSystem/FileStream/ParallelTests.cs @@ -0,0 +1,126 @@ +using System.IO; +using System.Text; + +namespace Testably.Abstractions.Tests.FileSystem.FileStream; + +[FileSystemTests] +public partial class ParallelTests +{ + [Theory] + [AutoData] + public async Task MultipleFlush_DifferentLength_ShouldKeepAdditionalBytes(string path) + { + using (FileSystemStream stream1 = FileSystem.File.Open(path, FileMode.OpenOrCreate, + FileAccess.Write, FileShare.ReadWrite)) + { + using FileSystemStream stream2 = FileSystem.File.Open(path, FileMode.OpenOrCreate, + FileAccess.Write, FileShare.ReadWrite); + + stream2.Write(Encoding.UTF8.GetBytes("foo"), 0, 3); + stream1.Write(Encoding.UTF8.GetBytes("barfoo"), 0, 6); + + await That(stream1).HasLength().EqualTo(6); + await That(stream2).HasLength().EqualTo(3); + + stream1.Flush(); + + await That(stream1).HasLength().EqualTo(6); + await That(stream2).HasLength().EqualTo(6); + + stream2.Flush(); + + await That(stream1).HasLength().EqualTo(6); + await That(stream2).HasLength().EqualTo(6); + } + + await That(FileSystem.File.ReadAllText(path)).IsEqualTo("foofoo"); + } + + [Theory] + [AutoData] + public async Task MultipleFlush_DifferentPosition_ShouldKeepAdditionalBytes(string path) + { + FileSystem.File.WriteAllText(path, "AAAAAAAAAAAA"); + using (FileSystemStream stream1 = FileSystem.File.Open(path, FileMode.OpenOrCreate, + FileAccess.Write, FileShare.ReadWrite)) + { + using FileSystemStream stream2 = FileSystem.File.Open(path, FileMode.OpenOrCreate, + FileAccess.Write, FileShare.ReadWrite); + stream2.Position = 3; + stream1.Position = 2; + + stream2.Write(Encoding.UTF8.GetBytes("CCC"), 0, 3); + stream1.Write(Encoding.UTF8.GetBytes("bbbbbb"), 0, 6); + + stream1.Flush(); + stream2.Flush(); + } + + await That(FileSystem.File.ReadAllText(path)).IsEqualTo("AAbCCCbbAAAA"); + } + + [Theory] + [AutoData] + public async Task MultipleFlush_DifferentPositionWithGaps_ShouldKeepAdditionalBytes(string path) + { + FileSystem.File.WriteAllText(path, "AAAAAAAAAAAA"); + using (FileSystemStream stream1 = FileSystem.File.Open(path, FileMode.OpenOrCreate, + FileAccess.Write, FileShare.ReadWrite)) + { + using FileSystemStream stream2 = FileSystem.File.Open(path, FileMode.OpenOrCreate, + FileAccess.Write, FileShare.ReadWrite); + stream1.Position = 2; + + stream2.Position = 3; + stream2.Write(Encoding.UTF8.GetBytes("C"), 0, 1); + stream2.Position = 5; + stream2.Write(Encoding.UTF8.GetBytes("C"), 0, 1); + stream1.Write(Encoding.UTF8.GetBytes("bbbbbb"), 0, 6); + + stream1.Flush(); + stream2.Flush(); + } + + await That(FileSystem.File.ReadAllText(path)).IsEqualTo("AAbbbCbbAAAA"); + } + + [Theory] + [AutoData] + public async Task MultipleFlush_ShouldKeepLatestChanges(string path) + { + using (FileSystemStream stream1 = FileSystem.File.Open(path, FileMode.OpenOrCreate, + FileAccess.Write, FileShare.ReadWrite)) + { + using FileSystemStream stream2 = FileSystem.File.Open(path, FileMode.OpenOrCreate, + FileAccess.Write, FileShare.ReadWrite); + + stream2.Write(Encoding.UTF8.GetBytes("foo"), 0, 3); + stream1.Write(Encoding.UTF8.GetBytes("bar"), 0, 3); + + stream1.Flush(); + stream2.Flush(); + } + + await That(FileSystem.File.ReadAllText(path)).IsEqualTo("foo"); + } + + [Theory] + [AutoData] + public async Task WriteEmpty_ShouldNotOverwrite(string path) + { + using (FileSystemStream stream1 = FileSystem.File.Open(path, FileMode.OpenOrCreate, + FileAccess.Write, FileShare.ReadWrite)) + { + using FileSystemStream stream2 = FileSystem.File.Open(path, FileMode.OpenOrCreate, + FileAccess.Write, FileShare.ReadWrite); + + stream2.Write(Encoding.UTF8.GetBytes(""), 0, 0); + stream1.Write(Encoding.UTF8.GetBytes("barfoo"), 0, 6); + + stream1.Flush(); + stream2.Flush(); + } + + await That(FileSystem.File.ReadAllText(path)).IsEqualTo("barfoo"); + } +}