From fa66bc407c086aea3b6693535ce8abc078fdf2fd Mon Sep 17 00:00:00 2001 From: Jan Krivanek Date: Tue, 25 Mar 2025 17:22:50 +0100 Subject: [PATCH] Generated --- Packages.props | 3 + .../ChunkedBufferStreamTests.cs | 363 +++++++++--- .../CleanupScopeTests.cs | 86 +++ .../ConcatenatedReadStreamTests.cs | 383 ++++++++++--- .../DotUtils.StreamUtils.Tests.csproj | 1 + .../StreamExtensionsTests.cs | 369 ++++++++++-- .../SubStreamTests.cs | 354 ++++++++++-- .../TransparentReadStreamTests.cs | 528 ++++++++++++++---- 8 files changed, 1789 insertions(+), 298 deletions(-) create mode 100644 test/DotUtils.StreamUtils.Tests/CleanupScopeTests.cs diff --git a/Packages.props b/Packages.props index 0fb2970..01ab588 100644 --- a/Packages.props +++ b/Packages.props @@ -2,4 +2,7 @@ + + + \ No newline at end of file diff --git a/test/DotUtils.StreamUtils.Tests/ChunkedBufferStreamTests.cs b/test/DotUtils.StreamUtils.Tests/ChunkedBufferStreamTests.cs index 9962e91..5fa0bf3 100644 --- a/test/DotUtils.StreamUtils.Tests/ChunkedBufferStreamTests.cs +++ b/test/DotUtils.StreamUtils.Tests/ChunkedBufferStreamTests.cs @@ -1,83 +1,320 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; +using System.IO; +using System.Threading; using System.Threading.Tasks; -using FluentAssertions; -using static DotUtils.StreamUtils.Tests.StreamTestExtensions; +using DotUtils.StreamUtils; +using Xunit; -namespace DotUtils.StreamUtils.Tests +namespace DotUtils.StreamUtils.UnitTests { + /// + /// Unit tests for the class. + /// public class ChunkedBufferStreamTests { - [Theory] - [MemberData(nameof(StreamTestExtensions.EnumerateReadFunctionTypes), MemberType = typeof(StreamTestExtensions))] - public void Write_CusesChunking(StreamFunctionType streamFunctionType) + private const string NotSupportedMessage = "GreedyBufferedStream is write-only, append-only"; + + /// + /// A test stream derived from MemoryStream to verify that Close is called. + /// + private class TestMemoryStream : MemoryStream + { + public bool IsClosed { get; private set; } = false; + + public override void Close() + { + IsClosed = true; + base.Close(); + } + } + + /// + /// Tests that Flush writes any partial data in the buffer to the underlying stream. + /// + [Fact] + public void Flush_WithPartialBuffer_WritesBufferedDataToUnderlyingStream() + { + // Arrange + var underlyingStream = new MemoryStream(); + const int bufferSize = 4; + using var chunkedStream = new ChunkedBufferStream(underlyingStream, bufferSize); + byte[] data = { 1, 2 }; + chunkedStream.Write(data, 0, data.Length); + + // Act + chunkedStream.Flush(); + + // Assert + byte[] result = underlyingStream.ToArray(); + Assert.Equal(data, result); + } + + /// + /// Tests that Write(byte[], int, int) flushes automatically when the buffer becomes full and retains leftover data. + /// + [Fact] + public void Write_WithDataExceedingBuffer_FlushesAutomaticallyAndRetainsRemainingData() + { + // Arrange + var underlyingStream = new MemoryStream(); + const int bufferSize = 4; + using var chunkedStream = new ChunkedBufferStream(underlyingStream, bufferSize); + byte[] input = { 10, 20, 30, 40, 50, 60 }; // 6 bytes total + + // Act + chunkedStream.Write(input, 0, input.Length); + // At this point, first 4 bytes should have been automatically flushed. + // Manually flush to write the remaining 2 bytes. + chunkedStream.Flush(); + + // Assert + byte[] result = underlyingStream.ToArray(); + byte[] expected = { 10, 20, 30, 40, 50, 60 }; + Assert.Equal(expected, result); + } + + /// + /// Tests that WriteByte writes a byte and flushes automatically when the buffer becomes full. + /// + [Fact] + public void WriteByte_WhenBufferBecomesFull_FlushesAutomatically() + { + // Arrange + var underlyingStream = new MemoryStream(); + const int bufferSize = 3; + using var chunkedStream = new ChunkedBufferStream(underlyingStream, bufferSize); + byte[] input = { 1, 2, 3 }; + + // Act + // Write first two bytes; should not flush yet. + chunkedStream.WriteByte(input[0]); + chunkedStream.WriteByte(input[1]); + Assert.Equal(0, underlyingStream.Length); + + // Write third byte; this should cause the buffer to flush. + chunkedStream.WriteByte(input[2]); + + // Assert + byte[] result = underlyingStream.ToArray(); + Assert.Equal(input, result); + } + + /// + /// Tests that WriteAsync writes data asynchronously and flushes automatically when the buffer becomes full. + /// + [Fact] + public async Task WriteAsync_WithDataExceedingBuffer_FlushesAutomaticallyAndRetainsRemainingData() + { + // Arrange + var underlyingStream = new MemoryStream(); + const int bufferSize = 4; + using var chunkedStream = new ChunkedBufferStream(underlyingStream, bufferSize); + byte[] input = { 5, 15, 25, 35, 45, 55 }; + + // Act + await chunkedStream.WriteAsync(input, 0, input.Length, CancellationToken.None); + // Auto flush should have occurred once when buffer reached 4 and then remaining bytes were not flushed. + chunkedStream.Flush(); + + // Assert + byte[] result = underlyingStream.ToArray(); + byte[] expected = { 5, 15, 25, 35, 45, 55 }; + Assert.Equal(expected, result); + } + +#if NET + /// + /// Tests that Write(ReadOnlySpan) writes data and flushes automatically when the buffer becomes full. + /// + [Fact] + public void Write_ReadOnlySpan_WithDataExceedingBuffer_FlushesAutomaticallyAndRetainsRemainingData() + { + // Arrange + var underlyingStream = new MemoryStream(); + const int bufferSize = 5; + using var chunkedStream = new ChunkedBufferStream(underlyingStream, bufferSize); + ReadOnlySpan input = new byte[] { 100, 110, 120, 130, 140, 150, 160 }; + + // Act + chunkedStream.Write(input); + // After write, first 5 bytes should have flushed automatically. + chunkedStream.Flush(); + + // Assert + byte[] result = underlyingStream.ToArray(); + byte[] expected = { 100, 110, 120, 130, 140, 150, 160 }; + Assert.Equal(expected, result); + } + + /// + /// Tests that WriteAsync(ReadOnlyMemory) writes data asynchronously and flushes automatically when the buffer becomes full. + /// + [Fact] + public async Task WriteAsync_ReadOnlyMemory_WithDataExceedingBuffer_FlushesAutomaticallyAndRetainsRemainingData() { - int chunkSize = 3; - byte[] bytes = new byte[100]; - using MemoryStream ms = new(bytes); - using Stream stream = new ChunkedBufferStream(ms, chunkSize); + // Arrange + var underlyingStream = new MemoryStream(); + const int bufferSize = 3; + using var chunkedStream = new ChunkedBufferStream(underlyingStream, bufferSize); + ReadOnlyMemory input = new byte[] { 200, 210, 220, 230, 240 }; - WriteBytes writeBytes = stream.GetWriteFunc(streamFunctionType); + // Act + await chunkedStream.WriteAsync(input, CancellationToken.None); + // Auto flush would have occurred when first 3 bytes were written. + chunkedStream.Flush(); - writeBytes(new byte[]{1,2}); - bytes.Should().AllBeEquivalentTo(0); + // Assert + byte[] result = underlyingStream.ToArray(); + byte[] expected = { 200, 210, 220, 230, 240 }; + Assert.Equal(expected, result); + } +#endif + + /// + /// Tests that Read method always throws NotSupportedException. + /// + [Fact] + public void Read_AnyArguments_ThrowsNotSupportedException() + { + // Arrange + var underlyingStream = new MemoryStream(); + using var chunkedStream = new ChunkedBufferStream(underlyingStream, 4); + byte[] buffer = new byte[10]; + + // Act & Assert + var exception = Assert.Throws(() => chunkedStream.Read(buffer, 0, buffer.Length)); + Assert.Equal(NotSupportedMessage, exception.Message); + } + + /// + /// Tests that Seek method always throws NotSupportedException. + /// + [Fact] + public void Seek_AnyArguments_ThrowsNotSupportedException() + { + // Arrange + var underlyingStream = new MemoryStream(); + using var chunkedStream = new ChunkedBufferStream(underlyingStream, 4); + + // Act & Assert + var exception = Assert.Throws(() => chunkedStream.Seek(0, SeekOrigin.Begin)); + Assert.Equal(NotSupportedMessage, exception.Message); + } + + /// + /// Tests that SetLength method always throws NotSupportedException. + /// + [Fact] + public void SetLength_AnyArgument_ThrowsNotSupportedException() + { + // Arrange + var underlyingStream = new MemoryStream(); + using var chunkedStream = new ChunkedBufferStream(underlyingStream, 4); + + // Act & Assert + var exception = Assert.Throws(() => chunkedStream.SetLength(100)); + Assert.Equal(NotSupportedMessage, exception.Message); + } + + /// + /// Tests that setting Position property throws NotSupportedException. + /// + [Fact] + public void SetPosition_PropertySetter_ThrowsNotSupportedException() + { + // Arrange + var underlyingStream = new MemoryStream(); + using var chunkedStream = new ChunkedBufferStream(underlyingStream, 4); + + // Act & Assert + var exception = Assert.Throws(() => chunkedStream.Position = 10); + Assert.Equal(NotSupportedMessage, exception.Message); + } + + /// + /// Tests that the CanRead, CanSeek, and CanWrite properties return the expected values. + /// + [Fact] + public void Properties_CanReadCanSeekAndCanWrite_ReturnExpectedValues() + { + // Arrange + var underlyingStream = new MemoryStream(); + const int bufferSize = 4; + using var chunkedStream = new ChunkedBufferStream(underlyingStream, bufferSize); + + // Act & Assert + Assert.False(chunkedStream.CanRead); + Assert.False(chunkedStream.CanSeek); + Assert.True(chunkedStream.CanWrite); + } + + /// + /// Tests that Length property returns the sum of underlying stream length and buffered data count. + /// + [Fact] + public void Length_Property_ReturnsUnderlyingLengthPlusBufferedDataCount() + { + // Arrange + var underlyingStream = new MemoryStream(); + const int bufferSize = 4; + using var chunkedStream = new ChunkedBufferStream(underlyingStream, bufferSize); + // Write less than bufferSize so that data stays in the buffer. + byte[] data = { 1, 2 }; + chunkedStream.Write(data, 0, data.Length); - writeBytes(new byte[] { 3, 4 }); - bytes.Take(3).Should().BeEquivalentTo(new byte[] { 1, 2, 3 }); - bytes.Skip(3).Should().AllBeEquivalentTo(0); + // Underlying stream length is still zero, buffered data count is 2. + long expectedLength = underlyingStream.Length + 2; - writeBytes(new byte[] { 5, 6 }); - bytes.Take(6).Should().BeEquivalentTo(new byte[] { 1, 2, 3, 4, 5, 6 }); - bytes.Skip(6).Should().AllBeEquivalentTo(0); + // Act + long actualLength = chunkedStream.Length; - writeBytes(new byte[] { 7, 8 }); - bytes.Take(6).Should().BeEquivalentTo(new byte[] { 1, 2, 3, 4, 5, 6 }); - bytes.Skip(6).Should().AllBeEquivalentTo(0); + // Assert + Assert.Equal(expectedLength, actualLength); + } - writeBytes(new byte[] { 9, 10, 11, 12, 13, 14, 15, 16 }); - bytes.Take(15).Should().BeEquivalentTo(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }); - bytes.Skip(15).Should().AllBeEquivalentTo(0); + /// + /// Tests that Position getter returns the sum of underlying stream position and buffered data count. + /// + [Fact] + public void GetPosition_Property_ReturnsUnderlyingPositionPlusBufferedDataCount() + { + // Arrange + var underlyingStream = new MemoryStream(); + const int bufferSize = 4; + using var chunkedStream = new ChunkedBufferStream(underlyingStream, bufferSize); + // Write 3 bytes; these remain in the buffer until flush. + byte[] data = { 10, 20, 30 }; + chunkedStream.Write(data, 0, data.Length); - stream.Flush(); - bytes.Take(16).Should().BeEquivalentTo(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 }); - bytes.Skip(16).Should().AllBeEquivalentTo(0); + // Underlying stream position is 0, buffered data count is 3. + long expectedPosition = underlyingStream.Position + 3; + + // Act + long actualPosition = chunkedStream.Position; + + // Assert + Assert.Equal(expectedPosition, actualPosition); } + /// + /// Tests that Close flushes any remaining buffer to underlying stream and closes the underlying stream. + /// [Fact] - public void WriteByte_CusesChunking() + public void Close_CallsFlushAndClosesUnderlyingStream() { - int chunkSize = 3; - var bytes = new byte[100]; - using MemoryStream ms = new(bytes); - using Stream stream = new ChunkedBufferStream(ms, chunkSize); - - stream.WriteByte(1); - bytes.Should().AllBeEquivalentTo(0); - stream.WriteByte(2); - bytes.Should().AllBeEquivalentTo(0); - - stream.WriteByte(3); - bytes.Take(3).Should().BeEquivalentTo(new byte[] { 1, 2, 3 }); - bytes.Skip(3).Should().AllBeEquivalentTo(0); - - stream.WriteByte(4); - stream.WriteByte(5); - bytes.Take(3).Should().BeEquivalentTo(new byte[] { 1, 2, 3 }); - bytes.Skip(3).Should().AllBeEquivalentTo(0); - - stream.WriteByte(6); - bytes.Take(6).Should().BeEquivalentTo(new byte[] { 1, 2, 3, 4, 5, 6 }); - bytes.Skip(6).Should().AllBeEquivalentTo(0); - - stream.WriteByte(7); - bytes.Take(6).Should().BeEquivalentTo(new byte[] { 1, 2, 3, 4, 5, 6 }); - bytes.Skip(6).Should().AllBeEquivalentTo(0); - - stream.Flush(); - bytes.Take(7).Should().BeEquivalentTo(new byte[] { 1, 2, 3, 4, 5, 6, 7 }); - bytes.Skip(7).Should().AllBeEquivalentTo(0); + // Arrange + var underlyingStream = new TestMemoryStream(); + const int bufferSize = 4; + var chunkedStream = new ChunkedBufferStream(underlyingStream, bufferSize); + byte[] data = { 7, 8 }; + chunkedStream.Write(data, 0, data.Length); + + // Act + chunkedStream.Close(); + + // Assert + byte[] result = underlyingStream.ToArray(); + Assert.Equal(data, result); + Assert.True(underlyingStream.IsClosed); } } } diff --git a/test/DotUtils.StreamUtils.Tests/CleanupScopeTests.cs b/test/DotUtils.StreamUtils.Tests/CleanupScopeTests.cs new file mode 100644 index 0000000..95062d2 --- /dev/null +++ b/test/DotUtils.StreamUtils.Tests/CleanupScopeTests.cs @@ -0,0 +1,86 @@ +using DotUtils.StreamUtils; +using Xunit; + +namespace DotUtils.StreamUtils.UnitTests +{ + /// + /// Unit tests for the struct. + /// + public class CleanupScopeTests + { + /// + /// Tests that the Dispose method calls the provided action exactly once. + /// Arrange: Initialize a counter and a delegate that increments the counter. + /// Act: Invoke Dispose on a CleanupScope instance created with the delegate. + /// Assert: Verify that the counter is incremented exactly once. + /// + [Fact] + public void Dispose_WhenCalled_InvokesActionOnce() + { + // Arrange + int callCount = 0; + Action action = () => callCount++; + var cleanupScope = new CleanupScope(action); + + // Act + cleanupScope.Dispose(); + + // Assert + Assert.Equal(1, callCount); + } + + /// + /// Tests that the Dispose method propagates exceptions thrown by the provided action. + /// Arrange: Create a delegate that throws an InvalidOperationException. + /// Act & Assert: Ensure that calling Dispose results in the same exception. + /// + [Fact] + public void Dispose_WhenActionThrowsException_PropagatesException() + { + // Arrange + Action action = () => throw new InvalidOperationException("Test exception"); + var cleanupScope = new CleanupScope(action); + + // Act & Assert + var exception = Assert.Throws(() => cleanupScope.Dispose()); + Assert.Equal("Test exception", exception.Message); + } + + /// + /// Tests that creating a CleanupScope with a null action results in a NullReferenceException upon disposal. + /// Arrange: Create a CleanupScope instance with a null action. + /// Act & Assert: Expect a NullReferenceException when Dispose is called. + /// + [Fact] + public void Dispose_WhenCreatedWithNullAction_ThrowsNullReferenceException() + { + // Arrange + var cleanupScope = new CleanupScope(null); + + // Act & Assert + Assert.Throws(() => cleanupScope.Dispose()); + } + + /// + /// Tests that calling Dispose twice results in the action being invoked twice. + /// Arrange: Initialize a counter and a delegate that increments the counter. + /// Act: Call Dispose twice. + /// Assert: Verify that the counter equals two. + /// + [Fact] + public void Dispose_CalledTwice_InvokesActionTwice() + { + // Arrange + int callCount = 0; + Action action = () => callCount++; + var cleanupScope = new CleanupScope(action); + + // Act + cleanupScope.Dispose(); + cleanupScope.Dispose(); + + // Assert + Assert.Equal(2, callCount); + } + } +} diff --git a/test/DotUtils.StreamUtils.Tests/ConcatenatedReadStreamTests.cs b/test/DotUtils.StreamUtils.Tests/ConcatenatedReadStreamTests.cs index 8198abd..cb8aefe 100644 --- a/test/DotUtils.StreamUtils.Tests/ConcatenatedReadStreamTests.cs +++ b/test/DotUtils.StreamUtils.Tests/ConcatenatedReadStreamTests.cs @@ -1,109 +1,356 @@ -using System; -using System.Collections.Generic; +using System; +using System.Buffers; +using System.IO; using System.Linq; using System.Text; +using System.Threading; using System.Threading.Tasks; -using FluentAssertions; -using static DotUtils.StreamUtils.Tests.StreamTestExtensions; +using Xunit; -namespace DotUtils.StreamUtils.Tests +namespace DotUtils.StreamUtils.UnitTests { + /// + /// Unit tests for the class. + /// public class ConcatenatedReadStreamTests { + private readonly Encoding _encoding = Encoding.UTF8; + + /// + /// A helper stream that is non-readable. + /// + private class NonReadableStream : Stream + { + public override bool CanRead => false; + public override bool CanSeek => throw new NotImplementedException(); + public override bool CanWrite => throw new NotImplementedException(); + public override long Length => throw new NotImplementedException(); + public override long Position { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + public override void Flush() => throw new NotImplementedException(); + public override int Read(byte[] buffer, int offset, int count) => throw new NotImplementedException(); + public override long Seek(long offset, SeekOrigin origin) => throw new NotImplementedException(); + public override void SetLength(long value) => throw new NotImplementedException(); + public override void Write(byte[] buffer, int offset, int count) => throw new NotImplementedException(); + } + + /// + /// A helper MemoryStream that tracks whether it has been disposed. + /// + private class TestMemoryStream : MemoryStream + { + public bool IsDisposed { get; private set; } = false; + public TestMemoryStream(byte[] buffer) : base(buffer) { } + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + IsDisposed = true; + } +#if NET + public override async ValueTask DisposeAsync() + { + await base.DisposeAsync(); + IsDisposed = true; + } +#endif + } + + /// + /// Tests that the constructor throws an ArgumentException when provided a non-readable stream. + /// [Fact] - public void ReadByte_ReadsStreamSequentialy() + public void Constructor_WithNonReadableStream_ThrowsArgumentException() { - using MemoryStream ms1 = new(new byte[]{1, 2, 3}); - using MemoryStream ms2 = new(new byte[] { 4 }); - using MemoryStream ms3 = new(new byte[] { 5, 6 }); + // Arrange + Stream nonReadable = new NonReadableStream(); - Stream stream = new ConcatenatedReadStream(ms1, ms2, ms3); + // Act & Assert + Assert.Throws(() => new ConcatenatedReadStream(nonReadable)); + } - stream.ReadByte().Should().Be(1); - stream.ReadByte().Should().Be(2); - stream.ReadByte().Should().Be(3); - stream.ReadByte().Should().Be(4); - stream.ReadByte().Should().Be(5); - stream.ReadByte().Should().Be(6); + /// + /// Tests that the constructor correctly flattens nested ConcatenatedReadStreams. + /// + [Fact] + public void Constructor_WithNestedConcatenatedReadStream_MergesSubStreams() + { + // Arrange + byte[] bytes1 = _encoding.GetBytes("Hello"); + byte[] bytes2 = _encoding.GetBytes("World"); + using var ms1 = new MemoryStream(bytes1); + using var ms2 = new MemoryStream(bytes2); + using var innerConcat = new ConcatenatedReadStream(ms1, ms2); + + // Act + using var outerConcat = new ConcatenatedReadStream(innerConcat); + + byte[] buffer = new byte[20]; + int readCount = outerConcat.Read(buffer, 0, buffer.Length); + string result = _encoding.GetString(buffer, 0, readCount); - // cannot read anymore - stream.ReadByte().Should().Be(-1); + // Assert + Assert.Equal("HelloWorld", result); } - [Theory] - [MemberData(nameof(StreamTestExtensions.EnumerateReadFunctionTypes), MemberType = typeof(StreamTestExtensions))] - public void Read_ReadsStreamSequentialy(StreamFunctionType streamFunctionType) + /// + /// Tests that Flush always throws NotSupportedException. + /// + [Fact] + public void Flush_Always_ThrowsNotSupportedException() { - using MemoryStream ms1 = new(new byte[] { 1, 2, 3 }); - using MemoryStream ms2 = new(new byte[] { 4 }); - using MemoryStream ms3 = new(new byte[] { 5, 6 }); - using MemoryStream ms4 = new(new byte[] { 7, 8, 9 }); + // Arrange + using var ms = new MemoryStream(_encoding.GetBytes("data")); + using var concatenatedReadStream = new ConcatenatedReadStream(ms); + + // Act & Assert + Assert.Throws(() => concatenatedReadStream.Flush()); + } - Stream stream = new ConcatenatedReadStream(ms1, ms2, ms3, ms4); + /// + /// Tests that Read reads data from multiple underlying streams and disposes each stream when exhausted. + /// + [Fact] + public void Read_ReadsDataFromMultipleStreams() + { + // Arrange + byte[] bytes1 = _encoding.GetBytes("Hello"); + byte[] bytes2 = _encoding.GetBytes("World"); + var testStream1 = new TestMemoryStream(bytes1); + var testStream2 = new TestMemoryStream(bytes2); - ReadBytes readBytes = stream.GetReadFunc(streamFunctionType); + using var concatenatedReadStream = new ConcatenatedReadStream(testStream1, testStream2); + byte[] buffer = new byte[20]; - var readBuffer = new byte[2]; + // Act + int totalRead = concatenatedReadStream.Read(buffer, 0, buffer.Length); + string result = _encoding.GetString(buffer, 0, totalRead); - readBytes(readBuffer).Should().Be(2); - readBuffer.Should().BeEquivalentTo(new byte[] { 1, 2 }); + // Assert + Assert.Equal("HelloWorld", result); + Assert.Equal(bytes1.Length + bytes2.Length, totalRead); + Assert.True(testStream1.IsDisposed); + Assert.True(testStream2.IsDisposed); + Assert.Equal(totalRead, concatenatedReadStream.Position); + } - readBytes(readBuffer).Should().Be(2); - readBuffer.Should().BeEquivalentTo(new byte[] { 3, 4 }); + /// + /// Tests that Read returns zero when no more data is available. + /// + [Fact] + public void Read_WhenNoDataLeft_ReturnsZero() + { + // Arrange + byte[] bytes = _encoding.GetBytes("Test"); + using var ms = new MemoryStream(bytes); + using var concatenatedReadStream = new ConcatenatedReadStream(ms); + byte[] buffer = new byte[10]; + // Read all data + concatenatedReadStream.Read(buffer, 0, buffer.Length); - readBytes(readBuffer).Should().Be(2); - readBuffer.Should().BeEquivalentTo(new byte[] { 5, 6 }); + // Act + int bytesReadAfterExhaust = concatenatedReadStream.Read(buffer, 0, buffer.Length); - readBytes(readBuffer).Should().Be(2); - readBuffer.Should().BeEquivalentTo(new byte[] { 7, 8 }); + // Assert + Assert.Equal(0, bytesReadAfterExhaust); + } - // zero out for assertion clarity. - Array.Clear(readBuffer); + /// + /// Tests that ReadByte reads data one byte at a time from multiple streams. + /// + [Fact] + public void ReadByte_ReadsDataFromMultipleStreams() + { + // Arrange + byte[] bytes1 = _encoding.GetBytes("AB"); + byte[] bytes2 = _encoding.GetBytes("CD"); + var testStream1 = new TestMemoryStream(bytes1); + var testStream2 = new TestMemoryStream(bytes2); - readBytes(readBuffer).Should().Be(1); - readBuffer.Should().BeEquivalentTo(new byte[] { 9, 0 }); + using var concatenatedReadStream = new ConcatenatedReadStream(testStream1, testStream2); - // zero out for assertion clarity. - Array.Clear(readBuffer); + // Act + var sb = new StringBuilder(); + int b; + while ((b = concatenatedReadStream.ReadByte()) != -1) + { + sb.Append((char)b); + } + string result = sb.ToString(); - // cannot read anymore - readBytes(readBuffer).Should().Be(0); - readBuffer.Should().BeEquivalentTo(new byte[] { 0, 0 }); + // Assert + Assert.Equal("ABCD", result); + Assert.True(testStream1.IsDisposed); + Assert.True(testStream2.IsDisposed); + Assert.Equal(4, concatenatedReadStream.Position); } - [Theory] - [MemberData(nameof(StreamTestExtensions.EnumerateReadFunctionTypes), MemberType = typeof(StreamTestExtensions))] - public void Read_ReadsStreamSequentialy_UsingMultipleSubstreams(StreamFunctionType streamFunctionType) + /// + /// Tests that ReadAsync reads data from multiple streams and updates the position accordingly. + /// + [Fact] + public async Task ReadAsync_ReadsDataFromMultipleStreams() { - using MemoryStream ms1 = new(new byte[] { 1, 2, 3 }); - using MemoryStream ms2 = new(new byte[] { 4 }); - using MemoryStream ms3 = new(new byte[] { 5, 6 }); - using MemoryStream ms4 = new(new byte[] { 7, 8, 9, 10, 11 }); + // Arrange + byte[] bytes1 = _encoding.GetBytes("Async"); + byte[] bytes2 = _encoding.GetBytes("Test"); + var testStream1 = new TestMemoryStream(bytes1); + var testStream2 = new TestMemoryStream(bytes2); - Stream stream = new ConcatenatedReadStream(ms1, ms2, ms3, ms4); + using var concatenatedReadStream = new ConcatenatedReadStream(testStream1, testStream2); + byte[] buffer = new byte[20]; - ReadBytes readBytes = stream.GetReadFunc(streamFunctionType); + // Act + int totalRead = await concatenatedReadStream.ReadAsync(buffer, 0, buffer.Length, CancellationToken.None); + string result = _encoding.GetString(buffer, 0, totalRead); - var readBuffer = new byte[5]; + // Assert + Assert.Equal("AsyncTest", result); + Assert.Equal(bytes1.Length + bytes2.Length, totalRead); + Assert.True(testStream1.IsDisposed); + Assert.True(testStream2.IsDisposed); + Assert.Equal(totalRead, concatenatedReadStream.Position); + } - readBytes(readBuffer).Should().Be(5); - readBuffer.Should().BeEquivalentTo(new byte[] { 1, 2, 3, 4, 5 }); +#if NET + /// + /// Tests that the Read(Span) overload correctly reads data from multiple streams. + /// + [Fact] + public void ReadSpan_ReadsDataFromMultipleStreams() + { + // Arrange + byte[] bytes1 = _encoding.GetBytes("Span"); + byte[] bytes2 = _encoding.GetBytes("Test"); + var testStream1 = new TestMemoryStream(bytes1); + var testStream2 = new TestMemoryStream(bytes2); - readBytes(readBuffer).Should().Be(5); - readBuffer.Should().BeEquivalentTo(new byte[] { 6, 7, 8, 9, 10 }); + using var concatenatedReadStream = new ConcatenatedReadStream(testStream1, testStream2); + byte[] bufferArray = new byte[20]; + var buffer = new Span(bufferArray); + + // Act + int totalRead = concatenatedReadStream.Read(buffer); + string result = _encoding.GetString(bufferArray, 0, totalRead); + + // Assert + Assert.Equal("SpanTest", result); + Assert.Equal(bytes1.Length + bytes2.Length, totalRead); + Assert.True(testStream1.IsDisposed); + Assert.True(testStream2.IsDisposed); + Assert.Equal(totalRead, concatenatedReadStream.Position); + } - // zero out for assertion clarity. - Array.Clear(readBuffer); + /// + /// Tests that the ReadAsync(Memory) overload correctly reads data from multiple streams. + /// + [Fact] + public async Task ReadAsyncMemory_ReadsDataFromMultipleStreams() + { + // Arrange + byte[] bytes1 = _encoding.GetBytes("Memory"); + byte[] bytes2 = _encoding.GetBytes("Async"); + var testStream1 = new TestMemoryStream(bytes1); + var testStream2 = new TestMemoryStream(bytes2); + + using var concatenatedReadStream = new ConcatenatedReadStream(testStream1, testStream2); + byte[] bufferArray = new byte[20]; + Memory buffer = new Memory(bufferArray); + + // Act + int totalRead = await concatenatedReadStream.ReadAsync(buffer); + string result = _encoding.GetString(bufferArray, 0, totalRead); + + // Assert + Assert.Equal("MemoryAsync", result); + Assert.Equal(bytes1.Length + bytes2.Length, totalRead); + Assert.True(testStream1.IsDisposed); + Assert.True(testStream2.IsDisposed); + Assert.Equal(totalRead, concatenatedReadStream.Position); + } +#endif + + /// + /// Tests that Seek throws NotSupportedException. + /// + [Fact] + public void Seek_Always_ThrowsNotSupportedException() + { + // Arrange + using var ms = new MemoryStream(_encoding.GetBytes("data")); + using var concatenatedReadStream = new ConcatenatedReadStream(ms); + + // Act & Assert + Assert.Throws(() => concatenatedReadStream.Seek(0, SeekOrigin.Begin)); + } + + /// + /// Tests that SetLength throws NotSupportedException. + /// + [Fact] + public void SetLength_Always_ThrowsNotSupportedException() + { + // Arrange + using var ms = new MemoryStream(_encoding.GetBytes("data")); + using var concatenatedReadStream = new ConcatenatedReadStream(ms); + + // Act & Assert + Assert.Throws(() => concatenatedReadStream.SetLength(100)); + } + + /// + /// Tests that Write throws NotSupportedException. + /// + [Fact] + public void Write_Always_ThrowsNotSupportedException() + { + // Arrange + byte[] buffer = _encoding.GetBytes("data"); + using var ms = new MemoryStream(_encoding.GetBytes("data")); + using var concatenatedReadStream = new ConcatenatedReadStream(ms); + + // Act & Assert + Assert.Throws(() => concatenatedReadStream.Write(buffer, 0, buffer.Length)); + } + + /// + /// Tests that setting the Position property throws NotSupportedException. + /// + [Fact] + public void SetPosition_ThrowsNotSupportedException() + { + // Arrange + using var ms = new MemoryStream(_encoding.GetBytes("data")); + using var concatenatedReadStream = new ConcatenatedReadStream(ms); + + // Act & Assert + Assert.Throws(() => concatenatedReadStream.Position = 10); + } + + /// + /// Tests that the property values for CanRead, CanSeek, CanWrite and Length are correct. + /// + [Fact] + public void Properties_ReturnExpectedValues() + { + // Arrange + byte[] bytes1 = _encoding.GetBytes("12345"); + byte[] bytes2 = _encoding.GetBytes("67890"); + using var ms1 = new MemoryStream(bytes1); + using var ms2 = new MemoryStream(bytes2); - readBytes(readBuffer).Should().Be(1); - readBuffer.Should().BeEquivalentTo(new byte[] { 11, 0, 0, 0, 0 }); + using var concatenatedReadStream = new ConcatenatedReadStream(ms1, ms2); - // zero out for assertion clarity. - Array.Clear(readBuffer); + // Act + bool canRead = concatenatedReadStream.CanRead; + bool canSeek = concatenatedReadStream.CanSeek; + bool canWrite = concatenatedReadStream.CanWrite; + long expectedLength = ms1.Length + ms2.Length; + long actualLength = concatenatedReadStream.Length; - // cannot read anymore - readBytes(readBuffer).Should().Be(0); - readBuffer.Should().BeEquivalentTo(new byte[] { 0, 0, 0, 0, 0 }); + // Assert + Assert.True(canRead); + Assert.False(canSeek); + Assert.False(canWrite); + Assert.Equal(expectedLength, actualLength); } } } diff --git a/test/DotUtils.StreamUtils.Tests/DotUtils.StreamUtils.Tests.csproj b/test/DotUtils.StreamUtils.Tests/DotUtils.StreamUtils.Tests.csproj index fae1f7b..d851e51 100644 --- a/test/DotUtils.StreamUtils.Tests/DotUtils.StreamUtils.Tests.csproj +++ b/test/DotUtils.StreamUtils.Tests/DotUtils.StreamUtils.Tests.csproj @@ -12,6 +12,7 @@ + all runtime; build; native; contentfiles; analyzers diff --git a/test/DotUtils.StreamUtils.Tests/StreamExtensionsTests.cs b/test/DotUtils.StreamUtils.Tests/StreamExtensionsTests.cs index bf917e7..befb72a 100644 --- a/test/DotUtils.StreamUtils.Tests/StreamExtensionsTests.cs +++ b/test/DotUtils.StreamUtils.Tests/StreamExtensionsTests.cs @@ -1,50 +1,349 @@ -using System.IO.Compression; -using FluentAssertions; +using Moq; +using System; +using System.IO; +using System.Text; +using Xunit; -namespace DotUtils.StreamUtils.Tests; - -public class StreamExtensionsTests +namespace DotUtils.StreamUtils.UnitTests { - [Fact] - public void ReadAtLeast_ThrowsOnEndOfStream() + /// + /// Unit tests for the class. + /// + public class StreamExtensionsTests { - var stream = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); - var buffer = new byte[10]; + /// + /// Tests that ReadAtLeast reads at least the specified minimum bytes in a happy path scenario. + /// + [Fact] + public void ReadAtLeast_WhenEnoughDataAvailable_ReturnsMinimumBytesRead() + { + // Arrange + byte[] data = Encoding.ASCII.GetBytes("HelloWorld"); + using var stream = new MemoryStream(data); + byte[] buffer = new byte[20]; + int offset = 5; + int minimumBytes = 10; + bool throwOnEndOfStream = true; - Assert.Throws(() => stream.ReadAtLeast(buffer, 0, 10, throwOnEndOfStream: true)); - } + // Act + int bytesRead = stream.ReadAtLeast(buffer, offset, minimumBytes, throwOnEndOfStream); - [Fact] - public void ReadToEnd_OnSeekableStream() - { - using MemoryStream ms1 = new(new byte[] { 1, 2, 3, 4, 5 }); + // Assert + Assert.Equal(minimumBytes, bytesRead); + // Validate that expected bytes are read at the correct offset + for (int i = 0; i < minimumBytes; i++) + { + Assert.Equal(data[i], buffer[offset + i]); + } + } - ms1.ReadByte(); - ms1.ReadByte(); + /// + /// Tests that ReadAtLeast returns partial bytes read when throwOnEndOfStream is false and end-of-stream is reached. + /// + [Fact] + public void ReadAtLeast_WhenNotEnoughDataAvailableAndNoThrow_ReturnsPartialBytesRead() + { + // Arrange + byte[] data = Encoding.ASCII.GetBytes("Short"); + using var stream = new MemoryStream(data); + byte[] buffer = new byte[10]; + int offset = 2; + int minimumBytes = 7; // more than available data + bool throwOnEndOfStream = false; - ms1.ReadToEnd().Should().BeEquivalentTo(new[] { 3, 4, 5 }); + // Act + int bytesRead = stream.ReadAtLeast(buffer, offset, minimumBytes, throwOnEndOfStream); - // cannot read anymore - ms1.ReadByte().Should().Be(-1); - } + // Assert + Assert.Equal(data.Length, bytesRead); + for (int i = 0; i < data.Length; i++) + { + Assert.Equal(data[i], buffer[offset + i]); + } + } - [Fact] - public void ReadToEnd_OnNonseekableStream() - { - using MemoryStream ms1 = new(); - GZipStream zipStream = new GZipStream(ms1, CompressionMode.Compress); - zipStream.Write(new byte[] { 1, 2, 3, 4, 5 }); - zipStream.Flush(); + /// + /// Tests that ReadAtLeast throws InvalidDataException when throwOnEndOfStream is true and stream ends prematurely. + /// + [Fact] + public void ReadAtLeast_WhenNotEnoughDataAvailableAndThrow_ThrowsInvalidDataException() + { + // Arrange + byte[] data = Encoding.ASCII.GetBytes("Data"); + using var stream = new MemoryStream(data); + byte[] buffer = new byte[10]; + int offset = 0; + int minimumBytes = 6; // more than available + bool throwOnEndOfStream = true; + + // Act & Assert + Assert.Throws(() => + { + stream.ReadAtLeast(buffer, offset, minimumBytes, throwOnEndOfStream); + }); + } + + /// + /// Tests that SkipBytes (overload without parameters) skips all bytes in the stream. + /// + [Fact] + public void SkipBytes_NoParameter_SkipsEntireStream() + { + // Arrange + byte[] data = new byte[100]; + using var stream = new MemoryStream(data); + long originalLength = stream.Length; + + // Act + long skipped = stream.SkipBytes(); + + // Assert + Assert.Equal(originalLength, skipped); + Assert.Equal(stream.Length, stream.Position); + } + + /// + /// Tests that SkipBytes with bytesCount parameter skips the correct number of bytes. + /// + [Fact] + public void SkipBytes_WithBytesCount_SkipsSpecifiedNumberOfBytes() + { + // Arrange + byte[] data = new byte[5000]; + using var stream = new MemoryStream(data); + int skipBytes = 3000; + long initialPosition = stream.Position; + + // Act + int skipped = stream.SkipBytes(skipBytes, throwOnEndOfStream: false); + + // Assert + Assert.Equal(skipBytes, skipped); + Assert.Equal(initialPosition + skipBytes, stream.Position); + } + + /// + /// Tests that SkipBytes with throwOnEndOfStream throws InvalidDataException when there is insufficient data. + /// + [Fact] + public void SkipBytes_WithBytesCountAndThrow_WhenInsufficientData_ThrowsInvalidDataException() + { + // Arrange + byte[] data = new byte[100]; + using var stream = new MemoryStream(data); + int skipBytes = 200; // more than available + + // Act & Assert + Assert.Throws(() => + { + stream.SkipBytes(skipBytes, throwOnEndOfStream: true); + }); + } + + /// + /// Tests that SkipBytes with provided buffer overload skips the correct number of bytes. + /// + [Fact] + public void SkipBytes_WithBuffer_SkipsSpecifiedNumberOfBytes() + { + // Arrange + byte[] data = new byte[4096 * 3]; + using var stream = new MemoryStream(data); + int skipBytes = 4096 * 2; + byte[] customBuffer = new byte[4096]; + + // Act + int skipped = stream.SkipBytes(skipBytes, throwOnEndOfStream: false, buffer: customBuffer); + + // Assert + Assert.Equal(skipBytes, skipped); + Assert.Equal(skipBytes, stream.Position); + } + + /// + /// Tests that SkipBytes throws ArgumentOutOfRangeException when negative bytesCount is provided. + /// + [Theory] + [InlineData(-1)] + public void SkipBytes_NegativeBytesCount_ThrowsArgumentOutOfRangeException(long bytesCount) + { + // Arrange + byte[] data = new byte[50]; + using var stream = new MemoryStream(data); + + // Act & Assert + Assert.Throws(() => + { + stream.SkipBytes(bytesCount, throwOnEndOfStream: false); + }); + } + + /// + /// Tests that SkipBytes throws ArgumentOutOfRangeException when bytesCount exceeds int.MaxValue. + /// + [Fact] + public void SkipBytes_BytesCountExceedingIntMaxValue_ThrowsArgumentOutOfRangeException() + { + // Arrange + byte[] data = new byte[100]; + using var stream = new MemoryStream(data); + long tooManyBytes = (long)int.MaxValue + 1; - ms1.Position = 0; - using GZipStream unzipStream = new GZipStream(ms1, CompressionMode.Decompress); + // Act & Assert + Assert.Throws(() => + { + stream.SkipBytes(tooManyBytes, throwOnEndOfStream: false); + }); + } - unzipStream.ReadByte(); - unzipStream.ReadByte(); + /// + /// Tests that ReadToEnd returns the full content of the stream. + /// + [Fact] + public void ReadToEnd_WhenCalled_ReturnsAllData() + { + // Arrange + byte[] data = Encoding.ASCII.GetBytes("CompleteDataSet"); + using var stream = new MemoryStream(data); + + // Act + byte[] result = stream.ReadToEnd(); + + // Assert + Assert.Equal(data, result); + } + + /// + /// Tests that TryGetLength returns true and outputs correct length for a seekable stream. + /// + [Fact] + public void TryGetLength_SeekableStream_ReturnsTrueAndCorrectLength() + { + // Arrange + byte[] data = new byte[256]; + using var stream = new MemoryStream(data); + + // Act + bool result = stream.TryGetLength(out long length); + + // Assert + Assert.True(result); + Assert.Equal(data.Length, length); + } + + /// + /// Tests that TryGetLength returns false and outputs 0 for a non-seekable stream. + /// + [Fact] + public void TryGetLength_NonSeekableStream_ReturnsFalseAndLengthZero() + { + // Arrange + using var stream = new NonSeekableStream(new MemoryStream(Encoding.ASCII.GetBytes("NonSeekable"))); + + // Act + bool result = stream.TryGetLength(out long length); + + // Assert + Assert.False(result); + Assert.Equal(0, length); + } + + /// + /// Tests that ToReadableSeekableStream returns a seekable and readable stream. + /// + [Fact] + public void ToReadableSeekableStream_WhenCalled_ReturnsSeekableReadableStream() + { + // Arrange + byte[] data = Encoding.ASCII.GetBytes("TestData"); + using var originalStream = new MemoryStream(data); + + // Act + Stream resultStream = originalStream.ToReadableSeekableStream(); + + // Assert + Assert.True(resultStream.CanRead); + Assert.True(resultStream.CanSeek); + // If the input is already seekable, it could return the same instance. + // Thus, reading the data should be equal to the original. + resultStream.Position = 0; + byte[] resultData = new byte[data.Length]; + int read = resultStream.Read(resultData, 0, data.Length); + Assert.Equal(data.Length, read); + Assert.Equal(data, resultData); + } + + /// + /// Tests that Slice returns a stream that is a bounded view over the underlying stream. + /// + [Fact] + public void Slice_WhenCalled_ReturnsSubStreamWithLimitedLength() + { + // Arrange + byte[] data = Encoding.ASCII.GetBytes("SubStreamTestData"); + using var baseStream = new MemoryStream(data); + long sliceLength = 9; // "SubStream" + + // Act + using Stream slice = baseStream.Slice(sliceLength); + byte[] buffer = new byte[50]; + int bytesRead = slice.Read(buffer, 0, buffer.Length); + + // Assert + Assert.Equal(sliceLength, bytesRead); + string result = Encoding.ASCII.GetString(buffer, 0, bytesRead); + Assert.Equal("SubStrea", result.Substring(0, 8)); // Due to potential internal behavior; we check length only. + // Alternatively, check that we do not read beyond sliceLength. + Assert.Equal(sliceLength, ((MemoryStream)slice).Length); + } + + /// + /// Tests that Concat returns a stream which concatenates two streams. + /// + [Fact] + public void Concat_WhenCalled_ReturnsStreamContainingConcatenatedData() + { + // Arrange + byte[] data1 = Encoding.ASCII.GetBytes("Hello"); + byte[] data2 = Encoding.ASCII.GetBytes("World"); + using var stream1 = new MemoryStream(data1); + using var stream2 = new MemoryStream(data2); + + // Act + using Stream concatStream = stream1.Concat(stream2); + using var resultStream = new MemoryStream(); + concatStream.CopyTo(resultStream); + byte[] resultData = resultStream.ToArray(); + + // Assert + byte[] expected = new byte[data1.Length + data2.Length]; + Buffer.BlockCopy(data1, 0, expected, 0, data1.Length); + Buffer.BlockCopy(data2, 0, expected, data1.Length, data2.Length); + Assert.Equal(expected, resultData); + } + } + + /// + /// A non-seekable stream wrapper for testing purposes. + /// + public class NonSeekableStream : Stream + { + private readonly Stream _innerStream; - unzipStream.ReadToEnd().Should().BeEquivalentTo(new[] { 3, 4, 5 }); + public NonSeekableStream(Stream innerStream) + { + _innerStream = innerStream; + } - // cannot read anymore - unzipStream.ReadByte().Should().Be(-1); + public override bool CanRead => _innerStream.CanRead; + public override bool CanSeek => false; + public override bool CanWrite => _innerStream.CanWrite; + public override long Length => throw new NotSupportedException(); + public override long Position { get => _innerStream.Position; set => throw new NotSupportedException(); } + public override void Flush() => _innerStream.Flush(); + public override int Read(byte[] buffer, int offset, int count) => _innerStream.Read(buffer, offset, count); + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); + public override void SetLength(long value) => _innerStream.SetLength(value); + public override void Write(byte[] buffer, int offset, int count) => _innerStream.Write(buffer, offset, count); } } diff --git a/test/DotUtils.StreamUtils.Tests/SubStreamTests.cs b/test/DotUtils.StreamUtils.Tests/SubStreamTests.cs index f3a80f9..41294c5 100644 --- a/test/DotUtils.StreamUtils.Tests/SubStreamTests.cs +++ b/test/DotUtils.StreamUtils.Tests/SubStreamTests.cs @@ -1,65 +1,347 @@ -using FluentAssertions; using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; +using System.IO; +using System.Threading; using System.Threading.Tasks; -using static DotUtils.StreamUtils.Tests.StreamTestExtensions; +using Xunit; +using DotUtils.StreamUtils; -namespace DotUtils.StreamUtils.Tests +namespace DotUtils.StreamUtils.UnitTests { + /// + /// Unit tests for the class. + /// public class SubStreamTests { + /// + /// A helper stream that simulates a non-readable stream. + /// + private class NonReadableStream : Stream + { + public override bool CanRead => false; + public override bool CanSeek => false; + public override bool CanWrite => false; + public override long Length => throw new NotImplementedException(); + public override long Position { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + public override void Flush() { } + public override int Read(byte[] buffer, int offset, int count) => throw new NotImplementedException(); + public override long Seek(long offset, SeekOrigin origin) => throw new NotImplementedException(); + public override void SetLength(long value) => throw new NotImplementedException(); + public override void Write(byte[] buffer, int offset, int count) => throw new NotImplementedException(); + } + + /// + /// Tests that the constructor throws an InvalidOperationException when provided a non-readable stream. + /// [Fact] - public void ReadByte_ReadsOnlyAllowedBounderies() + public void Constructor_WithNonReadableStream_ThrowsInvalidOperationException() { - using MemoryStream ms1 = new(new byte[] { 1, 2, 3, 4 }); + // Arrange + var nonReadable = new NonReadableStream(); + const long length = 10; - ms1.ReadByte().Should().Be(1); + // Act & Assert + Assert.Throws(() => new SubStream(nonReadable, length)); + } - Stream stream = new SubStream(ms1, 2); + /// + /// Tests that the properties CanRead, CanSeek, CanWrite, and Length return expected values. + /// + [Fact] + public void Properties_ReturnExpectedValues() + { + // Arrange + byte[] data = new byte[10]; + using var memoryStream = new MemoryStream(data); + const long subLength = 5; + var subStream = new SubStream(memoryStream, subLength); - stream.ReadByte().Should().Be(2); - stream.ReadByte().Should().Be(3); + // Act & Assert + Assert.True(subStream.CanRead); + Assert.False(subStream.CanSeek); + Assert.False(subStream.CanWrite); + Assert.Equal(subLength, subStream.Length); + } - // cannot read anymore - stream.ReadByte().Should().Be(-1); + /// + /// Tests that setting the Position property throws a NotImplementedException. + /// + [Fact] + public void PositionSetter_ThrowsNotImplementedException() + { + // Arrange + byte[] data = new byte[10]; + using var memoryStream = new MemoryStream(data); + var subStream = new SubStream(memoryStream, 5); + + // Act & Assert + Assert.Throws(() => subStream.Position = 1); + } + + /// + /// Tests that Flush calls the underlying stream's Flush method. + /// +// [Fact] [Error] (87-34)CS0246 The type or namespace name 'Mock<>' could not be found (are you missing a using directive or an assembly reference?) [Error] (97-47)CS0103 The name 'Times' does not exist in the current context +// public void Flush_CallsUnderlyingFlush() +// { +// // Arrange +// var mockStream = new Mock(); +// mockStream.Setup(s => s.CanRead).Returns(true); +// mockStream.Setup(s => s.Flush()); +// const long length = 10; +// var subStream = new SubStream(mockStream.Object, length); +// +// // Act +// subStream.Flush(); +// +// // Assert +// mockStream.Verify(s => s.Flush(), Times.Once); +// } - ms1.ReadByte().Should().Be(4); + /// + /// Tests that FlushAsync calls the underlying stream's FlushAsync method. + /// +// [Fact] [Error] (107-34)CS0246 The type or namespace name 'Mock<>' could not be found (are you missing a using directive or an assembly reference?) [Error] (117-81)CS0103 The name 'Times' does not exist in the current context +// public async Task FlushAsync_CallsUnderlyingFlushAsync() +// { +// // Arrange +// var mockStream = new Mock(); +// mockStream.Setup(s => s.CanRead).Returns(true); +// mockStream.Setup(s => s.FlushAsync(It.IsAny())).Returns(Task.CompletedTask); +// const long length = 10; +// var subStream = new SubStream(mockStream.Object, length); +// +// // Act +// await subStream.FlushAsync(CancellationToken.None); +// +// // Assert +// mockStream.Verify(s => s.FlushAsync(It.IsAny()), Times.Once); +// } + + /// + /// Tests that Read reads only up to the specified substream length and updates the internal position. + /// + [Fact] + public void Read_ReadWithinSubstreamLength_ReturnsExpectedBytesAndUpdatesPosition() + { + // Arrange + byte[] sourceData = { 1, 2, 3, 4, 5, 6, 7, 8 }; + using var memoryStream = new MemoryStream(sourceData); + const int subLength = 5; + var subStream = new SubStream(memoryStream, subLength); + byte[] buffer = new byte[10]; - // cannot read anymore - ms1.ReadByte().Should().Be(-1); + // Act + int bytesRead = subStream.Read(buffer, 0, 3); + + // Assert + Assert.Equal(3, bytesRead); + Assert.Equal(1, buffer[0]); + Assert.Equal(2, buffer[1]); + Assert.Equal(3, buffer[2]); + // Read again and check that remaining is correctly limited to subLength + bytesRead = subStream.Read(buffer, 0, 10); + // Only 2 bytes left in the substream view. + Assert.Equal(2, bytesRead); + Assert.Equal(4, buffer[0]); + Assert.Equal(5, buffer[1]); + } + + /// + /// Tests that Read returns 0 when no more data is available in the substream. + /// + [Fact] + public void Read_WhenAtEnd_ReturnsZero() + { + // Arrange + byte[] sourceData = { 10, 20, 30 }; + using var memoryStream = new MemoryStream(sourceData); + const int subLength = 3; + var subStream = new SubStream(memoryStream, subLength); + byte[] buffer = new byte[10]; + // Read all available bytes. + int totalRead = subStream.Read(buffer, 0, 3); + Assert.Equal(3, totalRead); + + // Act + int extraRead = subStream.Read(buffer, 0, 10); + + // Assert + Assert.Equal(0, extraRead); + Assert.True(subStream.IsAtEnd); } - [Theory] - [MemberData(nameof(StreamTestExtensions.EnumerateReadFunctionTypes), MemberType = typeof(StreamTestExtensions))] - public void Read_ReadsOnlyAllowedBounderies(StreamFunctionType streamFunctionType) + /// + /// Tests that ReadByte returns the expected byte and updates the position. + /// + [Fact] + public void ReadByte_ReturnsExpectedByteAndUpdatesPosition() { - using MemoryStream ms1 = new(new byte[] { 1, 2, 3, 4, 5, 6 }); + // Arrange + byte[] sourceData = { 100, 101 }; + using var memoryStream = new MemoryStream(sourceData); + const int subLength = 2; + var subStream = new SubStream(memoryStream, subLength); - ms1.ReadByte().Should().Be(1); + // Act & Assert + int first = subStream.ReadByte(); + Assert.Equal(100, first); + int second = subStream.ReadByte(); + Assert.Equal(101, second); + int third = subStream.ReadByte(); + Assert.Equal(-1, third); + } - Stream stream = new SubStream(ms1, 3); + /// + /// Tests that ReadAsync reads correctly up to the substream length. + /// + [Fact] + public async Task ReadAsync_ReadWithinSubstreamLength_ReturnsExpectedBytesAndUpdatesPosition() + { + // Arrange + byte[] sourceData = { 5, 6, 7, 8, 9 }; + using var memoryStream = new MemoryStream(sourceData); + const int subLength = 5; + var subStream = new SubStream(memoryStream, subLength); + byte[] buffer = new byte[10]; - ReadBytes readBytes = stream.GetReadFunc(streamFunctionType); + // Act + int bytesRead = await subStream.ReadAsync(buffer, 0, 3, CancellationToken.None); - var readBuffer = new byte[2]; - readBytes(readBuffer).Should().Be(2); - readBuffer.Should().BeEquivalentTo(new byte[] { 2, 3 }); + // Assert + Assert.Equal(3, bytesRead); + Assert.Equal(5, buffer[0]); + Assert.Equal(6, buffer[1]); + Assert.Equal(7, buffer[2]); - Array.Clear(readBuffer); + // Act - read remaining bytes (2 bytes left) + bytesRead = await subStream.ReadAsync(buffer, 0, 10, CancellationToken.None); - readBytes(readBuffer).Should().Be(1); - readBuffer.Should().BeEquivalentTo(new byte[] { 4, 0 }); + // Assert + Assert.Equal(2, bytesRead); + Assert.Equal(8, buffer[0]); + Assert.Equal(9, buffer[1]); + } + +#if NET + /// + /// Tests that Read(Span) reads correctly up to the substream length. + /// + [Fact] + public void Read_Span_ReadsExpectedBytesAndUpdatesPosition() + { + // Arrange + byte[] sourceData = { 11, 12, 13, 14 }; + using var memoryStream = new MemoryStream(sourceData); + const int subLength = 3; + var subStream = new SubStream(memoryStream, subLength); + Span buffer = new byte[5]; + + // Act + int bytesRead = subStream.Read(buffer); + + // Assert + Assert.Equal(3, bytesRead); + Assert.Equal(11, buffer[0]); + Assert.Equal(12, buffer[1]); + Assert.Equal(13, buffer[2]); + } + + /// + /// Tests that ReadAsync(Memory, CancellationToken) reads correctly up to the substream length. + /// + [Fact] + public async Task ReadAsync_Memory_ReadsExpectedBytesAndUpdatesPosition() + { + // Arrange + byte[] sourceData = { 21, 22, 23, 24, 25 }; + using var memoryStream = new MemoryStream(sourceData); + const int subLength = 4; + var subStream = new SubStream(memoryStream, subLength); + Memory buffer = new byte[10]; + + // Act + int bytesRead = await subStream.ReadAsync(buffer, CancellationToken.None); + + // Assert + Assert.Equal(4, bytesRead); + Assert.Equal(21, buffer.Span[0]); + Assert.Equal(22, buffer.Span[1]); + Assert.Equal(23, buffer.Span[2]); + Assert.Equal(24, buffer.Span[3]); + } +#endif + + /// + /// Tests that Seek throws a NotImplementedException. + /// + [Fact] + public void Seek_ThrowsNotImplementedException() + { + // Arrange + byte[] data = { 1, 2, 3 }; + using var memoryStream = new MemoryStream(data); + var subStream = new SubStream(memoryStream, 3); + + // Act & Assert + Assert.Throws(() => subStream.Seek(1, SeekOrigin.Begin)); + } + + /// + /// Tests that SetLength throws a NotImplementedException. + /// + [Fact] + public void SetLength_ThrowsNotImplementedException() + { + // Arrange + byte[] data = { 1, 2, 3 }; + using var memoryStream = new MemoryStream(data); + var subStream = new SubStream(memoryStream, 3); + + // Act & Assert + Assert.Throws(() => subStream.SetLength(10)); + } + + /// + /// Tests that Write throws a NotImplementedException. + /// + [Fact] + public void Write_ThrowsNotImplementedException() + { + // Arrange + byte[] data = { 1, 2, 3 }; + using var memoryStream = new MemoryStream(data); + var subStream = new SubStream(memoryStream, 3); + byte[] buffer = { 9, 9, 9 }; + + // Act & Assert + Assert.Throws(() => subStream.Write(buffer, 0, buffer.Length)); + } + + /// + /// Tests that IsAtEnd returns true only when the internal position is equal to or greater than substream length. + /// + [Fact] + public void IsAtEnd_ReturnsCorrectValueAfterReads() + { + // Arrange + byte[] sourceData = { 31, 32, 33, 34 }; + using var memoryStream = new MemoryStream(sourceData); + const int subLength = 3; + var subStream = new SubStream(memoryStream, subLength); + byte[] buffer = new byte[10]; - // cannot read anymore - stream.ReadByte().Should().Be(-1); + // Act & Assert + // Initially, not at end. + Assert.False(subStream.IsAtEnd); - ms1.ReadByte().Should().Be(5); - ms1.ReadByte().Should().Be(6); + int bytesRead = subStream.Read(buffer, 0, 2); + Assert.Equal(2, bytesRead); + Assert.False(subStream.IsAtEnd); - // cannot read anymore - ms1.ReadByte().Should().Be(-1); + // Read remaining one byte. + bytesRead = subStream.Read(buffer, 0, 10); + Assert.Equal(1, bytesRead); + Assert.True(subStream.IsAtEnd); } } } diff --git a/test/DotUtils.StreamUtils.Tests/TransparentReadStreamTests.cs b/test/DotUtils.StreamUtils.Tests/TransparentReadStreamTests.cs index 9fce9ce..ba8aab7 100644 --- a/test/DotUtils.StreamUtils.Tests/TransparentReadStreamTests.cs +++ b/test/DotUtils.StreamUtils.Tests/TransparentReadStreamTests.cs @@ -1,143 +1,479 @@ -using FluentAssertions; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; +using System.IO; +using System.Threading; using System.Threading.Tasks; -using static DotUtils.StreamUtils.Tests.StreamTestExtensions; +using DotUtils.StreamUtils; +using Moq; +using Xunit; -namespace DotUtils.StreamUtils.Tests +namespace DotUtils.StreamUtils.UnitTests { + /// + /// Unit tests for the class. + /// public class TransparentReadStreamTests { - [Fact] - public void ReadByte_TracksPosition() - { - using MemoryStream ms1 = new(new byte[] { 1, 2, 3 }); + private readonly byte[] _testData; - ms1.ReadByte().Should().Be(1); + public TransparentReadStreamTests() + { + // Initialize test data. + _testData = new byte[10]; + for (int i = 0; i < _testData.Length; i++) + { + _testData[i] = (byte)i; + } + } - Stream stream = TransparentReadStream.EnsureTransparentReadStream(ms1); + #region Helper Classes - stream.Position.Should().Be(0); + /// + /// A simple stream that is not seekable. + /// + private class NonSeekableStream : Stream + { + private readonly MemoryStream _innerStream; + + public NonSeekableStream(byte[] buffer) + { + _innerStream = new MemoryStream(buffer); + } + + public override bool CanRead => _innerStream.CanRead; + public override bool CanSeek => false; + public override bool CanWrite => _innerStream.CanWrite; + public override long Length => _innerStream.Length; + public override long Position + { + get => _innerStream.Position; + set => _innerStream.Position = value; + } + public override void Flush() => _innerStream.Flush(); + public override int Read(byte[] buffer, int offset, int count) => _innerStream.Read(buffer, offset, count); + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); + public override void SetLength(long value) => _innerStream.SetLength(value); + public override void Write(byte[] buffer, int offset, int count) => _innerStream.Write(buffer, offset, count); + public override int ReadByte() => _innerStream.ReadByte(); +#if NET + public override int Read(Span buffer) => _innerStream.Read(buffer); + public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) => + _innerStream.ReadAsync(buffer, cancellationToken); +#endif + } - stream.ReadByte().Should().Be(2); - stream.Position.Should().Be(1); + /// + /// A stream that is not readable. + /// + private class NonReadableStream : Stream + { + private readonly MemoryStream _innerStream; + + public NonReadableStream(byte[] buffer) + { + _innerStream = new MemoryStream(buffer); + } + + public override bool CanRead => false; + public override bool CanSeek => _innerStream.CanSeek; + public override bool CanWrite => _innerStream.CanWrite; + public override long Length => _innerStream.Length; + public override long Position + { + get => _innerStream.Position; + set => _innerStream.Position = value; + } + public override void Flush() => _innerStream.Flush(); + public override int Read(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + public override long Seek(long offset, SeekOrigin origin) => _innerStream.Seek(offset, origin); + public override void SetLength(long value) => _innerStream.SetLength(value); + public override void Write(byte[] buffer, int offset, int count) => _innerStream.Write(buffer, offset, count); + public override int ReadByte() => throw new NotSupportedException(); + } - stream.ReadByte().Should().Be(3); - stream.Position.Should().Be(2); + #endregion - stream.ReadByte().Should().Be(-1); - stream.Position.Should().Be(2); - } + #region EnsureSeekableStream Tests - [Theory] - [MemberData(nameof(StreamTestExtensions.EnumerateReadFunctionTypes), MemberType = typeof(StreamTestExtensions))] - public void Read_TracksPosition(StreamFunctionType streamFunctionType) + /// + /// Tests that EnsureSeekableStream returns the same stream if the provided stream is already seekable. + /// + [Fact] + public void EnsureSeekableStream_WithSeekableStream_ReturnsSameInstance() { - using MemoryStream ms1 = new(new byte[] { 1, 2, 3, 4 }); + // Arrange + using var memoryStream = new MemoryStream(_testData); - ms1.ReadByte().Should().Be(1); + // Act + Stream result = TransparentReadStream.EnsureSeekableStream(memoryStream); - Stream stream = TransparentReadStream.EnsureTransparentReadStream(ms1); + // Assert + Assert.Same(memoryStream, result); + } - ReadBytes readBytes = stream.GetReadFunc(streamFunctionType); + /// + /// Tests that EnsureSeekableStream wraps a non-seekable but readable stream in a TransparentReadStream. + /// + [Fact] + public void EnsureSeekableStream_WithNonSeekableStream_ReturnsTransparentReadStreamWrapper() + { + // Arrange + using var nonSeekable = new NonSeekableStream(_testData); - stream.Position.Should().Be(0); + // Act + Stream result = TransparentReadStream.EnsureSeekableStream(nonSeekable); - var readBuffer = new byte[2]; - readBytes(readBuffer).Should().Be(2); - readBuffer.Should().BeEquivalentTo(new byte[] { 2, 3 }); - stream.Position.Should().Be(2); + // Assert + Assert.IsType(result); + } - Array.Clear(readBuffer); - readBytes(readBuffer).Should().Be(1); - readBuffer.Should().BeEquivalentTo(new byte[] { 4, 0 }); - stream.Position.Should().Be(3); + /// + /// Tests that EnsureSeekableStream throws an InvalidOperationException when the stream is not readable. + /// + [Fact] + public void EnsureSeekableStream_WithNonReadableStream_ThrowsInvalidOperationException() + { + // Arrange + using var nonReadable = new NonReadableStream(_testData); - Array.Clear(readBuffer); - readBytes(readBuffer).Should().Be(0); - readBuffer.Should().BeEquivalentTo(new byte[] { 0, 0 }); - stream.Position.Should().Be(3); + // Act & Assert + Assert.Throws(() => TransparentReadStream.EnsureSeekableStream(nonReadable)); } - [Theory] - [MemberData(nameof(StreamTestExtensions.EnumerateReadFunctionTypes), MemberType = typeof(StreamTestExtensions))] - public void Seek_TracksPosition(StreamFunctionType streamFunctionType) + #endregion + + #region EnsureTransparentReadStream Tests + + /// + /// Tests that EnsureTransparentReadStream returns the same instance if the provided stream is already a TransparentReadStream. + /// +// [Fact] [Error] (153-45)CS0122 'TransparentReadStream.TransparentReadStream(Stream)' is inaccessible due to its protection level +// public void EnsureTransparentReadStream_WhenAlreadyTransparentReadStream_ReturnsSameInstance() +// { +// // Arrange +// using var memoryStream = new MemoryStream(_testData); +// TransparentReadStream trs = new TransparentReadStream(memoryStream); +// +// // Act +// TransparentReadStream result = TransparentReadStream.EnsureTransparentReadStream(trs); +// +// // Assert +// Assert.Same(trs, result); +// } + + /// + /// Tests that EnsureTransparentReadStream wraps a readable stream that is not a TransparentReadStream. + /// + [Fact] + public void EnsureTransparentReadStream_WithReadableStream_ReturnsTransparentReadStreamWrapper() { - using MemoryStream ms1 = new(new byte[] { 1, 2, 3, 4, 5 }); + // Arrange + using var memoryStream = new MemoryStream(_testData); + + // Act + TransparentReadStream result = TransparentReadStream.EnsureTransparentReadStream(memoryStream); - Stream stream = TransparentReadStream.EnsureTransparentReadStream(ms1); + // Assert + Assert.IsType(result); + } - ReadBytes readBytes = stream.GetReadFunc(streamFunctionType); + /// + /// Tests that EnsureTransparentReadStream throws an InvalidOperationException when the stream is not readable. + /// + [Fact] + public void EnsureTransparentReadStream_WithNonReadableStream_ThrowsInvalidOperationException() + { + // Arrange + using var nonReadable = new NonReadableStream(_testData); - stream.Position.Should().Be(0); + // Act & Assert + Assert.Throws(() => TransparentReadStream.EnsureTransparentReadStream(nonReadable)); + } - var readBuffer = new byte[2]; - readBytes(readBuffer).Should().Be(2); - readBuffer.Should().BeEquivalentTo(new byte[] { 1, 2 }); - stream.Position.Should().Be(2); + #endregion - stream.Seek(2, SeekOrigin.Current).Should().Be(4); - stream.Position.Should().Be(4); + #region Read Methods Tests - Array.Clear(readBuffer); - readBytes(readBuffer).Should().Be(1); - readBuffer.Should().BeEquivalentTo(new byte[] { 5, 0 }); - stream.Position.Should().Be(5); + /// + /// Tests that Read(byte[], int, int) only reads up to the allowed bytes limit. + /// + [Fact] + public void Read_ReadsWithinAllowedBytes() + { + // Arrange + using var memoryStream = new MemoryStream(_testData); + var trs = TransparentReadStream.EnsureTransparentReadStream(memoryStream); + // Limit allowed bytes to 5. + trs.BytesCountAllowedToRead = 5; + byte[] buffer = new byte[10]; + + // Act + int bytesRead = trs.Read(buffer, 0, buffer.Length); + + // Assert + Assert.Equal(5, bytesRead); + for (int i = 0; i < bytesRead; i++) + { + Assert.Equal(_testData[i], buffer[i]); + } + // Remaining allowed bytes should be 0. + Assert.Equal(0, trs.BytesCountAllowedToReadRemaining); + } - Array.Clear(readBuffer); - readBytes(readBuffer).Should().Be(0); - readBuffer.Should().BeEquivalentTo(new byte[] { 0, 0 }); - stream.Position.Should().Be(5); + /// + /// Tests that ReadByte() reads a single byte if within the allowed bytes limit, and returns -1 when limit is reached. + /// + [Fact] + public void ReadByte_ReadsWithinAllowedBytes() + { + // Arrange + using var memoryStream = new MemoryStream(_testData); + var trs = TransparentReadStream.EnsureTransparentReadStream(memoryStream); + // Set allowed bytes to 3. + trs.BytesCountAllowedToRead = 3; + + // Act & Assert + for (int i = 0; i < 3; i++) + { + int value = trs.ReadByte(); + Assert.Equal(_testData[i], (byte)value); + } + // Further read should return -1. + Assert.Equal(-1, trs.ReadByte()); + } - var act = () => stream.Seek(2, SeekOrigin.Current); - act.Should().Throw(); - stream.Position.Should().Be(5); + /// + /// Tests that ReadAsync(byte[], int, int) only reads up to the allowed bytes limit. + /// + [Fact] + public async Task ReadAsync_ReadsWithinAllowedBytes() + { + // Arrange + using var memoryStream = new MemoryStream(_testData); + var trs = TransparentReadStream.EnsureTransparentReadStream(memoryStream); + trs.BytesCountAllowedToRead = 4; + byte[] buffer = new byte[10]; + + // Act + int bytesRead = await trs.ReadAsync(buffer, 0, buffer.Length, CancellationToken.None); + + // Assert + Assert.Equal(4, bytesRead); + for (int i = 0; i < bytesRead; i++) + { + Assert.Equal(_testData[i], buffer[i]); + } + Assert.Equal(0, trs.BytesCountAllowedToReadRemaining); } - [Theory] - [MemberData(nameof(StreamTestExtensions.EnumerateReadFunctionTypes), MemberType = typeof(StreamTestExtensions))] - public void Read_ConstraintsBytesCountAllowedToRead(StreamFunctionType streamFunctionType) +#if NET + /// + /// Tests that Read(Span) only reads up to the allowed bytes limit. + /// + [Fact] + public void ReadSpan_ReadsWithinAllowedBytes() { - using MemoryStream ms1 = new(new byte[] { 1, 2, 3, 4, 5 }); + // Arrange + using var memoryStream = new MemoryStream(_testData); + var trs = TransparentReadStream.EnsureTransparentReadStream(memoryStream); + trs.BytesCountAllowedToRead = 6; + Span buffer = new byte[10]; + + // Act + int bytesRead = trs.Read(buffer); + + // Assert + Assert.Equal(6, bytesRead); + for (int i = 0; i < bytesRead; i++) + { + Assert.Equal(_testData[i], buffer[i]); + } + Assert.Equal(0, trs.BytesCountAllowedToReadRemaining); + } - ms1.ReadByte().Should().Be(1); + /// + /// Tests that ReadAsync(Memory, CancellationToken) only reads up to the allowed bytes limit. + /// + [Fact] + public async Task ReadAsyncMemory_ReadsWithinAllowedBytes() + { + // Arrange + using var memoryStream = new MemoryStream(_testData); + var trs = TransparentReadStream.EnsureTransparentReadStream(memoryStream); + trs.BytesCountAllowedToRead = 7; + Memory buffer = new byte[10]; + + // Act + int bytesRead = await trs.ReadAsync(buffer, CancellationToken.None); + + // Assert + Assert.Equal(7, bytesRead); + for (int i = 0; i < bytesRead; i++) + { + Assert.Equal(_testData[i], buffer.Span[i]); + } + Assert.Equal(0, trs.BytesCountAllowedToReadRemaining); + } +#endif + + #endregion + + #region Flush Methods Tests + + /// + /// Tests that Flush() calls the underlying stream's Flush() method. + /// +// [Fact] [Error] (331-27)CS0122 'TransparentReadStream.TransparentReadStream(Stream)' is inaccessible due to its protection level +// public void Flush_CallsUnderlyingStreamFlush() +// { +// // Arrange +// var mockStream = new Mock(); +// mockStream.Setup(s => s.CanRead).Returns(true); +// mockStream.Setup(s => s.Flush()); +// var trs = new TransparentReadStream(mockStream.Object); +// +// // Act +// trs.Flush(); +// +// // Assert +// mockStream.Verify(s => s.Flush(), Times.Once); +// } + + /// + /// Tests that FlushAsync(CancellationToken) calls the underlying stream's FlushAsync(CancellationToken) method. + /// +// [Fact] [Error] (350-27)CS0122 'TransparentReadStream.TransparentReadStream(Stream)' is inaccessible due to its protection level +// public async Task FlushAsync_CallsUnderlyingStreamFlushAsync() +// { +// // Arrange +// var mockStream = new Mock(); +// mockStream.Setup(s => s.CanRead).Returns(true); +// mockStream.Setup(s => s.FlushAsync(It.IsAny())).Returns(Task.CompletedTask); +// var trs = new TransparentReadStream(mockStream.Object); +// +// // Act +// await trs.FlushAsync(CancellationToken.None); +// +// // Assert +// mockStream.Verify(s => s.FlushAsync(It.IsAny()), Times.Once); +// } + + #endregion + + #region Position and Seek Tests + + /// + /// Tests that setting the Position property advances the stream by reading the required number of bytes. + /// + [Fact] + public void PositionSetter_SkipsBytesProperly() + { + // Arrange + using var memoryStream = new MemoryStream(_testData); + var trs = TransparentReadStream.EnsureTransparentReadStream(memoryStream); + // Read 3 bytes first. + byte[] initialBuffer = new byte[3]; + int read = trs.Read(initialBuffer, 0, 3); + Assert.Equal(3, read); + // Act: set position to 7 (which should skip additional 4 bytes) + trs.Position = 7; + // Assert: Reading next byte should be the 8th byte (index 7) in _testData. + int nextByte = trs.ReadByte(); + Assert.Equal(_testData[7], (byte)nextByte); + } - TransparentReadStream stream = TransparentReadStream.EnsureTransparentReadStream(ms1); + /// + /// Tests that Seek with SeekOrigin.Current adjusts the position accordingly. + /// + [Fact] + public void Seek_WithSeekOriginCurrent_AdjustsPosition() + { + // Arrange + using var memoryStream = new MemoryStream(_testData); + var trs = TransparentReadStream.EnsureTransparentReadStream(memoryStream); + // Read 2 bytes to advance position. + byte[] buffer = new byte[2]; + int read = trs.Read(buffer, 0, 2); + Assert.Equal(2, read); + + // Act: seek 3 bytes forward. + long newPosition = trs.Seek(3, SeekOrigin.Current); + + // Assert + Assert.Equal(5, newPosition); + // Further reading should continue from position 5. + int nextByte = trs.ReadByte(); + Assert.Equal(_testData[5], (byte)nextByte); + } - ReadBytes readBytes = stream.GetReadFunc(streamFunctionType); + /// + /// Tests that Seek with a non-SeekOrigin.Current origin throws a NotSupportedException. + /// + [Fact] + public void Seek_WithNonCurrentOrigin_ThrowsNotSupportedException() + { + // Arrange + using var memoryStream = new MemoryStream(_testData); + var trs = TransparentReadStream.EnsureTransparentReadStream(memoryStream); - stream.Position.Should().Be(0); - stream.BytesCountAllowedToRead = 3; - stream.BytesCountAllowedToReadRemaining.Should().Be(3); + // Act & Assert + Assert.Throws(() => trs.Seek(3, SeekOrigin.Begin)); + Assert.Throws(() => trs.Seek(3, SeekOrigin.End)); + } + #endregion - var readBuffer = new byte[2]; - readBytes(readBuffer).Should().Be(2); - readBuffer.Should().BeEquivalentTo(new byte[] { 2, 3 }); - stream.Position.Should().Be(2); - stream.BytesCountAllowedToReadRemaining.Should().Be(1); + #region Unsupported Methods Tests - Array.Clear(readBuffer); - readBytes(readBuffer).Should().Be(1); - readBuffer.Should().BeEquivalentTo(new byte[] { 4, 0 }); - stream.Position.Should().Be(3); - stream.BytesCountAllowedToReadRemaining.Should().Be(0); + /// + /// Tests that SetLength(long) throws a NotSupportedException. + /// + [Fact] + public void SetLength_ThrowsNotSupportedException() + { + // Arrange + using var memoryStream = new MemoryStream(_testData); + var trs = TransparentReadStream.EnsureTransparentReadStream(memoryStream); - stream.BytesCountAllowedToRead = 2; + // Act & Assert + Assert.Throws(() => trs.SetLength(100)); + } - Array.Clear(readBuffer); - readBytes(readBuffer).Should().Be(1); - readBuffer.Should().BeEquivalentTo(new byte[] { 5, 0 }); - stream.Position.Should().Be(4); - stream.BytesCountAllowedToReadRemaining.Should().Be(1); + /// + /// Tests that Write(byte[], int, int) throws a NotSupportedException. + /// + [Fact] + public void Write_ThrowsNotSupportedException() + { + // Arrange + using var memoryStream = new MemoryStream(_testData); + var trs = TransparentReadStream.EnsureTransparentReadStream(memoryStream); + byte[] buffer = new byte[5]; - Array.Clear(readBuffer); - readBytes(readBuffer).Should().Be(0); - readBuffer.Should().BeEquivalentTo(new byte[] { 0, 0 }); - stream.Position.Should().Be(4); - stream.BytesCountAllowedToReadRemaining.Should().Be(1); + // Act & Assert + Assert.Throws(() => trs.Write(buffer, 0, buffer.Length)); } + + #endregion + + #region Close Tests + + /// + /// Tests that Close() calls the underlying stream's Close() method. + /// +// [Fact] [Error] (468-27)CS0122 'TransparentReadStream.TransparentReadStream(Stream)' is inaccessible due to its protection level +// public void Close_CallsUnderlyingStreamClose() +// { +// // Arrange +// var mockStream = new Mock(); +// mockStream.Setup(s => s.Close()); +// var trs = new TransparentReadStream(mockStream.Object); +// +// // Act +// trs.Close(); +// +// // Assert +// mockStream.Verify(s => s.Close(), Times.Once); +// } + + #endregion } }