diff --git a/src/Common/tests/System/IO/DelegateStream.cs b/src/Common/tests/System/IO/DelegateStream.cs index 29aa5559adc4..8af013cac961 100644 --- a/src/Common/tests/System/IO/DelegateStream.cs +++ b/src/Common/tests/System/IO/DelegateStream.cs @@ -52,16 +52,12 @@ public DelegateStream( _positionSetFunc = positionSetFunc ?? (_ => { throw new NotSupportedException(); }); _positionGetFunc = positionGetFunc ?? (() => { throw new NotSupportedException(); }); - if (readAsyncFunc != null && readFunc == null) - throw new InvalidOperationException("If reads are supported, must provide a synchronous read implementation"); _readFunc = readFunc; _readAsyncFunc = readAsyncFunc ?? ((buffer, offset, count, token) => base.ReadAsync(buffer, offset, count, token)); _seekFunc = seekFunc ?? ((_, __) => { throw new NotSupportedException(); }); _setLengthFunc = setLengthFunc ?? (_ => { throw new NotSupportedException(); }); - if (writeAsyncFunc != null && writeFunc == null) - throw new InvalidOperationException("If writes are supported, must provide a synchronous write implementation"); _writeFunc = writeFunc; _writeAsyncFunc = writeAsyncFunc ?? ((buffer, offset, count, token) => base.WriteAsync(buffer, offset, count, token)); } diff --git a/src/System.IO.FileSystem/tests/FileStream/ReadWriteSpan.netcoreapp.cs b/src/System.IO.FileSystem/tests/FileStream/ReadWriteSpan.netcoreapp.cs index 3a67c5c9d0c6..272b19769e1c 100644 --- a/src/System.IO.FileSystem/tests/FileStream/ReadWriteSpan.netcoreapp.cs +++ b/src/System.IO.FileSystem/tests/FileStream/ReadWriteSpan.netcoreapp.cs @@ -3,6 +3,8 @@ // See the LICENSE file in the project root for more information. using System.Linq; +using System.Threading; +using System.Threading.Tasks; using Xunit; namespace System.IO.Tests @@ -123,6 +125,119 @@ public void NonEmptyWrite_WritesExpectedData() Assert.Equal(TestBuffer, buffer); } } + + [Fact] + public void DisposedStream_ReadWriteAsync_Throws() + { + var fs = CreateFileStream(GetTestFilePath(), FileMode.Create); + fs.Dispose(); + Assert.Throws(() => { fs.ReadAsync(new Memory(new byte[1])); }); + Assert.Throws(() => { fs.WriteAsync(new ReadOnlyMemory(new byte[1])); }); + } + + [Fact] + public async Task EmptyFile_ReadAsync_Succeeds() + { + using (var fs = CreateFileStream(GetTestFilePath(), FileMode.Create)) + { + // use a recognizable pattern + var buffer = (byte[])TestBuffer.Clone(); + + Assert.Equal(0, await fs.ReadAsync(Memory.Empty)); + Assert.Equal(0, await fs.ReadAsync(new Memory(buffer, 0, 1))); + Assert.Equal(TestBuffer, buffer); + + Assert.Equal(0, await fs.ReadAsync(new Memory(buffer, 0, buffer.Length))); + Assert.Equal(TestBuffer, buffer); + + Assert.Equal(0, await fs.ReadAsync(new Memory(buffer, buffer.Length - 1, 1))); + Assert.Equal(TestBuffer, buffer); + + Assert.Equal(0, await fs.ReadAsync(new Memory(buffer, buffer.Length / 2, buffer.Length - buffer.Length / 2))); + Assert.Equal(TestBuffer, buffer); + } + } + + [Fact] + public async Task NonEmptyFile_ReadAsync_GetsExpectedData() + { + string fileName = GetTestFilePath(); + File.WriteAllBytes(fileName, TestBuffer); + + using (var fs = CreateFileStream(fileName, FileMode.Open)) + { + var buffer = new byte[TestBuffer.Length]; + Assert.Equal(TestBuffer.Length, await fs.ReadAsync(new Memory(buffer, 0, buffer.Length))); + Assert.Equal(TestBuffer, buffer); + + // Larger than needed buffer, read into beginning, rest remains untouched + fs.Position = 0; + buffer = new byte[TestBuffer.Length * 2]; + Assert.Equal(TestBuffer.Length, await fs.ReadAsync(new Memory(buffer))); + Assert.Equal(TestBuffer, buffer.Take(TestBuffer.Length)); + Assert.Equal(new byte[buffer.Length - TestBuffer.Length], buffer.Skip(TestBuffer.Length)); + + // Larger than needed buffer, read into middle, beginning and end remain untouched + fs.Position = 0; + buffer = new byte[TestBuffer.Length * 2]; + Assert.Equal(TestBuffer.Length, await fs.ReadAsync(new Memory(buffer, 2, buffer.Length - 2))); + Assert.Equal(TestBuffer, buffer.Skip(2).Take(TestBuffer.Length)); + Assert.Equal(new byte[2], buffer.Take(2)); + Assert.Equal(new byte[buffer.Length - TestBuffer.Length - 2], buffer.Skip(2 + TestBuffer.Length)); + } + } + + [Fact] + public void ReadOnly_WriteAsync_Throws() + { + string fileName = GetTestFilePath(); + File.WriteAllBytes(fileName, TestBuffer); + + using (var fs = CreateFileStream(fileName, FileMode.Open, FileAccess.Read)) + { + Assert.Throws(() => { fs.WriteAsync(new ReadOnlyMemory(new byte[1])); }); + fs.Dispose(); + Assert.Throws(() => { fs.WriteAsync(new ReadOnlyMemory(new byte[1])); }); // Disposed checking happens first + } + } + + [Fact] + public void WriteOnly_ReadAsync_Throws() + { + using (var fs = CreateFileStream(GetTestFilePath(), FileMode.Create, FileAccess.Write)) + { + Assert.Throws(() => { fs.ReadAsync(new Memory(new byte[1])); }); + fs.Dispose(); + Assert.Throws(() => { fs.ReadAsync(new Memory(new byte[1])); });// Disposed checking happens first + } + } + + [Fact] + public async Task EmptyWriteAsync_NoDataWritten() + { + using (var fs = CreateFileStream(GetTestFilePath(), FileMode.Create)) + { + await fs.WriteAsync(Memory.Empty); + Assert.Equal(0, fs.Length); + Assert.Equal(0, fs.Position); + } + } + + [Fact] + public async Task NonEmptyWriteAsync_WritesExpectedData() + { + using (var fs = CreateFileStream(GetTestFilePath(), FileMode.Create)) + { + await fs.WriteAsync(new Memory(TestBuffer)); + Assert.Equal(TestBuffer.Length, fs.Length); + Assert.Equal(TestBuffer.Length, fs.Position); + + fs.Position = 0; + var buffer = new byte[TestBuffer.Length]; + Assert.Equal(TestBuffer.Length, await fs.ReadAsync(new Memory(buffer))); + Assert.Equal(TestBuffer, buffer); + } + } } public class Sync_FileStream_ReadWrite_Span : FileStream_ReadWrite_Span @@ -160,6 +275,25 @@ public void CallSpanReadWriteOnDerivedFileStream_ArrayMethodsUsed() Assert.True(fs.ReadArrayInvoked); } } + + [Fact] + public async Task CallMemoryReadWriteAsyncOnDerivedFileStream_ArrayMethodsUsed() + { + using (var fs = (DerivedFileStream)CreateFileStream(GetTestFilePath(), FileMode.Create, FileAccess.ReadWrite)) + { + Assert.False(fs.WriteAsyncArrayInvoked); + Assert.False(fs.ReadAsyncArrayInvoked); + + await fs.WriteAsync(new ReadOnlyMemory(new byte[1])); + Assert.True(fs.WriteAsyncArrayInvoked); + Assert.False(fs.ReadAsyncArrayInvoked); + + fs.Position = 0; + await fs.ReadAsync(new Memory(new byte[1])); + Assert.True(fs.WriteAsyncArrayInvoked); + Assert.True(fs.ReadAsyncArrayInvoked); + } + } } public sealed class Async_DerivedFileStream_ReadWrite_Span : Async_FileStream_ReadWrite_Span @@ -171,6 +305,7 @@ protected override FileStream CreateFileStream(string path, FileMode mode, FileA internal sealed class DerivedFileStream : FileStream { public bool ReadArrayInvoked = false, WriteArrayInvoked = false; + public bool ReadAsyncArrayInvoked = false, WriteAsyncArrayInvoked = false; public DerivedFileStream(string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options) : base(path, mode, access, share, bufferSize, options) @@ -188,5 +323,17 @@ public override void Write(byte[] array, int offset, int count) WriteArrayInvoked = true; base.Write(array, offset, count); } + + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + ReadAsyncArrayInvoked = true; + return base.ReadAsync(buffer, offset, count, cancellationToken); + } + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + WriteAsyncArrayInvoked = true; + return base.WriteAsync(buffer, offset, count, cancellationToken); + } } } diff --git a/src/System.IO.UnmanagedMemoryStream/tests/UmsReadWrite.netcoreapp.cs b/src/System.IO.UnmanagedMemoryStream/tests/UmsReadWrite.netcoreapp.cs index 0432d5a7c982..a438d9cf425a 100644 --- a/src/System.IO.UnmanagedMemoryStream/tests/UmsReadWrite.netcoreapp.cs +++ b/src/System.IO.UnmanagedMemoryStream/tests/UmsReadWrite.netcoreapp.cs @@ -11,4 +11,12 @@ public override int Read(UnmanagedMemoryStream stream, byte[] array, int offset, public override void Write(UnmanagedMemoryStream stream, byte[] array, int offset, int count) => stream.Write(new Span(array, offset, count)); } + + public sealed class MemoryUmsReadWriteTests : UmsReadWriteTests + { + public override int Read(UnmanagedMemoryStream stream, byte[] array, int offset, int count) => + stream.ReadAsync(new Memory(array, offset, count)).GetAwaiter().GetResult(); + public override void Write(UnmanagedMemoryStream stream, byte[] array, int offset, int count) => + stream.WriteAsync(new Memory(array, offset, count)).GetAwaiter().GetResult(); + } } diff --git a/src/System.IO/tests/MemoryStream/MemoryStreamTests.netcoreapp.cs b/src/System.IO/tests/MemoryStream/MemoryStreamTests.netcoreapp.cs index f3d9e1f80165..4ea914b071e1 100644 --- a/src/System.IO/tests/MemoryStream/MemoryStreamTests.netcoreapp.cs +++ b/src/System.IO/tests/MemoryStream/MemoryStreamTests.netcoreapp.cs @@ -4,6 +4,8 @@ using Xunit; using System.Linq; +using System.Threading; +using System.Threading.Tasks; namespace System.IO.Tests { @@ -64,34 +66,115 @@ public void ReadSpan_DataReadAndPositionUpdated_Success() public void DerivedMemoryStream_ReadWriteSpanCalled_ReadWriteArrayUsed() { var s = new ReadWriteOverridingMemoryStream(); - Assert.False(s.WriteInvoked); - Assert.False(s.ReadInvoked); + Assert.False(s.WriteArrayInvoked); + Assert.False(s.ReadArrayInvoked); s.Write((ReadOnlySpan)new byte[1]); - Assert.True(s.WriteInvoked); - Assert.False(s.ReadInvoked); + Assert.True(s.WriteArrayInvoked); + Assert.False(s.ReadArrayInvoked); s.Position = 0; s.Read((Span)new byte[1]); - Assert.True(s.WriteInvoked); - Assert.True(s.ReadInvoked); + Assert.True(s.WriteArrayInvoked); + Assert.True(s.ReadArrayInvoked); + } + + [Fact] + public async Task WriteAsyncReadOnlyMemory_DataWrittenAndPositionUpdated_Success() + { + const int Iters = 100; + var rand = new Random(); + byte[] data = Enumerable.Range(0, (Iters * (Iters + 1)) / 2).Select(_ => (byte)rand.Next(256)).ToArray(); + var s = new MemoryStream(); + + int expectedPos = 0; + for (int i = 0; i <= Iters; i++) + { + await s.WriteAsync(new ReadOnlyMemory(data, expectedPos, i)); + expectedPos += i; + Assert.Equal(expectedPos, s.Position); + } + + Assert.Equal(data, s.ToArray()); + } + + [Fact] + public async Task ReadAsyncMemory_DataReadAndPositionUpdated_Success() + { + const int Iters = 100; + var rand = new Random(); + byte[] data = Enumerable.Range(0, (Iters * (Iters + 1)) / 2).Select(_ => (byte)rand.Next(256)).ToArray(); + var s = new MemoryStream(data); + + int expectedPos = 0; + for (int i = 0; i <= Iters; i++) + { + var toRead = new Memory(new byte[i * 3]); // enough room to read the data and have some offset and have slack at the end + + // Do the read and validate we read the expected number of bytes + Assert.Equal(i, await s.ReadAsync(toRead.Slice(i, i))); + + // The contents prior to and after the read should be empty. + Assert.Equal(new byte[i], toRead.Slice(0, i).ToArray()); + Assert.Equal(new byte[i], toRead.Slice(i * 2, i).ToArray()); + + // And the data read should match what was expected. + Assert.Equal(new Span(data, expectedPos, i).ToArray(), toRead.Slice(i, i).ToArray()); + + // Updated position should match + expectedPos += i; + Assert.Equal(expectedPos, s.Position); + } + + // A final read should be empty + Assert.Equal(0, await s.ReadAsync(new Memory(new byte[1]))); + } + + [Fact] + public async Task DerivedMemoryStream_ReadWriteAsyncMemoryCalled_ReadWriteAsyncArrayUsed() + { + var s = new ReadWriteOverridingMemoryStream(); + Assert.False(s.WriteArrayInvoked); + Assert.False(s.ReadArrayInvoked); + + await s.WriteAsync((ReadOnlyMemory)new byte[1]); + Assert.True(s.WriteArrayInvoked); + Assert.False(s.ReadArrayInvoked); + + s.Position = 0; + await s.ReadAsync((Memory)new byte[1]); + Assert.True(s.WriteArrayInvoked); + Assert.True(s.ReadArrayInvoked); } private class ReadWriteOverridingMemoryStream : MemoryStream { - public bool ReadInvoked, WriteInvoked; + public bool ReadArrayInvoked, WriteArrayInvoked; + public bool ReadAsyncArrayInvoked, WriteAsyncArrayInvoked; public override int Read(byte[] buffer, int offset, int count) { - ReadInvoked = true; + ReadArrayInvoked = true; return base.Read(buffer, offset, count); } public override void Write(byte[] buffer, int offset, int count) { - WriteInvoked = true; + WriteArrayInvoked = true; base.Write(buffer, offset, count); } + + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + ReadAsyncArrayInvoked = true; + return base.ReadAsync(buffer, offset, count, cancellationToken); + } + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + WriteAsyncArrayInvoked = true; + return base.WriteAsync(buffer, offset, count, cancellationToken); + } } } } diff --git a/src/System.IO/tests/Stream/Stream.ReadWriteSpan.netcoreapp.cs b/src/System.IO/tests/Stream/Stream.ReadWriteSpan.netcoreapp.cs index a860c97f9a2f..9a88254acb97 100644 --- a/src/System.IO/tests/Stream/Stream.ReadWriteSpan.netcoreapp.cs +++ b/src/System.IO/tests/Stream/Stream.ReadWriteSpan.netcoreapp.cs @@ -2,6 +2,10 @@ // 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.Buffers; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; using Xunit; namespace System.IO.Tests @@ -58,5 +62,222 @@ public void WriteSpan_DelegatesToWrite_Success() Assert.True(writeInvoked); writeInvoked = false; } + + [Fact] + public async Task ReadAsyncMemory_WrapsArray_DelegatesToReadAsyncArray_Success() + { + bool readInvoked = false; + var s = new DelegateStream( + canReadFunc: () => true, + readAsyncFunc: (array, offset, count, cancellationToken) => + { + readInvoked = true; + Assert.NotNull(array); + Assert.Equal(5, offset); + Assert.Equal(20, count); + + for (int i = 0; i < 10; i++) + { + array[offset + i] = (byte)i; + } + return Task.FromResult(10); + }); + + Memory totalMemory = new byte[30]; + Memory targetMemory = totalMemory.Slice(5, 20); + + Assert.Equal(10, await s.ReadAsync(targetMemory)); + Assert.True(readInvoked); + for (int i = 0; i < 10; i++) + Assert.Equal(i, targetMemory.Span[i]); + for (int i = 10; i < 20; i++) + Assert.Equal(0, targetMemory.Span[i]); + readInvoked = false; + } + + [Fact] + public async Task ReadAsyncMemory_WrapsNative_DelegatesToReadAsyncArrayWithPool_Success() + { + bool readInvoked = false; + var s = new DelegateStream( + canReadFunc: () => true, + readAsyncFunc: (array, offset, count, cancellationToken) => + { + readInvoked = true; + Assert.NotNull(array); + Assert.Equal(0, offset); + Assert.Equal(20, count); + + for (int i = 0; i < 10; i++) + { + array[offset + i] = (byte)i; + } + return Task.FromResult(10); + }); + + using (var totalNativeMemory = new NativeOwnedMemory(30)) + { + Memory totalMemory = totalNativeMemory.AsMemory; + Memory targetMemory = totalMemory.Slice(5, 20); + + Assert.Equal(10, await s.ReadAsync(targetMemory)); + Assert.True(readInvoked); + for (int i = 0; i < 10; i++) + Assert.Equal(i, targetMemory.Span[i]); + readInvoked = false; + } + } + + [Fact] + public async Task WriteAsyncMemory_WrapsArray_DelegatesToWrite_Success() + { + bool writeInvoked = false; + var s = new DelegateStream( + canWriteFunc: () => true, + writeAsyncFunc: (array, offset, count, cancellationToken) => + { + writeInvoked = true; + Assert.NotNull(array); + Assert.Equal(2, offset); + Assert.Equal(3, count); + + for (int i = 0; i < count; i++) + Assert.Equal(i, array[offset + i]); + + return Task.CompletedTask; + }); + + Memory memory = new byte[10]; + memory.Span[3] = 1; + memory.Span[4] = 2; + await s.WriteAsync(memory.Slice(2, 3)); + Assert.True(writeInvoked); + writeInvoked = false; + } + + [Fact] + public async Task WriteAsyncMemory_WrapsNative_DelegatesToWrite_Success() + { + bool writeInvoked = false; + var s = new DelegateStream( + canWriteFunc: () => true, + writeAsyncFunc: (array, offset, count, cancellationToken) => + { + writeInvoked = true; + Assert.NotNull(array); + Assert.Equal(0, offset); + Assert.Equal(3, count); + + for (int i = 0; i < count; i++) + Assert.Equal(i, array[i]); + + return Task.CompletedTask; + }); + + using (var nativeMemory = new NativeOwnedMemory(10)) + { + Memory memory = nativeMemory.AsMemory; + memory.Span[2] = 0; + memory.Span[3] = 1; + memory.Span[4] = 2; + await s.WriteAsync(memory.Slice(2, 3)); + Assert.True(writeInvoked); + writeInvoked = false; + } + } + + private sealed class NativeOwnedMemory : OwnedMemory + { + private readonly int _length; + private IntPtr _ptr; + private int _retainedCount; + private bool _disposed; + + public NativeOwnedMemory(int length) + { + _length = length; + _ptr = Marshal.AllocHGlobal(length); + } + + public override bool IsDisposed + { + get + { + lock (this) + { + return _disposed && _retainedCount == 0; + } + } + } + + public override int Length => _length; + + protected override bool IsRetained + { + get + { + lock (this) + { + return _retainedCount > 0; + } + } + } + + public override unsafe Span AsSpan() => new Span((void*)_ptr, _length); + + public override unsafe MemoryHandle Pin() => new MemoryHandle(this, (void*)_ptr); + + public override bool Release() + { + lock (this) + { + if (_retainedCount > 0) + { + _retainedCount--; + if (_retainedCount == 0) + { + if (_disposed) + { + Marshal.FreeHGlobal(_ptr); + _ptr = IntPtr.Zero; + } + return true; + } + } + } + return false; + } + + public override void Retain() + { + lock (this) + { + if (_retainedCount == 0 && _disposed) + { + throw new Exception(); + } + _retainedCount++; + } + } + + protected override void Dispose(bool disposing) + { + lock (this) + { + _disposed = true; + if (_retainedCount == 0) + { + Marshal.FreeHGlobal(_ptr); + _ptr = IntPtr.Zero; + } + } + } + + protected override bool TryGetArray(out ArraySegment arraySegment) + { + arraySegment = default(ArraySegment); + return false; + } + } } } diff --git a/src/System.IO/tests/System.IO.Tests.csproj b/src/System.IO/tests/System.IO.Tests.csproj index ae420f24ff29..af1e6b63c086 100644 --- a/src/System.IO/tests/System.IO.Tests.csproj +++ b/src/System.IO/tests/System.IO.Tests.csproj @@ -5,6 +5,7 @@ System.IO System.IO.Tests {492EC54D-D2C4-4B3F-AC1F-646B3F7EBB02} + true diff --git a/src/System.Runtime/ref/System.Runtime.cs b/src/System.Runtime/ref/System.Runtime.cs index fde04e5a5e5d..dd73b47cf078 100644 --- a/src/System.Runtime/ref/System.Runtime.cs +++ b/src/System.Runtime/ref/System.Runtime.cs @@ -5183,6 +5183,7 @@ protected virtual void ObjectInvariant() { } public virtual int Read(System.Span destination) { throw null; } public System.Threading.Tasks.Task ReadAsync(byte[] buffer, int offset, int count) { throw null; } public virtual System.Threading.Tasks.Task ReadAsync(byte[] buffer, int offset, int count, System.Threading.CancellationToken cancellationToken) { throw null; } + public System.Threading.Tasks.ValueTask ReadAsync(Memory destination, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public virtual int ReadByte() { throw null; } public abstract long Seek(long offset, System.IO.SeekOrigin origin); public abstract void SetLength(long value); @@ -5191,6 +5192,7 @@ protected virtual void ObjectInvariant() { } public virtual void Write(System.ReadOnlySpan source) { } public System.Threading.Tasks.Task WriteAsync(byte[] buffer, int offset, int count) { throw null; } public virtual System.Threading.Tasks.Task WriteAsync(byte[] buffer, int offset, int count, System.Threading.CancellationToken cancellationToken) { throw null; } + public System.Threading.Tasks.Task WriteAsync(ReadOnlyMemory source, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public virtual void WriteByte(byte value) { } } public partial class FileStream : System.IO.Stream