Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,7 @@
<Import Project="dependabot\Packages.props" />
<ItemGroup>
</ItemGroup>
<ItemGroup>
<PackageVersion Include="Moq" Version="4.16.1" />
</ItemGroup>
</Project>
363 changes: 300 additions & 63 deletions test/DotUtils.StreamUtils.Tests/ChunkedBufferStreamTests.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Unit tests for the <see cref="ChunkedBufferStream"/> class.
/// </summary>
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";

/// <summary>
/// A test stream derived from MemoryStream to verify that Close is called.
/// </summary>
private class TestMemoryStream : MemoryStream
{
public bool IsClosed { get; private set; } = false;

public override void Close()
{
IsClosed = true;
base.Close();
}
}

/// <summary>
/// Tests that Flush writes any partial data in the buffer to the underlying stream.
/// </summary>
[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);
}

/// <summary>
/// Tests that Write(byte[], int, int) flushes automatically when the buffer becomes full and retains leftover data.
/// </summary>
[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);
}

/// <summary>
/// Tests that WriteByte writes a byte and flushes automatically when the buffer becomes full.
/// </summary>
[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);
}

/// <summary>
/// Tests that WriteAsync writes data asynchronously and flushes automatically when the buffer becomes full.
/// </summary>
[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
/// <summary>
/// Tests that Write(ReadOnlySpan) writes data and flushes automatically when the buffer becomes full.
/// </summary>
[Fact]
public void Write_ReadOnlySpan_WithDataExceedingBuffer_FlushesAutomaticallyAndRetainsRemainingData()
{
// Arrange
var underlyingStream = new MemoryStream();
const int bufferSize = 5;
using var chunkedStream = new ChunkedBufferStream(underlyingStream, bufferSize);
ReadOnlySpan<byte> 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);
}

/// <summary>
/// Tests that WriteAsync(ReadOnlyMemory) writes data asynchronously and flushes automatically when the buffer becomes full.
/// </summary>
[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<byte> 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

/// <summary>
/// Tests that Read method always throws NotSupportedException.
/// </summary>
[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<NotSupportedException>(() => chunkedStream.Read(buffer, 0, buffer.Length));
Assert.Equal(NotSupportedMessage, exception.Message);
}

/// <summary>
/// Tests that Seek method always throws NotSupportedException.
/// </summary>
[Fact]
public void Seek_AnyArguments_ThrowsNotSupportedException()
{
// Arrange
var underlyingStream = new MemoryStream();
using var chunkedStream = new ChunkedBufferStream(underlyingStream, 4);

// Act & Assert
var exception = Assert.Throws<NotSupportedException>(() => chunkedStream.Seek(0, SeekOrigin.Begin));
Assert.Equal(NotSupportedMessage, exception.Message);
}

/// <summary>
/// Tests that SetLength method always throws NotSupportedException.
/// </summary>
[Fact]
public void SetLength_AnyArgument_ThrowsNotSupportedException()
{
// Arrange
var underlyingStream = new MemoryStream();
using var chunkedStream = new ChunkedBufferStream(underlyingStream, 4);

// Act & Assert
var exception = Assert.Throws<NotSupportedException>(() => chunkedStream.SetLength(100));
Assert.Equal(NotSupportedMessage, exception.Message);
}

/// <summary>
/// Tests that setting Position property throws NotSupportedException.
/// </summary>
[Fact]
public void SetPosition_PropertySetter_ThrowsNotSupportedException()
{
// Arrange
var underlyingStream = new MemoryStream();
using var chunkedStream = new ChunkedBufferStream(underlyingStream, 4);

// Act & Assert
var exception = Assert.Throws<NotSupportedException>(() => chunkedStream.Position = 10);
Assert.Equal(NotSupportedMessage, exception.Message);
}

/// <summary>
/// Tests that the CanRead, CanSeek, and CanWrite properties return the expected values.
/// </summary>
[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);
}

/// <summary>
/// Tests that Length property returns the sum of underlying stream length and buffered data count.
/// </summary>
[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);
/// <summary>
/// Tests that Position getter returns the sum of underlying stream position and buffered data count.
/// </summary>
[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);
}

/// <summary>
/// Tests that Close flushes any remaining buffer to underlying stream and closes the underlying stream.
/// </summary>
[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);
}
}
}
Loading
Loading