diff --git a/Microsoft.Toolkit.HighPerformance/Extensions/ArrayPoolBufferWriterExtensions.cs b/Microsoft.Toolkit.HighPerformance/Extensions/ArrayPoolBufferWriterExtensions.cs new file mode 100644 index 00000000000..21026113957 --- /dev/null +++ b/Microsoft.Toolkit.HighPerformance/Extensions/ArrayPoolBufferWriterExtensions.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics.Contracts; +using System.IO; +using System.Runtime.CompilerServices; +using Microsoft.Toolkit.HighPerformance.Buffers; +using Microsoft.Toolkit.HighPerformance.Streams; +using Microsoft.Toolkit.HighPerformance.Streams.Sources; + +namespace Microsoft.Toolkit.HighPerformance.Extensions +{ + /// + /// Helpers for working with the type. + /// + public static class ArrayPoolBufferWriterExtensions + { + /// + /// Returns a that can be used to write to a target an of instance. + /// + /// The target instance. + /// A wrapping and writing data to its underlying buffer. + /// The returned can only be written to and does not support seeking. + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Stream AsStream(this ArrayPoolBufferWriter writer) + { + return new IBufferWriterStream(new ArrayBufferWriterOwner(writer)); + } + } +} diff --git a/Microsoft.Toolkit.HighPerformance/Extensions/IBufferWriterExtensions.cs b/Microsoft.Toolkit.HighPerformance/Extensions/IBufferWriterExtensions.cs index 67c30579beb..07a5bd6c2b8 100644 --- a/Microsoft.Toolkit.HighPerformance/Extensions/IBufferWriterExtensions.cs +++ b/Microsoft.Toolkit.HighPerformance/Extensions/IBufferWriterExtensions.cs @@ -4,8 +4,13 @@ using System; using System.Buffers; +using System.Diagnostics.Contracts; +using System.IO; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using Microsoft.Toolkit.HighPerformance.Buffers; +using Microsoft.Toolkit.HighPerformance.Streams; +using Microsoft.Toolkit.HighPerformance.Streams.Sources; namespace Microsoft.Toolkit.HighPerformance.Extensions { @@ -14,6 +19,28 @@ namespace Microsoft.Toolkit.HighPerformance.Extensions /// public static class IBufferWriterExtensions { + /// + /// Returns a that can be used to write to a target an of instance. + /// + /// The target instance. + /// A wrapping and writing data to its underlying buffer. + /// The returned can only be written to and does not support seeking. + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Stream AsStream(this IBufferWriter writer) + { + if (writer.GetType() == typeof(ArrayPoolBufferWriter)) + { + // If the input writer is of type ArrayPoolBufferWriter, we can use the type + // specific buffer writer owner to let the JIT elide callvirts when accessing it. + var internalWriter = Unsafe.As>(writer)!; + + return new IBufferWriterStream(new ArrayBufferWriterOwner(internalWriter)); + } + + return new IBufferWriterStream(new IBufferWriterOwner(writer)); + } + /// /// Writes a value of a specified type into a target instance. /// diff --git a/Microsoft.Toolkit.HighPerformance/Extensions/StreamExtensions.cs b/Microsoft.Toolkit.HighPerformance/Extensions/StreamExtensions.cs index 331786d086d..74526cb1f0f 100644 --- a/Microsoft.Toolkit.HighPerformance/Extensions/StreamExtensions.cs +++ b/Microsoft.Toolkit.HighPerformance/Extensions/StreamExtensions.cs @@ -167,24 +167,24 @@ public static void Write(this Stream stream, ReadOnlySpan buffer) /// The value read from . /// Thrown if reaches the end. #if SPAN_RUNTIME_SUPPORT - // Avoid inlining as we're renting a stack buffer, which - // cause issues if this method was called inside a loop - [MethodImpl(MethodImplOptions.NoInlining)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif public static T Read(this Stream stream) where T : unmanaged { #if SPAN_RUNTIME_SUPPORT - Span span = stackalloc byte[Unsafe.SizeOf()]; + T result = default; + int length = Unsafe.SizeOf(); - if (stream.Read(span) != span.Length) + unsafe { - ThrowInvalidOperationExceptionForEndOfStream(); + if (stream.Read(new Span(&result, length)) != length) + { + ThrowInvalidOperationExceptionForEndOfStream(); + } } - ref byte r0 = ref MemoryMarshal.GetReference(span); - - return Unsafe.ReadUnaligned(ref r0); + return result; #else int length = Unsafe.SizeOf(); byte[] buffer = ArrayPool.Shared.Rent(length); diff --git a/Microsoft.Toolkit.HighPerformance/Streams/IBufferWriterStream{TWriter}.Memory.cs b/Microsoft.Toolkit.HighPerformance/Streams/IBufferWriterStream{TWriter}.Memory.cs new file mode 100644 index 00000000000..f9b431fd904 --- /dev/null +++ b/Microsoft.Toolkit.HighPerformance/Streams/IBufferWriterStream{TWriter}.Memory.cs @@ -0,0 +1,76 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#if SPAN_RUNTIME_SUPPORT + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Toolkit.HighPerformance.Streams +{ + /// + internal sealed partial class IBufferWriterStream + { + /// + public override void CopyTo(Stream destination, int bufferSize) + { + throw MemoryStream.GetNotSupportedException(); + } + + /// + public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + { + throw MemoryStream.GetNotSupportedException(); + } + + /// + public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) + { + if (cancellationToken.IsCancellationRequested) + { + return new ValueTask(Task.FromCanceled(cancellationToken)); + } + + try + { + Write(buffer.Span); + + return default; + } + catch (OperationCanceledException e) + { + return new ValueTask(Task.FromCanceled(e.CancellationToken)); + } + catch (Exception e) + { + return new ValueTask(Task.FromException(e)); + } + } + + /// + public override int Read(Span buffer) + { + throw MemoryStream.GetNotSupportedException(); + } + + /// + public override void Write(ReadOnlySpan buffer) + { + MemoryStream.ValidateDisposed(this.disposed); + + Span destination = this.bufferWriter.GetSpan(buffer.Length); + + if (!buffer.TryCopyTo(destination)) + { + MemoryStream.ThrowArgumentExceptionForEndOfStreamOnWrite(); + } + + this.bufferWriter.Advance(buffer.Length); + } + } +} + +#endif diff --git a/Microsoft.Toolkit.HighPerformance/Streams/IBufferWriterStream{TWriter}.cs b/Microsoft.Toolkit.HighPerformance/Streams/IBufferWriterStream{TWriter}.cs new file mode 100644 index 00000000000..2be38aa9761 --- /dev/null +++ b/Microsoft.Toolkit.HighPerformance/Streams/IBufferWriterStream{TWriter}.cs @@ -0,0 +1,173 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Buffers; +using System.IO; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Toolkit.HighPerformance.Streams +{ + /// + /// A implementation wrapping an instance. + /// + /// The type of buffer writer to use. + internal sealed partial class IBufferWriterStream : Stream + where TWriter : struct, IBufferWriter + { + /// + /// The target instance to use. + /// + private readonly TWriter bufferWriter; + + /// + /// Indicates whether or not the current instance has been disposed + /// + private bool disposed; + + /// + /// Initializes a new instance of the class. + /// + /// The target instance to use. + public IBufferWriterStream(TWriter bufferWriter) + { + this.bufferWriter = bufferWriter; + } + + /// + public override bool CanRead => false; + + /// + public override bool CanSeek => false; + + /// + public override bool CanWrite + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => !this.disposed; + } + + /// + public override long Length => throw MemoryStream.GetNotSupportedException(); + + /// + public override long Position + { + get => throw MemoryStream.GetNotSupportedException(); + set => throw MemoryStream.GetNotSupportedException(); + } + + /// + public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) + { + throw MemoryStream.GetNotSupportedException(); + } + + /// + public override void Flush() + { + } + + /// + public override Task FlushAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + + return Task.CompletedTask; + } + + /// + public override Task ReadAsync(byte[]? buffer, int offset, int count, CancellationToken cancellationToken) + { + throw MemoryStream.GetNotSupportedException(); + } + + /// + public override Task WriteAsync(byte[]? buffer, int offset, int count, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + + try + { + Write(buffer, offset, count); + + return Task.CompletedTask; + } + catch (OperationCanceledException e) + { + return Task.FromCanceled(e.CancellationToken); + } + catch (Exception e) + { + return Task.FromException(e); + } + } + + /// + public override long Seek(long offset, SeekOrigin origin) + { + throw MemoryStream.GetNotSupportedException(); + } + + /// + public override void SetLength(long value) + { + throw MemoryStream.GetNotSupportedException(); + } + + /// + public override int Read(byte[]? buffer, int offset, int count) + { + throw MemoryStream.GetNotSupportedException(); + } + + /// + public override int ReadByte() + { + throw MemoryStream.GetNotSupportedException(); + } + + /// + public override void Write(byte[]? buffer, int offset, int count) + { + MemoryStream.ValidateDisposed(this.disposed); + MemoryStream.ValidateBuffer(buffer, offset, count); + + Span + source = buffer.AsSpan(offset, count), + destination = this.bufferWriter.GetSpan(count); + + if (!source.TryCopyTo(destination)) + { + MemoryStream.ThrowArgumentExceptionForEndOfStreamOnWrite(); + } + + this.bufferWriter.Advance(count); + } + + /// + public override void WriteByte(byte value) + { + MemoryStream.ValidateDisposed(this.disposed); + + this.bufferWriter.GetSpan(1)[0] = value; + + this.bufferWriter.Advance(1); + } + + /// + protected override void Dispose(bool disposing) + { + this.disposed = true; + } + } +} diff --git a/Microsoft.Toolkit.HighPerformance/Streams/MemoryStream.ThrowExceptions.cs b/Microsoft.Toolkit.HighPerformance/Streams/MemoryStream.ThrowExceptions.cs index cdfc24112d5..2d316df2242 100644 --- a/Microsoft.Toolkit.HighPerformance/Streams/MemoryStream.ThrowExceptions.cs +++ b/Microsoft.Toolkit.HighPerformance/Streams/MemoryStream.ThrowExceptions.cs @@ -13,19 +13,28 @@ namespace Microsoft.Toolkit.HighPerformance.Streams internal static partial class MemoryStream { /// - /// Throws an when trying to write too many bytes to the target stream. + /// Gets a standard instance for a stream. /// - public static void ThrowArgumentExceptionForEndOfStreamOnWrite() + /// A with the standard text. + public static Exception GetNotSupportedException() { - throw new ArgumentException("The current stream can't contain the requested input data."); + return new NotSupportedException("The requested operation is not supported for this stream."); + } + + /// + /// Throws a when trying to perform a not supported operation. + /// + public static void ThrowNotSupportedException() + { + throw GetNotSupportedException(); } /// - /// Throws a when trying to set the length of the stream. + /// Throws an when trying to write too many bytes to the target stream. /// - public static void ThrowNotSupportedExceptionForSetLength() + public static void ThrowArgumentExceptionForEndOfStreamOnWrite() { - throw new NotSupportedException("Setting the length is not supported for this stream."); + throw new ArgumentException("The current stream can't contain the requested input data."); } /// @@ -77,14 +86,6 @@ private static void ThrowArgumentExceptionForLength() throw new ArgumentException("The sum of offset and count can't be larger than the buffer length.", "buffer"); } - /// - /// Throws a when trying to write on a readonly stream. - /// - private static void ThrowNotSupportedExceptionForCanWrite() - { - throw new NotSupportedException("The current stream doesn't support writing."); - } - /// /// Throws an when using a disposed instance. /// diff --git a/Microsoft.Toolkit.HighPerformance/Streams/MemoryStream.Validate.cs b/Microsoft.Toolkit.HighPerformance/Streams/MemoryStream.Validate.cs index 2f35380918f..2f83e4872d7 100644 --- a/Microsoft.Toolkit.HighPerformance/Streams/MemoryStream.Validate.cs +++ b/Microsoft.Toolkit.HighPerformance/Streams/MemoryStream.Validate.cs @@ -64,7 +64,7 @@ public static void ValidateCanWrite(bool canWrite) { if (!canWrite) { - ThrowNotSupportedExceptionForCanWrite(); + ThrowNotSupportedException(); } } diff --git a/Microsoft.Toolkit.HighPerformance/Streams/MemoryStream{TSource}.cs b/Microsoft.Toolkit.HighPerformance/Streams/MemoryStream{TSource}.cs index b0dcd8bc908..0243dbd60d5 100644 --- a/Microsoft.Toolkit.HighPerformance/Streams/MemoryStream{TSource}.cs +++ b/Microsoft.Toolkit.HighPerformance/Streams/MemoryStream{TSource}.cs @@ -220,7 +220,7 @@ public sealed override long Seek(long offset, SeekOrigin origin) /// public sealed override void SetLength(long value) { - MemoryStream.ThrowNotSupportedExceptionForSetLength(); + throw MemoryStream.GetNotSupportedException(); } /// diff --git a/Microsoft.Toolkit.HighPerformance/Streams/Sources/ArrayBufferWriterOwner.cs b/Microsoft.Toolkit.HighPerformance/Streams/Sources/ArrayBufferWriterOwner.cs new file mode 100644 index 00000000000..0a534dc2af6 --- /dev/null +++ b/Microsoft.Toolkit.HighPerformance/Streams/Sources/ArrayBufferWriterOwner.cs @@ -0,0 +1,52 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Buffers; +using System.Runtime.CompilerServices; +using Microsoft.Toolkit.HighPerformance.Buffers; + +namespace Microsoft.Toolkit.HighPerformance.Streams.Sources +{ + /// + /// An implementation wrapping an instance. + /// + internal readonly struct ArrayBufferWriterOwner : IBufferWriter + { + /// + /// The wrapped array. + /// + private readonly ArrayPoolBufferWriter writer; + + /// + /// Initializes a new instance of the struct. + /// + /// The wrapped instance. + public ArrayBufferWriterOwner(ArrayPoolBufferWriter writer) + { + this.writer = writer; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Advance(int count) + { + this.writer.Advance(count); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Memory GetMemory(int sizeHint = 0) + { + return this.writer.GetMemory(sizeHint); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Span GetSpan(int sizeHint = 0) + { + return this.writer.GetSpan(sizeHint); + } + } +} diff --git a/Microsoft.Toolkit.HighPerformance/Streams/Sources/IBufferWriterOwner.cs b/Microsoft.Toolkit.HighPerformance/Streams/Sources/IBufferWriterOwner.cs new file mode 100644 index 00000000000..2580037f6d9 --- /dev/null +++ b/Microsoft.Toolkit.HighPerformance/Streams/Sources/IBufferWriterOwner.cs @@ -0,0 +1,52 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Buffers; +using System.Runtime.CompilerServices; +using Microsoft.Toolkit.HighPerformance.Buffers; + +namespace Microsoft.Toolkit.HighPerformance.Streams.Sources +{ + /// + /// An implementation wrapping an instance. + /// + internal readonly struct IBufferWriterOwner : IBufferWriter + { + /// + /// The wrapped array. + /// + private readonly IBufferWriter writer; + + /// + /// Initializes a new instance of the struct. + /// + /// The wrapped instance. + public IBufferWriterOwner(IBufferWriter writer) + { + this.writer = writer; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Advance(int count) + { + this.writer.Advance(count); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Memory GetMemory(int sizeHint = 0) + { + return this.writer.GetMemory(sizeHint); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Span GetSpan(int sizeHint = 0) + { + return this.writer.GetSpan(sizeHint); + } + } +} diff --git a/UnitTests/UnitTests.HighPerformance.Shared/Buffers/Test_ArrayPoolBufferWriter{T}.cs b/UnitTests/UnitTests.HighPerformance.Shared/Buffers/Test_ArrayPoolBufferWriter{T}.cs index 3a481399422..a704aab1a64 100644 --- a/UnitTests/UnitTests.HighPerformance.Shared/Buffers/Test_ArrayPoolBufferWriter{T}.cs +++ b/UnitTests/UnitTests.HighPerformance.Shared/Buffers/Test_ArrayPoolBufferWriter{T}.cs @@ -188,28 +188,43 @@ public void Test_ArrayPoolBufferWriterOfT_MultipleDispose() [TestMethod] public void Test_ArrayPoolBufferWriterOfT_AsStream() { - var writer = new ArrayPoolBufferWriter(); - - Span data = Guid.NewGuid().ToByteArray(); + const int GuidSize = 16; - data.CopyTo(writer.GetSpan(data.Length)); + var writer = new ArrayPoolBufferWriter(); + var guid = Guid.NewGuid(); - writer.Advance(data.Length); + // Here we first get a stream with the extension targeting ArrayPoolBufferWriter. + // This will wrap it into a custom internal stream type and produce a write-only + // stream that essentially mirrors the IBufferWriter functionality as a stream. + using (Stream writeStream = writer.AsStream()) + { + writeStream.Write(guid); + } - Assert.AreEqual(writer.WrittenCount, data.Length); + Assert.AreEqual(writer.WrittenCount, GuidSize); - Stream stream = writer.AsStream(); + // Here we get a readable stream instead, and read from it to ensure + // the previous data was written correctly from the writeable stream. + using (Stream stream = writer.WrittenMemory.AsStream()) + { + Assert.AreEqual(stream.Length, GuidSize); - Assert.AreEqual(stream.Length, data.Length); + byte[] result = new byte[GuidSize]; - byte[] result = new byte[16]; + stream.Read(result, 0, result.Length); - stream.Read(result, 0, result.Length); + // Read the guid data and ensure it matches our initial guid + Assert.IsTrue(new Guid(result).Equals(guid)); + } - Assert.IsTrue(data.SequenceEqual(result)); + // Do a dummy write just to ensure the writer isn't disposed here. + // This is because we got a stream from a memory, not a memory owner. + writer.Write((byte)42); + writer.Advance(1); - stream.Dispose(); + writer.Dispose(); + // Now check that the writer is actually disposed instead Assert.ThrowsException(() => writer.Capacity); } } diff --git a/UnitTests/UnitTests.HighPerformance.Shared/Streams/Test_IBufferWriterStream.cs b/UnitTests/UnitTests.HighPerformance.Shared/Streams/Test_IBufferWriterStream.cs new file mode 100644 index 00000000000..bd3677c079c --- /dev/null +++ b/UnitTests/UnitTests.HighPerformance.Shared/Streams/Test_IBufferWriterStream.cs @@ -0,0 +1,161 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Buffers; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Threading.Tasks; +using Microsoft.Toolkit.HighPerformance.Buffers; +using Microsoft.Toolkit.HighPerformance.Extensions; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace UnitTests.HighPerformance.Streams +{ + [TestClass] + public class Test_IBufferWriterStream + { + [TestCategory("IBufferWriterStream")] + [TestMethod] + public void Test_IBufferWriterStream_Lifecycle() + { + ArrayPoolBufferWriter writer = new ArrayPoolBufferWriter(); + + // Get a stream from a buffer writer aand validate that it can only be written to. + // This is to mirror the same functionality as the IBufferWriter interface. + Stream stream = ((IBufferWriter)writer).AsStream(); + + Assert.IsFalse(stream.CanRead); + Assert.IsFalse(stream.CanSeek); + Assert.IsTrue(stream.CanWrite); + + Assert.ThrowsException(() => stream.Length); + Assert.ThrowsException(() => stream.Position); + + // Dispose the stream and check that no operation is now allowed + stream.Dispose(); + + Assert.IsFalse(stream.CanRead); + Assert.IsFalse(stream.CanSeek); + Assert.IsFalse(stream.CanWrite); + Assert.ThrowsException(() => stream.Length); + Assert.ThrowsException(() => stream.Position); + } + + [TestCategory("IBufferWriterStream")] + [TestMethod] + public void Test_IBufferWriterStream_Write_Array() + { + ArrayPoolBufferWriter writer = new ArrayPoolBufferWriter(); + Stream stream = ((IBufferWriter)writer).AsStream(); + + byte[] data = Test_MemoryStream.CreateRandomData(64); + + // Write random data to the stream wrapping the buffer writer, and validate + // that the state of the writer is consistent, and the written content matches. + stream.Write(data, 0, data.Length); + + Assert.AreEqual(writer.WrittenCount, data.Length); + Assert.IsTrue(writer.WrittenSpan.SequenceEqual(data)); + + // A few tests with invalid inputs (null buffers, invalid indices, etc.) + Assert.ThrowsException(() => stream.Write(null, 0, 10)); + Assert.ThrowsException(() => stream.Write(data, -1, 10)); + Assert.ThrowsException(() => stream.Write(data, 200, 10)); + Assert.ThrowsException(() => stream.Write(data, 0, -24)); + Assert.ThrowsException(() => stream.Write(data, 0, 200)); + + stream.Dispose(); + + Assert.ThrowsException(() => stream.Write(data, 0, data.Length)); + } + + [TestCategory("IBufferWriterStream")] + [TestMethod] + public async Task Test_IBufferWriterStream_WriteAsync_Array() + { + ArrayPoolBufferWriter writer = new ArrayPoolBufferWriter(); + Stream stream = ((IBufferWriter)writer).AsStream(); + + byte[] data = Test_MemoryStream.CreateRandomData(64); + + // Same test as above, but using an asynchronous write instead + await stream.WriteAsync(data, 0, data.Length); + + Assert.AreEqual(writer.WrittenCount, data.Length); + Assert.IsTrue(writer.WrittenSpan.SequenceEqual(data)); + + await Assert.ThrowsExceptionAsync(() => stream.WriteAsync(null, 0, 10)); + await Assert.ThrowsExceptionAsync(() => stream.WriteAsync(data, -1, 10)); + await Assert.ThrowsExceptionAsync(() => stream.WriteAsync(data, 200, 10)); + await Assert.ThrowsExceptionAsync(() => stream.WriteAsync(data, 0, -24)); + await Assert.ThrowsExceptionAsync(() => stream.WriteAsync(data, 0, 200)); + + stream.Dispose(); + + await Assert.ThrowsExceptionAsync(() => stream.WriteAsync(data, 0, data.Length)); + } + + [TestCategory("IBufferWriterStream")] + [TestMethod] + [SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1500", Justification = "Array initialization")] + public void Test_IBufferWriterStream_WriteByte() + { + ArrayPoolBufferWriter writer = new ArrayPoolBufferWriter(); + Stream stream = ((IBufferWriter)writer).AsStream(); + + ReadOnlySpan data = stackalloc byte[] { 1, 128, 255, 32 }; + + foreach (var item in data.Enumerate()) + { + // Since we're enumerating, we can also double check the current written count + // at each iteration, to ensure the writes are done correctly every time. + Assert.AreEqual(writer.WrittenCount, item.Index); + + // Write a number of bytes one by one to test this API as well + stream.WriteByte(item.Value); + } + + // Validate the final written length and actual data + Assert.AreEqual(writer.WrittenCount, data.Length); + Assert.IsTrue(data.SequenceEqual(writer.WrittenSpan)); + + Assert.ThrowsException(() => stream.ReadByte()); + } + + [TestCategory("IBufferWriterStream")] + [TestMethod] + public void Test_IBufferWriterStream_Write_Span() + { + ArrayPoolBufferWriter writer = new ArrayPoolBufferWriter(); + Stream stream = ((IBufferWriter)writer).AsStream(); + + Memory data = Test_MemoryStream.CreateRandomData(64); + + // This will use the extension when on .NET Standard 2.0, + // as the Stream class doesn't have Spam or Memory + // public APIs there. This is the case eg. on UWP as well. + stream.Write(data.Span); + + Assert.AreEqual(writer.WrittenCount, data.Length); + Assert.IsTrue(data.Span.SequenceEqual(writer.WrittenSpan)); + } + + [TestCategory("IBufferWriterStream")] + [TestMethod] + public async Task Test_IBufferWriterStream_WriteAsync_Memory() + { + ArrayPoolBufferWriter writer = new ArrayPoolBufferWriter(); + Stream stream = ((IBufferWriter)writer).AsStream(); + + Memory data = Test_MemoryStream.CreateRandomData(64); + + // Same as the other asynchronous test above, but writing from a Memory + await stream.WriteAsync(data); + + Assert.AreEqual(writer.WrittenCount, data.Length); + Assert.IsTrue(data.Span.SequenceEqual(writer.WrittenSpan)); + } + } +} diff --git a/UnitTests/UnitTests.HighPerformance.Shared/Streams/Test_MemoryStream.cs b/UnitTests/UnitTests.HighPerformance.Shared/Streams/Test_MemoryStream.cs index f2c63182c63..f3b09be2081 100644 --- a/UnitTests/UnitTests.HighPerformance.Shared/Streams/Test_MemoryStream.cs +++ b/UnitTests/UnitTests.HighPerformance.Shared/Streams/Test_MemoryStream.cs @@ -272,7 +272,7 @@ public async Task Test_MemoryStream_ReadWriteAsync_Memory() /// The number of array items to create. /// The returned random array. [Pure] - private static byte[] CreateRandomData(int count) + internal static byte[] CreateRandomData(int count) { var random = new Random(DateTime.Now.Ticks.GetHashCode()); diff --git a/UnitTests/UnitTests.HighPerformance.Shared/UnitTests.HighPerformance.Shared.projitems b/UnitTests/UnitTests.HighPerformance.Shared/UnitTests.HighPerformance.Shared.projitems index 6e776be38bb..a114963de54 100644 --- a/UnitTests/UnitTests.HighPerformance.Shared/UnitTests.HighPerformance.Shared.projitems +++ b/UnitTests/UnitTests.HighPerformance.Shared/UnitTests.HighPerformance.Shared.projitems @@ -47,6 +47,7 @@ +