diff --git a/src/libraries/System.Console/src/System/IO/ConsoleStream.cs b/src/libraries/System.Console/src/System/IO/ConsoleStream.cs index 3dbec2ef5fe2d5..bfdf63137bd10f 100644 --- a/src/libraries/System.Console/src/System/IO/ConsoleStream.cs +++ b/src/libraries/System.Console/src/System/IO/ConsoleStream.cs @@ -3,6 +3,8 @@ using System.Diagnostics; using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; namespace System.IO { @@ -28,6 +30,46 @@ public override void Write(byte[] buffer, int offset, int count) public override void WriteByte(byte value) => Write(new ReadOnlySpan(in value)); + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + ValidateWrite(buffer, offset, count); + + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + + try + { + Write(new ReadOnlySpan(buffer, offset, count)); + return Task.CompletedTask; + } + catch (Exception ex) + { + return Task.FromException(ex); + } + } + + public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) + { + ValidateCanWrite(); + + if (cancellationToken.IsCancellationRequested) + { + return ValueTask.FromCanceled(cancellationToken); + } + + try + { + Write(buffer.Span); + return ValueTask.CompletedTask; + } + catch (Exception ex) + { + return ValueTask.FromException(ex); + } + } + public override int Read(byte[] buffer, int offset, int count) { ValidateRead(buffer, offset, count); @@ -41,6 +83,44 @@ public override int ReadByte() return result != 0 ? b : -1; } + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + ValidateRead(buffer, offset, count); + + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + + try + { + return Task.FromResult(Read(new Span(buffer, offset, count))); + } + catch (Exception exception) + { + return Task.FromException(exception); + } + } + + public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + { + ValidateCanRead(); + + if (cancellationToken.IsCancellationRequested) + { + return ValueTask.FromCanceled(cancellationToken); + } + + try + { + return ValueTask.FromResult(Read(buffer.Span)); + } + catch (Exception exception) + { + return ValueTask.FromException(exception); + } + } + protected override void Dispose(bool disposing) { _canRead = false; @@ -74,7 +154,11 @@ public override void Flush() protected void ValidateRead(byte[] buffer, int offset, int count) { ValidateBufferArguments(buffer, offset, count); + ValidateCanRead(); + } + private void ValidateCanRead() + { if (!_canRead) { throw Error.GetReadNotSupported(); @@ -84,7 +168,11 @@ protected void ValidateRead(byte[] buffer, int offset, int count) protected void ValidateWrite(byte[] buffer, int offset, int count) { ValidateBufferArguments(buffer, offset, count); + ValidateCanWrite(); + } + private void ValidateCanWrite() + { if (!_canWrite) { throw Error.GetWriteNotSupported(); diff --git a/src/libraries/System.Console/tests/ConsoleStreamTests.cs b/src/libraries/System.Console/tests/ConsoleStreamTests.cs new file mode 100644 index 00000000000000..b8dc23865eaa65 --- /dev/null +++ b/src/libraries/System.Console/tests/ConsoleStreamTests.cs @@ -0,0 +1,99 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +public class ConsoleStreamTests +{ + [Fact] + public void WriteToOutputStream_EmptyArray() + { + Stream outStream = Console.OpenStandardOutput(); + outStream.Write(new byte[] { }, 0, 0); + } + + [ConditionalFact(typeof(Helpers), nameof(Helpers.IsConsoleInSupported))] + public void ReadAsyncRespectsCancellation() + { + Stream inStream = Console.OpenStandardInput(); + CancellationTokenSource cts = new CancellationTokenSource(); + cts.Cancel(); + + byte[] buffer = new byte[1024]; + Task result = inStream.ReadAsync(buffer, 0, buffer.Length, cts.Token); + Assert.True(result.IsCanceled); + + ValueTask valueTaskResult = inStream.ReadAsync(buffer.AsMemory(), cts.Token); + Assert.True(valueTaskResult.IsCanceled); + } + + [ConditionalFact(typeof(Helpers), nameof(Helpers.IsConsoleInSupported))] + public void ReadAsyncHandlesInvalidParams() + { + Stream inStream = Console.OpenStandardInput(); + + byte[] buffer = new byte[1024]; + Assert.Throws(() => { inStream.ReadAsync(null, 0, buffer.Length); }); + Assert.Throws(() => { inStream.ReadAsync(buffer, -1, buffer.Length); }); + Assert.Throws(() => { inStream.ReadAsync(buffer, 0, buffer.Length + 1); }); + } + + [Fact] + public void WriteAsyncRespectsCancellation() + { + Stream outStream = Console.OpenStandardOutput(); + CancellationTokenSource cts = new CancellationTokenSource(); + cts.Cancel(); + + byte[] bytes = Encoding.ASCII.GetBytes("Hi"); + Task result = outStream.WriteAsync(bytes, 0, bytes.Length, cts.Token); + Assert.True(result.IsCanceled); + + ValueTask valueTaskResult = outStream.WriteAsync(bytes.AsMemory(), cts.Token); + Assert.True(valueTaskResult.IsCanceled); + } + + [Fact] + public void WriteAsyncHandlesInvalidParams() + { + Stream outStream = Console.OpenStandardOutput(); + + byte[] bytes = Encoding.ASCII.GetBytes("Hi"); + Assert.Throws(() => { outStream.WriteAsync(null, 0, bytes.Length); }); + Assert.Throws(() => { outStream.WriteAsync(bytes, -1, bytes.Length); }); + Assert.Throws(() => { outStream.WriteAsync(bytes, 0, bytes.Length + 1); }); + } + + [ConditionalFact(typeof(Helpers), nameof(Helpers.IsConsoleInSupported))] + public void InputCannotWriteAsync() + { + Stream inStream = Console.OpenStandardInput(); + + byte[] bytes = Encoding.ASCII.GetBytes("Hi"); + Assert.Throws(() => { inStream.WriteAsync(bytes, 0, bytes.Length); }); + + Assert.Throws(() => { inStream.WriteAsync(bytes.AsMemory()); }); + } + + [Fact] + public void OutputCannotReadAsync() + { + Stream outStream = Console.OpenStandardOutput(); + + byte[] buffer = new byte[1024]; + Assert.Throws(() => + { + outStream.ReadAsync(buffer, 0, buffer.Length); + }); + + Assert.Throws(() => + { + outStream.ReadAsync(buffer.AsMemory()); + }); + } +} diff --git a/src/libraries/System.Console/tests/Helpers.cs b/src/libraries/System.Console/tests/Helpers.cs index 8c057a1c32dff2..04abc28fd702f6 100644 --- a/src/libraries/System.Console/tests/Helpers.cs +++ b/src/libraries/System.Console/tests/Helpers.cs @@ -6,8 +6,11 @@ using System.Text; using Xunit; -class Helpers +static class Helpers { + public static bool IsConsoleInSupported => + !PlatformDetection.IsAndroid && !PlatformDetection.IsiOS && !PlatformDetection.IsMacCatalyst && !PlatformDetection.IstvOS && !PlatformDetection.IsBrowser; + public static void SetAndReadHelper(Action setHelper, Func getHelper, Func readHelper) { const string TestString = "Test"; diff --git a/src/libraries/System.Console/tests/ReadAndWrite.cs b/src/libraries/System.Console/tests/ReadAndWrite.cs index c671e4f90313f0..85e6452c928125 100644 --- a/src/libraries/System.Console/tests/ReadAndWrite.cs +++ b/src/libraries/System.Console/tests/ReadAndWrite.cs @@ -30,13 +30,6 @@ public static void WriteOverloads() } } - [Fact] - public static void WriteToOutputStream_EmptyArray() - { - Stream outStream = Console.OpenStandardOutput(); - outStream.Write(new byte[] { }, 0, 0); - } - [Fact] [OuterLoop] public static void WriteOverloadsToRealConsole() diff --git a/src/libraries/System.Console/tests/SetIn.cs b/src/libraries/System.Console/tests/SetIn.cs index 4df9bdef462884..34e0fd00f46f95 100644 --- a/src/libraries/System.Console/tests/SetIn.cs +++ b/src/libraries/System.Console/tests/SetIn.cs @@ -10,8 +10,7 @@ // public class SetIn { - [Fact] - [SkipOnPlatform(TestPlatforms.Browser | TestPlatforms.iOS | TestPlatforms.MacCatalyst | TestPlatforms.tvOS, "Not supported on Browser, iOS, MacCatalyst, or tvOS.")] + [ConditionalFact(typeof(Helpers), nameof(Helpers.IsConsoleInSupported))] public static void SetInThrowsOnNull() { TextReader savedIn = Console.In; @@ -25,8 +24,7 @@ public static void SetInThrowsOnNull() } } - [Fact] - [SkipOnPlatform(TestPlatforms.Browser | TestPlatforms.iOS | TestPlatforms.MacCatalyst | TestPlatforms.tvOS, "Not supported on Browser, iOS, MacCatalyst, or tvOS.")] + [ConditionalFact(typeof(Helpers), nameof(Helpers.IsConsoleInSupported))] public static void SetInReadLine() { const string TextStringFormat = "Test {0}"; diff --git a/src/libraries/System.Console/tests/System.Console.Tests.csproj b/src/libraries/System.Console/tests/System.Console.Tests.csproj index 3592a18092ad2a..03dddf764a90c9 100644 --- a/src/libraries/System.Console/tests/System.Console.Tests.csproj +++ b/src/libraries/System.Console/tests/System.Console.Tests.csproj @@ -7,6 +7,7 @@ + @@ -36,8 +37,8 @@ + Link="%(RecursiveDir)%(Filename)%(Extension)" + CopyToOutputDirectory="PreserveNewest" />