aspnetcore/test/Microsoft.AspNetCore.Server.../MemoryPoolIteratorTests.cs

406 lines
16 KiB
C#

// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.IO.Pipelines;
using System.Linq;
using System.Numerics;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Server.Kestrel.Internal.Http;
using Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure;
using Xunit;
using MemoryPool = Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure.MemoryPool;
using MemoryPoolBlock = Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure.MemoryPoolBlock;
namespace Microsoft.AspNetCore.Server.KestrelTests
{
public class MemoryPoolIteratorTests : IDisposable
{
private readonly MemoryPool _pool;
public MemoryPoolIteratorTests()
{
_pool = new MemoryPool();
}
public void Dispose()
{
_pool.Dispose();
}
[Fact]
public void Put()
{
var blocks = new MemoryPoolBlock[4];
for (var i = 0; i < 4; ++i)
{
blocks[i] = _pool.Lease();
blocks[i].End += 16;
for (var j = 0; j < blocks.Length; ++j)
{
blocks[i].Array[blocks[i].Start + j] = 0x00;
}
if (i != 0)
{
blocks[i - 1].Next = blocks[i];
}
}
// put FF at first block's head
var head = blocks[0].GetIterator();
Assert.True(head.Put(0xFF));
// data is put at correct position
Assert.Equal(0xFF, blocks[0].Array[blocks[0].Start]);
Assert.Equal(0x00, blocks[0].Array[blocks[0].Start + 1]);
// iterator is moved to next byte after put
Assert.Equal(1, head.Index - blocks[0].Start);
for (var i = 0; i < 14; ++i)
{
// move itr to the end of the block 0
head.Take();
}
// write to the end of block 0
Assert.True(head.Put(0xFE));
Assert.Equal(0xFE, blocks[0].Array[blocks[0].End - 1]);
Assert.Equal(0x00, blocks[1].Array[blocks[1].Start]);
// put data across the block link
Assert.True(head.Put(0xFD));
Assert.Equal(0xFD, blocks[1].Array[blocks[1].Start]);
Assert.Equal(0x00, blocks[1].Array[blocks[1].Start + 1]);
// paint every block
head = blocks[0].GetIterator();
for (var i = 0; i < 64; ++i)
{
Assert.True(head.Put((byte)i), $"Fail to put data at {i}.");
}
// Can't put anything by the end
Assert.ThrowsAny<InvalidOperationException>(() => head.Put(0xFF));
for (var i = 0; i < 4; ++i)
{
_pool.Return(blocks[i]);
}
}
[Fact]
public async Task PeekArraySegment()
{
using (var pipeFactory = new PipeFactory())
{
// Arrange
var pipe = pipeFactory.Create();
var buffer = pipe.Writer.Alloc();
buffer.Append(ReadableBuffer.Create(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7 }));
await buffer.FlushAsync();
// Act
var result = await pipe.Reader.PeekAsync();
// Assert
Assert.Equal(new byte[] {0, 1, 2, 3, 4, 5, 6, 7}, result);
pipe.Writer.Complete();
pipe.Reader.Complete();
}
}
[Fact]
public async Task PeekArraySegmentAtEndOfDataReturnsDefaultArraySegment()
{
using (var pipeFactory = new PipeFactory())
{
// Arrange
var pipe = pipeFactory.Create();
pipe.Writer.Complete();
// Act
var result = await pipe.Reader.PeekAsync();
// Assert
// Assert.Equals doesn't work since xunit tries to access the underlying array.
Assert.True(default(ArraySegment<byte>).Equals(result));
pipe.Reader.Complete();
}
}
[Fact]
public async Task PeekArraySegmentAtBlockBoundary()
{
using (var pipeFactory = new PipeFactory())
{
var pipe = pipeFactory.Create();
var buffer = pipe.Writer.Alloc();
buffer.Append(ReadableBuffer.Create(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7 }));
buffer.Append(ReadableBuffer.Create(new byte[] { 8, 9, 10, 11, 12, 13, 14, 15 }));
await buffer.FlushAsync();
// Act
var result = await pipe.Reader.PeekAsync();
// Assert
Assert.Equal(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7 }, result);
// Act
// Advance past the data in the first block
var readResult = pipe.Reader.ReadAsync().GetAwaiter().GetResult();
pipe.Reader.Advance(readResult.Buffer.Move(readResult.Buffer.Start, 8));
result = await pipe.Reader.PeekAsync();
// Assert
Assert.Equal(new byte[] { 8, 9, 10, 11, 12, 13, 14, 15 }, result);
pipe.Writer.Complete();
pipe.Reader.Complete();
}
}
[Fact]
public void EmptyIteratorBehaviourIsValid()
{
const byte byteCr = (byte)'\n';
var end = default(MemoryPoolIterator);
Assert.True(default(MemoryPoolIterator).IsDefault);
Assert.True(default(MemoryPoolIterator).IsEnd);
default(MemoryPoolIterator).CopyFrom(default(ArraySegment<byte>));
default(MemoryPoolIterator).CopyFromAscii("");
Assert.ThrowsAny<InvalidOperationException>(() => default(MemoryPoolIterator).Put(byteCr));
Assert.ThrowsAny<InvalidOperationException>(() => default(MemoryPoolIterator).GetLength(end));
}
[Theory]
[InlineData("a", "a", 1)]
[InlineData("ab", "a...", 1)]
[InlineData("abcde", "abcde", 5)]
[InlineData("abcde", "abcd...", 4)]
[InlineData("abcde", "abcde", 6)]
public void TestGetAsciiStringEscaped(string input, string expected, int maxChars)
{
// Arrange
var buffer = new Span<byte>(Encoding.ASCII.GetBytes(input));
// Act
var result = buffer.GetAsciiStringEscaped(maxChars);
// Assert
Assert.Equal(expected, result);
}
[Fact]
public void CorrectContentLengthsOutput()
{
using (var pool = new MemoryPool())
{
var block = pool.Lease();
try
{
for (var i = 0u; i <= 9u; i++)
{
block.Reset();
var iter = new MemoryPoolIterator(block);
iter.CopyFromNumeric(i);
Assert.Equal(block.Array[block.Start], (byte)(i + '0'));
Assert.Equal(block.End, block.Start + 1);
Assert.Equal(iter.Index, block.End);
}
for (var i = 10u; i <= 99u; i++)
{
block.Reset();
var iter = new MemoryPoolIterator(block);
iter.CopyFromNumeric(i);
Assert.Equal(block.Array[block.Start], (byte)((i / 10) + '0'));
Assert.Equal(block.Array[block.Start + 1], (byte)((i % 10) + '0'));
Assert.Equal(block.End, block.Start + 2);
Assert.Equal(iter.Index, block.End);
}
for (var i = 100u; i <= 999u; i++)
{
block.Reset();
var iter = new MemoryPoolIterator(block);
iter.CopyFromNumeric(i);
Assert.Equal(block.Array[block.Start], (byte)((i / 100) + '0'));
Assert.Equal(block.Array[block.Start + 1], (byte)(((i % 100) / 10) + '0'));
Assert.Equal(block.Array[block.Start + 2], (byte)((i % 10) + '0'));
Assert.Equal(block.End, block.Start + 3);
Assert.Equal(iter.Index, block.End);
}
for (var i = 1000u; i <= 9999u; i++)
{
block.Reset();
var iter = new MemoryPoolIterator(block);
iter.CopyFromNumeric(i);
Assert.Equal(block.Array[block.Start], (byte)((i / 1000) + '0'));
Assert.Equal(block.Array[block.Start + 1], (byte)(((i % 1000) / 100) + '0'));
Assert.Equal(block.Array[block.Start + 2], (byte)(((i % 100) / 10) + '0'));
Assert.Equal(block.Array[block.Start + 3], (byte)((i % 10) + '0'));
Assert.Equal(block.End, block.Start + 4);
Assert.Equal(iter.Index, block.End);
}
{
block.Reset();
var iter = new MemoryPoolIterator(block);
iter.CopyFromNumeric(ulong.MaxValue);
var outputBytes = Encoding.ASCII.GetBytes(ulong.MaxValue.ToString("0"));
for (var i = 0; i < outputBytes.Length; i++)
{
Assert.Equal(block.Array[block.Start + i], outputBytes[i]);
}
Assert.Equal(block.End, block.Start + outputBytes.Length);
Assert.Equal(iter.Index, block.End);
}
}
finally
{
pool.Return(block);
}
}
}
public static IEnumerable<object[]> SeekByteLimitData
{
get
{
var vectorSpan = Vector<byte>.Count;
// string input, char seek, int limit, int expectedBytesScanned, int expectedReturnValue
var data = new List<object[]>();
// Non-vector inputs
data.Add(new object[] { "hello, world", 'h', 12, 1, 'h' });
data.Add(new object[] { "hello, world", ' ', 12, 7, ' ' });
data.Add(new object[] { "hello, world", 'd', 12, 12, 'd' });
data.Add(new object[] { "hello, world", '!', 12, 12, -1 });
data.Add(new object[] { "hello, world", 'h', 13, 1, 'h' });
data.Add(new object[] { "hello, world", ' ', 13, 7, ' ' });
data.Add(new object[] { "hello, world", 'd', 13, 12, 'd' });
data.Add(new object[] { "hello, world", '!', 13, 12, -1 });
data.Add(new object[] { "hello, world", 'h', 5, 1, 'h' });
data.Add(new object[] { "hello, world", 'o', 5, 5, 'o' });
data.Add(new object[] { "hello, world", ',', 5, 5, -1 });
data.Add(new object[] { "hello, world", 'd', 5, 5, -1 });
data.Add(new object[] { "abba", 'a', 4, 1, 'a' });
data.Add(new object[] { "abba", 'b', 4, 2, 'b' });
// Vector inputs
// Single vector, no seek char in input, expect failure
data.Add(new object[] { new string('a', vectorSpan), 'b', vectorSpan, vectorSpan, -1 });
// Two vectors, no seek char in input, expect failure
data.Add(new object[] { new string('a', vectorSpan * 2), 'b', vectorSpan * 2, vectorSpan * 2, -1 });
// Two vectors plus non vector length (thus hitting slow path too), no seek char in input, expect failure
data.Add(new object[] { new string('a', vectorSpan * 2 + vectorSpan / 2), 'b', vectorSpan * 2 + vectorSpan / 2, vectorSpan * 2 + vectorSpan / 2, -1 });
// For each input length from 1/2 to 3 1/2 vector spans in 1/2 vector span increments...
for (var length = vectorSpan / 2; length <= vectorSpan * 3 + vectorSpan / 2; length += vectorSpan / 2)
{
// ...place the seek char at vector and input boundaries...
for (var i = Math.Min(vectorSpan - 1, length - 1); i < length; i += ((i + 1) % vectorSpan == 0) ? 1 : Math.Min(i + (vectorSpan - 1), length - 1))
{
var input = new StringBuilder(new string('a', length));
input[i] = 'b';
// ...and check with a seek byte limit before, at, and past the seek char position...
for (var limitOffset = -1; limitOffset <= 1; limitOffset++)
{
var limit = (i + 1) + limitOffset;
if (limit >= i + 1)
{
// ...that Seek() succeeds when the seek char is within that limit...
data.Add(new object[] { input.ToString(), 'b', limit, i + 1, 'b' });
}
else
{
// ...and fails when it's not.
data.Add(new object[] { input.ToString(), 'b', limit, Math.Min(length, limit), -1 });
}
}
}
}
return data;
}
}
public static IEnumerable<object[]> SeekIteratorLimitData
{
get
{
var vectorSpan = Vector<byte>.Count;
// string input, char seek, char limitAt, int expectedReturnValue
var data = new List<object[]>();
// Non-vector inputs
data.Add(new object[] { "hello, world", 'h', 'd', 'h' });
data.Add(new object[] { "hello, world", ' ', 'd', ' ' });
data.Add(new object[] { "hello, world", 'd', 'd', 'd' });
data.Add(new object[] { "hello, world", '!', 'd', -1 });
data.Add(new object[] { "hello, world", 'h', 'w', 'h' });
data.Add(new object[] { "hello, world", 'o', 'w', 'o' });
data.Add(new object[] { "hello, world", 'r', 'w', -1 });
data.Add(new object[] { "hello, world", 'd', 'w', -1 });
// Vector inputs
// Single vector, no seek char in input, expect failure
data.Add(new object[] { new string('a', vectorSpan), 'b', 'b', -1 });
// Two vectors, no seek char in input, expect failure
data.Add(new object[] { new string('a', vectorSpan * 2), 'b', 'b', -1 });
// Two vectors plus non vector length (thus hitting slow path too), no seek char in input, expect failure
data.Add(new object[] { new string('a', vectorSpan * 2 + vectorSpan / 2), 'b', 'b', -1 });
// For each input length from 1/2 to 3 1/2 vector spans in 1/2 vector span increments...
for (var length = vectorSpan / 2; length <= vectorSpan * 3 + vectorSpan / 2; length += vectorSpan / 2)
{
// ...place the seek char at vector and input boundaries...
for (var i = Math.Min(vectorSpan - 1, length - 1); i < length; i += ((i + 1) % vectorSpan == 0) ? 1 : Math.Min(i + (vectorSpan - 1), length - 1))
{
var input = new StringBuilder(new string('a', length));
input[i] = 'b';
// ...along with sentinel characters to seek the limit iterator to...
input[i - 1] = 'A';
if (i < length - 1) input[i + 1] = 'B';
// ...and check that Seek() succeeds with a limit iterator at or past the seek char position...
data.Add(new object[] { input.ToString(), 'b', 'b', 'b' });
if (i < length - 1) data.Add(new object[] { input.ToString(), 'b', 'B', 'b' });
// ...and fails with a limit iterator before the seek char position.
data.Add(new object[] { input.ToString(), 'b', 'A', -1 });
}
}
return data;
}
}
}
}