Flesh out `ByteRangeStream` methods to make it always limit reads (#213)

* Flesh out `ByteRangeStream` methods to make it always limit reads
- #206
- `Seek(...)` was a no-op, `Position` was incorrect if `_lowerbounds != 0`, `ReadAsync(...)` read entire inner `Stream`
  - rewrite `ByteRangeStreamTest` to cover new `ByteRangeStream` members and hold fewer resources
- remove `DelegatingStream.CopyToAsync(...)` override because it copied entire inner `Stream` and was not needed
  - base implementation invokes `ReadAsync(...)`
This commit is contained in:
Doug Bunting 2018-11-27 19:17:30 -08:00 коммит произвёл GitHub
Родитель cfda5e5e44
Коммит 39d3064baf
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
5 изменённых файлов: 585 добавлений и 81 удалений

Просмотреть файл

@ -3,12 +3,14 @@
using System.IO;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
using System.Web.Http;
namespace System.Net.Http.Internal
{
/// <summary>
/// Stream which only exposes a read-only only range view of an
/// Stream which only exposes a read-only only range view of an
/// inner stream.
/// </summary>
internal class ByteRangeStream : DelegatingStream
@ -16,7 +18,7 @@ namespace System.Net.Http.Internal
// The offset stream position at which the range starts.
private readonly long _lowerbounds;
// The total number of bytes within the range.
// The total number of bytes within the range.
private readonly long _totalCount;
// The current number of bytes read into the range
@ -92,6 +94,23 @@ namespace System.Net.Http.Internal
get { return false; }
}
public override long Position
{
get
{
return _currentCount;
}
set
{
if (value < 0)
{
throw Error.ArgumentMustBeGreaterThanOrEqualTo("value", value, 0L);
}
_currentCount = value;
}
}
public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
{
return base.BeginRead(buffer, offset, PrepareStreamForRangeRead(count), callback, state);
@ -102,6 +121,11 @@ namespace System.Net.Http.Internal
return base.Read(buffer, offset, PrepareStreamForRangeRead(count));
}
public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
return base.ReadAsync(buffer, offset, PrepareStreamForRangeRead(count), cancellationToken);
}
public override int ReadByte()
{
int effectiveCount = PrepareStreamForRangeRead(1);
@ -109,9 +133,35 @@ namespace System.Net.Http.Internal
{
return -1;
}
return base.ReadByte();
}
public override long Seek(long offset, SeekOrigin origin)
{
switch (origin)
{
case SeekOrigin.Begin:
_currentCount = offset;
break;
case SeekOrigin.Current:
_currentCount = _currentCount + offset;
break;
case SeekOrigin.End:
_currentCount = _totalCount + offset;
break;
default:
throw Error.InvalidEnumArgument("origin", (int)origin, typeof(SeekOrigin));
}
if (_currentCount < 0L)
{
throw new IOException(Properties.Resources.ByteRangeStreamInvalidOffset);
}
return _currentCount;
}
public override void SetLength(long value)
{
throw Error.NotSupported(Properties.Resources.ByteRangeStreamReadOnly);
@ -132,33 +182,49 @@ namespace System.Net.Http.Internal
throw Error.NotSupported(Properties.Resources.ByteRangeStreamReadOnly);
}
public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
throw Error.NotSupported(Properties.Resources.ByteRangeStreamReadOnly);
}
public override void WriteByte(byte value)
{
throw Error.NotSupported(Properties.Resources.ByteRangeStreamReadOnly);
}
/// <summary>
/// Gets the
/// Gets the correct count for the next read operation.
/// </summary>
/// <param name="count">The count requested to be read by the caller.</param>
/// <returns>The remaining bytes to read within the range defined for this stream.</returns>
private int PrepareStreamForRangeRead(int count)
{
long effectiveCount = Math.Min(count, _totalCount - _currentCount);
if (effectiveCount > 0)
// A negative count causes base.Raad* methods to throw an ArgumentOutOfRangeException.
if (count <= 0)
{
// Check if we should update the stream position
long position = InnerStream.Position;
if (_lowerbounds + _currentCount != position)
{
InnerStream.Position = _lowerbounds + _currentCount;
}
// Update current number of bytes read
_currentCount += effectiveCount;
return count;
}
// Effective count can never be bigger than int
// Reading past the end simply does nothing.
if (_currentCount >= _totalCount)
{
return 0;
}
long effectiveCount = Math.Min(count, _totalCount - _currentCount);
// Check if we should update the inner stream's position.
var newPosition = _lowerbounds + _currentCount;
var position = InnerStream.Position;
if (newPosition != position)
{
InnerStream.Position = newPosition;
}
// Update current number of bytes read.
_currentCount += effectiveCount;
// Effective count can never be bigger than int.
return (int)effectiveCount;
}
}

Просмотреть файл

@ -9,12 +9,12 @@ using System.Web.Http;
namespace System.Net.Http.Internal
{
/// <summary>
/// Stream that delegates to inner stream.
/// Stream that delegates to inner stream.
/// This is taken from System.Net.Http
/// </summary>
internal abstract class DelegatingStream : Stream
{
private Stream _innerStream;
private readonly Stream _innerStream;
protected DelegatingStream(Stream innerStream)
{
@ -119,11 +119,6 @@ namespace System.Net.Http.Internal
_innerStream.Flush();
}
public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken)
{
return _innerStream.CopyToAsync(destination, bufferSize, cancellationToken);
}
public override Task FlushAsync(CancellationToken cancellationToken)
{
return _innerStream.FlushAsync(cancellationToken);

Просмотреть файл

@ -19,7 +19,7 @@ namespace System.Net.Http.Properties {
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources {
@ -137,6 +137,15 @@ namespace System.Net.Http.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to An attempt was made to move the position before the beginning of the stream..
/// </summary>
internal static string ByteRangeStreamInvalidOffset {
get {
return ResourceManager.GetString("ByteRangeStreamInvalidOffset", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to None of the requested ranges ({0}) overlap with the current extent of the selected resource..
/// </summary>

Просмотреть файл

@ -339,4 +339,7 @@
<data name="RemoteStreamInfoCannotBeNull" xml:space="preserve">
<value>The '{0}' method in '{1}' returned null. It must return a RemoteStreamResult instance containing a writable stream and a valid URL.</value>
</data>
<data name="ByteRangeStreamInvalidOffset" xml:space="preserve">
<value>An attempt was made to move the position before the beginning of the stream.</value>
</data>
</root>

Просмотреть файл

@ -3,6 +3,8 @@
using System.IO;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.TestCommon;
using Moq;
@ -10,10 +12,63 @@ namespace System.Net.Http.Internal
{
public class ByteRangeStreamTest
{
// from, to, expectedText
public static TheoryDataSet<long?, long?, string> CopyBoundsData
{
get
{
return new TheoryDataSet<long?, long?, string>
{
{ null, 23, "This is the whole text." },
{ 0, null, "This is the whole text." },
{ 0, 22, "This is the whole text." },
{ 0, 3, "This" },
{ 12, 16, "whole" },
{ null, 5, "text." },
{ 18, null, "text." },
{ 18, 22, "text." },
};
}
}
// from, to, innerLength, effectiveLength
public static TheoryDataSet<int, int, int, int> ReadBoundsData
{
get
{
return new TheoryDataSet<int, int, int, int>
{
{ 0, 9, 20, 10 },
{ 8, 8, 10, 1 },
{ 0, 19, 20, 20 },
{ 0, 29, 40, 30 },
{ 0, 29, 20, 20 },
{ 19, 29, 20, 1 },
};
}
}
// from, to, innerLength, effectiveLength for reads limited by byte[] size.
public static TheoryDataSet<int, int, int, int> ReadBoundsDataWithLimit
{
get
{
return new TheoryDataSet<int, int, int, int>
{
{ 0, 9, 20, 10 },
{ 8, 8, 10, 1 },
{ 0, 19, 20, 20 },
{ 0, 29, 40, 25 },
{ 0, 29, 20, 20 },
{ 19, 29, 20, 1 },
};
}
}
[Fact]
public void Ctor_ThrowsOnNullInnerStream()
{
RangeItemHeaderValue range = new RangeItemHeaderValue(0, 10);
var range = new RangeItemHeaderValue(0, 10);
Assert.ThrowsArgumentNull(() => new ByteRangeStream(innerStream: null, range: range), "innerStream");
}
@ -27,9 +82,9 @@ namespace System.Net.Http.Internal
public void Ctor_ThrowsIfCantSeekInnerStream()
{
// Arrange
Mock<Stream> mockInnerStream = new Mock<Stream>();
var mockInnerStream = new Mock<Stream>();
mockInnerStream.Setup(s => s.CanSeek).Returns(false);
RangeItemHeaderValue range = new RangeItemHeaderValue(0, 10);
var range = new RangeItemHeaderValue(0, 10);
// Act/Assert
Assert.ThrowsArgument(() => new ByteRangeStream(mockInnerStream.Object, range), "innerStream");
@ -39,10 +94,10 @@ namespace System.Net.Http.Internal
public void Ctor_ThrowsIfLowerRangeExceedsInnerStream()
{
// Arrange
Mock<Stream> mockInnerStream = new Mock<Stream>();
var mockInnerStream = new Mock<Stream>();
mockInnerStream.Setup(s => s.CanSeek).Returns(true);
mockInnerStream.Setup(s => s.Length).Returns(5);
RangeItemHeaderValue range = new RangeItemHeaderValue(10, 20);
var range = new RangeItemHeaderValue(10, 20);
// Act/Assert
Assert.ThrowsArgumentOutOfRange(() => new ByteRangeStream(mockInnerStream.Object, range), "range",
@ -53,17 +108,18 @@ namespace System.Net.Http.Internal
public void Ctor_SetsContentRange()
{
// Arrange
ContentRangeHeaderValue expectedContentRange = new ContentRangeHeaderValue(5, 9, 20);
Mock<Stream> mockInnerStream = new Mock<Stream>();
var expectedContentRange = new ContentRangeHeaderValue(5, 9, 20);
var mockInnerStream = new Mock<Stream>();
mockInnerStream.Setup(s => s.CanSeek).Returns(true);
mockInnerStream.Setup(s => s.Length).Returns(20);
RangeItemHeaderValue range = new RangeItemHeaderValue(5, 9);
var range = new RangeItemHeaderValue(5, 9);
// Act
ByteRangeStream rangeStream = new ByteRangeStream(mockInnerStream.Object, range);
// Assert
Assert.Equal(expectedContentRange, rangeStream.ContentRange);
using (var rangeStream = new ByteRangeStream(mockInnerStream.Object, range))
{
// Assert
Assert.Equal(expectedContentRange, rangeStream.ContentRange);
}
}
[Theory]
@ -72,87 +128,462 @@ namespace System.Net.Http.Internal
public void Ctor_ThrowsIfInnerStreamLengthIsLessThanOne(int innerLength)
{
// Arrange
Mock<Stream> mockInnerStream = new Mock<Stream>();
var mockInnerStream = new Mock<Stream>();
mockInnerStream.Setup(s => s.CanSeek).Returns(true);
mockInnerStream.Setup(s => s.Length).Returns(innerLength);
RangeItemHeaderValue range = new RangeItemHeaderValue(null, 0);
var range = new RangeItemHeaderValue(null, 0);
// Act/Assert
Assert.ThrowsArgumentOutOfRange(() => new ByteRangeStream(mockInnerStream.Object, range), "innerStream",
"The stream over which 'ByteRangeStream' provides a range view must have a length greater than or equal to 1.",
false, innerLength);
Assert.ThrowsArgumentOutOfRange(
() => new ByteRangeStream(mockInnerStream.Object, range),
"innerStream",
"The stream over which 'ByteRangeStream' provides a range view must have a length greater than or " +
"equal to 1.",
false,
innerLength);
}
[Theory]
[InlineData(0, 9, 20, 10)]
[InlineData(8, 8, 10, 1)]
[InlineData(0, 19, 20, 20)]
[PropertyData("ReadBoundsData")]
public void Ctor_SetsLength(int from, int to, int innerLength, int expectedLength)
{
// Arrange
Mock<Stream> mockInnerStream = new Mock<Stream>();
var mockInnerStream = new Mock<Stream>();
mockInnerStream.Setup(s => s.CanSeek).Returns(true);
mockInnerStream.Setup(s => s.Length).Returns(innerLength);
RangeItemHeaderValue range = new RangeItemHeaderValue(from, to);
var range = new RangeItemHeaderValue(from, to);
// Act
ByteRangeStream rangeStream = new ByteRangeStream(mockInnerStream.Object, range);
// Assert
Assert.Equal(expectedLength, rangeStream.Length);
using (var rangeStream = new ByteRangeStream(mockInnerStream.Object, range))
{
// Assert
Assert.Equal(expectedLength, rangeStream.Length);
}
}
[Theory]
[InlineData(0, 9, 20, 10)]
[InlineData(8, 8, 10, 1)]
[InlineData(0, 19, 20, 20)]
[InlineData(0, 29, 40, 25)]
[InlineData(0, 29, 20, 20)]
[InlineData(19, 29, 20, 1)]
[PropertyData("CopyBoundsData")]
public async Task CopyTo_ReadsSpecifiedRange(long? from, long? to, string expectedText)
{
// Arrange
var originalText = "This is the whole text.";
var range = new RangeItemHeaderValue(from, to);
using (var innerStream = new MemoryStream())
using (var writer = new StreamWriter(innerStream))
using (var targetStream = new MemoryStream())
using (var reader = new StreamReader(targetStream))
{
await writer.WriteAsync(originalText);
await writer.FlushAsync();
// Act
using (var rangeStream = new ByteRangeStream(innerStream, range))
{
rangeStream.CopyTo(targetStream);
}
// Assert
targetStream.Position = 0L;
var text = await reader.ReadToEndAsync();
Assert.Equal(expectedText, text);
}
}
[Theory]
[PropertyData("CopyBoundsData")]
public async Task CopyToAsync_ReadsSpecifiedRange(long? from, long? to, string expectedText)
{
// Arrange
var originalText = "This is the whole text.";
var range = new RangeItemHeaderValue(from, to);
using (var innerStream = new MemoryStream())
using (var writer = new StreamWriter(innerStream))
using (var targetStream = new MemoryStream())
using (var reader = new StreamReader(targetStream))
{
await writer.WriteAsync(originalText);
await writer.FlushAsync();
// Act
using (var rangeStream = new ByteRangeStream(innerStream, range))
{
await rangeStream.CopyToAsync(targetStream);
}
// Assert
targetStream.Position = 0L;
var text = await reader.ReadToEndAsync();
Assert.Equal(expectedText, text);
}
}
[Fact]
public void Position_ThrowsOnNegativeValue()
{
// Arrange
var mockInnerStream = new Mock<Stream>();
mockInnerStream.Setup(s => s.CanSeek).Returns(true);
mockInnerStream.Setup(s => s.Length).Returns(10L);
var range = new RangeItemHeaderValue(0, 25L);
using (var rangeStream = new ByteRangeStream(mockInnerStream.Object, range))
{
// Act & Assert
Assert.Throws<ArgumentOutOfRangeException>(() => rangeStream.Position = -1L);
}
}
[Theory]
[InlineData(null)]
[InlineData(0L)]
[InlineData(7L)]
[InlineData(9L)]
public void Position_ReturnsZeroInitially(long? from)
{
// Arrange
var mockInnerStream = new Mock<Stream>();
mockInnerStream.Setup(s => s.CanSeek).Returns(true);
mockInnerStream.Setup(s => s.Length).Returns(10L);
var range = new RangeItemHeaderValue(from, 25L);
using (var rangeStream = new ByteRangeStream(mockInnerStream.Object, range))
{
// Act
var position = rangeStream.Position;
// Assert
Assert.Equal(0L, position);
}
}
[Fact]
public void Position_CanBeSetAfterLength()
{
// Arrange
var expectedPosition = 300L;
var mockInnerStream = new Mock<Stream>();
mockInnerStream.Setup(s => s.CanSeek).Returns(true);
mockInnerStream.Setup(s => s.Length).Returns(10L);
var range = new RangeItemHeaderValue(0L, 10L);
using (var rangeStream = new ByteRangeStream(mockInnerStream.Object, range))
{
// Act
rangeStream.Position = expectedPosition;
// Assert
Assert.Equal(expectedPosition, rangeStream.Position);
}
}
[Fact]
public async Task Position_PositionsNextRead()
{
// Arrange
var originalText = "890123456789";
var range = new RangeItemHeaderValue(2L, null);
using (var innerStream = new MemoryStream())
using (var writer = new StreamWriter(innerStream))
{
await writer.WriteAsync(originalText);
await writer.FlushAsync();
using (var rangeStream = new ByteRangeStream(innerStream, range))
{
// Act
rangeStream.Position = 5L;
// Assert
var read = rangeStream.ReadByte();
Assert.Equal('5', (char)read);
}
}
}
[Theory]
[PropertyData("ReadBoundsDataWithLimit")]
public void BeginRead_ReadsEffectiveLengthBytes(int from, int to, int innerLength, int effectiveLength)
{
// Arrange
var mockInnerStream = new Mock<Stream>();
mockInnerStream.Setup(s => s.CanSeek).Returns(true);
mockInnerStream.Setup(s => s.Length).Returns(innerLength);
var range = new RangeItemHeaderValue(from, to);
var data = new byte[25];
var offset = 5;
var callback = new AsyncCallback(_ => { });
var userState = new object();
using (var rangeStream = new ByteRangeStream(mockInnerStream.Object, range))
{
// Act
var result = rangeStream.BeginRead(data, offset, data.Length, callback, userState);
rangeStream.EndRead(result);
// Assert
mockInnerStream.Verify(
s => s.BeginRead(data, offset, effectiveLength, callback, userState),
Times.Once());
Assert.Equal(effectiveLength, rangeStream.Position);
}
}
[Fact]
public async Task BeginRead_CanReadAfterLength()
{
// Arrange
var originalText = "This is the whole text.";
var range = new RangeItemHeaderValue(0L, null);
var data = new byte[25];
var callback = new AsyncCallback(_ => { });
var userState = new object();
using (var innerStream = new MemoryStream())
using (var writer = new StreamWriter(innerStream))
{
await writer.WriteAsync(originalText);
await writer.FlushAsync();
using (var rangeStream = new ByteRangeStream(innerStream, range))
{
rangeStream.Position = 50L;
// Act
var result = rangeStream.BeginRead(data, 0, data.Length, callback, userState);
var read = rangeStream.EndRead(result);
// Assert
Assert.Equal(0, read);
}
}
}
[Theory]
[PropertyData("ReadBoundsDataWithLimit")]
public void Read_ReadsEffectiveLengthBytes(int from, int to, int innerLength, int effectiveLength)
{
// Arrange
Mock<Stream> mockInnerStream = new Mock<Stream>();
var mockInnerStream = new Mock<Stream>();
mockInnerStream.Setup(s => s.CanSeek).Returns(true);
mockInnerStream.Setup(s => s.Length).Returns(innerLength);
RangeItemHeaderValue range = new RangeItemHeaderValue(from, to);
byte[] data = new byte[25];
int offset = 5;
var range = new RangeItemHeaderValue(from, to);
var data = new byte[25];
var offset = 5;
// Act
ByteRangeStream rangeStream = new ByteRangeStream(mockInnerStream.Object, range);
rangeStream.Read(data, offset, data.Length);
using (var rangeStream = new ByteRangeStream(mockInnerStream.Object, range))
{
// Act
rangeStream.Read(data, offset, data.Length);
// Assert
mockInnerStream.Verify(s => s.Read(data, offset, effectiveLength), Times.Once());
// Assert
mockInnerStream.Verify(s => s.Read(data, offset, effectiveLength), Times.Once());
Assert.Equal(effectiveLength, rangeStream.Position);
}
}
[Fact]
public async Task Read_CanReadAfterLength()
{
// Arrange
var originalText = "This is the whole text.";
var range = new RangeItemHeaderValue(0L, null);
var data = new byte[25];
using (var innerStream = new MemoryStream())
using (var writer = new StreamWriter(innerStream))
{
await writer.WriteAsync(originalText);
await writer.FlushAsync();
using (var rangeStream = new ByteRangeStream(innerStream, range))
{
rangeStream.Position = 50L;
// Act
var read = rangeStream.Read(data, 0, data.Length);
// Assert
Assert.Equal(0, read);
}
}
}
[Theory]
[InlineData(0, 9, 20, 10)]
[InlineData(8, 8, 10, 1)]
[InlineData(0, 19, 20, 20)]
[InlineData(0, 29, 40, 30)]
[InlineData(0, 29, 20, 20)]
[InlineData(19, 29, 20, 1)]
[PropertyData("ReadBoundsDataWithLimit")]
public async Task ReadAsync_ReadsEffectiveLengthBytes(int from, int to, int innerLength, int effectiveLength)
{
// Arrange
var mockInnerStream = new Mock<Stream>();
mockInnerStream.Setup(s => s.CanSeek).Returns(true);
mockInnerStream.Setup(s => s.Length).Returns(innerLength);
var range = new RangeItemHeaderValue(from, to);
var data = new byte[25];
var offset = 5;
using (var rangeStream = new ByteRangeStream(mockInnerStream.Object, range))
{
// Act
await rangeStream.ReadAsync(data, offset, data.Length);
// Assert
mockInnerStream.Verify(
s => s.ReadAsync(data, offset, effectiveLength, CancellationToken.None),
Times.Once());
Assert.Equal(effectiveLength, rangeStream.Position);
}
}
[Fact]
public async Task ReadAsync_CanReadAfterLength()
{
// Arrange
var originalText = "This is the whole text.";
var range = new RangeItemHeaderValue(0L, null);
var data = new byte[25];
using (var innerStream = new MemoryStream())
using (var writer = new StreamWriter(innerStream))
{
await writer.WriteAsync(originalText);
await writer.FlushAsync();
using (var rangeStream = new ByteRangeStream(innerStream, range))
{
rangeStream.Position = 50L;
// Act
var read = await rangeStream.ReadAsync(data, 0, data.Length);
// Assert
Assert.Equal(0, read);
}
}
}
[Theory]
[PropertyData("ReadBoundsData")]
public void ReadByte_ReadsEffectiveLengthTimes(int from, int to, int innerLength, int effectiveLength)
{
// Arrange
Mock<Stream> mockInnerStream = new Mock<Stream>();
var mockInnerStream = new Mock<Stream>();
mockInnerStream.Setup(s => s.CanSeek).Returns(true);
mockInnerStream.Setup(s => s.Length).Returns(innerLength);
RangeItemHeaderValue range = new RangeItemHeaderValue(from, to);
var range = new RangeItemHeaderValue(from, to);
var counter = 0;
// Act
ByteRangeStream rangeStream = new ByteRangeStream(mockInnerStream.Object, range);
int counter = 0;
while (rangeStream.ReadByte() != -1)
using (var rangeStream = new ByteRangeStream(mockInnerStream.Object, range))
{
counter++;
}
// Act
while (rangeStream.ReadByte() != -1)
{
counter++;
}
// Assert
Assert.Equal(effectiveLength, counter);
mockInnerStream.Verify(s => s.ReadByte(), Times.Exactly(effectiveLength));
// Assert
mockInnerStream.Verify(s => s.ReadByte(), Times.Exactly(effectiveLength));
Assert.Equal(effectiveLength, counter);
Assert.Equal(effectiveLength, rangeStream.Position);
}
}
[Fact]
public async Task ReadByte_CanReadAfterLength()
{
// Arrange
var originalText = "This is the whole text.";
var range = new RangeItemHeaderValue(0L, null);
using (var innerStream = new MemoryStream())
using (var writer = new StreamWriter(innerStream))
{
await writer.WriteAsync(originalText);
await writer.FlushAsync();
using (var rangeStream = new ByteRangeStream(innerStream, range))
{
rangeStream.Position = 50L;
// Act
var read = rangeStream.ReadByte();
// Assert
Assert.Equal(-1, read);
}
}
}
[Theory]
[InlineData(-1, SeekOrigin.Begin)]
[InlineData(-1, SeekOrigin.Current)]
[InlineData(-11, SeekOrigin.End)]
public void Seek_ThrowsIfBeforeOrigin(int offset, SeekOrigin origin)
{
// Arrange
var mockInnerStream = new Mock<Stream>();
mockInnerStream.Setup(s => s.CanSeek).Returns(true);
mockInnerStream.Setup(s => s.Length).Returns(10L);
var range = new RangeItemHeaderValue(0, 25L);
using (var rangeStream = new ByteRangeStream(mockInnerStream.Object, range))
{
// Act & Assert
Assert.Throws<IOException>(() => rangeStream.Seek(offset, origin));
}
}
[Theory]
[InlineData(25, SeekOrigin.Begin)]
[InlineData(25, SeekOrigin.Current)]
[InlineData(15, SeekOrigin.End)]
public void Seek_CanMoveAfterLength(int offset, SeekOrigin origin)
{
// Arrange
var expectedPosition = 25L;
var mockInnerStream = new Mock<Stream>();
mockInnerStream.Setup(s => s.CanSeek).Returns(true);
mockInnerStream.Setup(s => s.Length).Returns(10L);
var range = new RangeItemHeaderValue(0L, 10L);
using (var rangeStream = new ByteRangeStream(mockInnerStream.Object, range))
{
// Act
var newPosition = rangeStream.Seek(offset, origin);
// Assert
Assert.Equal(expectedPosition, newPosition);
Assert.Equal(expectedPosition, rangeStream.Position);
}
}
[Theory]
[InlineData(5, SeekOrigin.Begin)]
[InlineData(5, SeekOrigin.Current)]
[InlineData(-5, SeekOrigin.End)]
public async Task Seek_PositionsNextRead(int offset, SeekOrigin origin)
{
// Arrange
var originalText = "890123456789";
var range = new RangeItemHeaderValue(2L, null);
using (var innerStream = new MemoryStream())
using (var writer = new StreamWriter(innerStream))
{
await writer.WriteAsync(originalText);
await writer.FlushAsync();
using (var rangeStream = new ByteRangeStream(innerStream, range))
{
// Act
rangeStream.Seek(offset, origin);
// Assert
var read = rangeStream.ReadByte();
Assert.Equal('5', (char)read);
}
}
}
}
}