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
}
}