diff --git a/src/libraries/System.IO/tests/Stream/Stream.NullTests.cs b/src/libraries/System.IO/tests/Stream/Stream.NullTests.cs index 5b2ab1f81e8789..efe700c1e9c753 100644 --- a/src/libraries/System.IO/tests/Stream/Stream.NullTests.cs +++ b/src/libraries/System.IO/tests/Stream/Stream.NullTests.cs @@ -123,6 +123,16 @@ public static void TestNullStream_WriteByte() Assert.Equal(0, source.Position); } + [Theory] + [MemberData(nameof(NullReaders))] + public static void TestNullTextReaderDispose(TextReader input) + { + // dispose should be a no-op + input.Dispose(); + input.Dispose(); + Assert.Equal("", input.ReadToEnd()); + } + [Theory] [MemberData(nameof(NullReaders))] public static void TestNullTextReader(TextReader input) @@ -131,16 +141,54 @@ public static void TestNullTextReader(TextReader input) if (sr != null) Assert.True(sr.EndOfStream, "EndOfStream property didn't return true"); - input.ReadLine(); - input.Dispose(); - - input.ReadLine(); + Assert.Null(input.ReadLine()); if (sr != null) Assert.True(sr.EndOfStream, "EndOfStream property didn't return true"); - input.Read(); - input.Peek(); - input.Read(new char[2], 0, 2); - input.ReadToEnd(); + + Assert.Equal(-1, input.Read()); + Assert.Equal(-1, input.Peek()); + var chars = new char[2]; + Assert.Equal(0, input.Read(chars, 0, chars.Length)); + Assert.Equal(0, input.Read(chars.AsSpan())); + Assert.Equal(0, input.ReadBlock(chars, 0, chars.Length)); + Assert.Equal(0, input.ReadBlock(chars.AsSpan())); + Assert.Equal("", input.ReadToEnd()); + input.Dispose(); + } + + [Theory] + [MemberData(nameof(NullReaders))] + public static async Task TestNullTextReaderAsync(TextReader input) + { + var chars = new char[2]; + Assert.Equal(0, await input.ReadAsync(chars, 0, chars.Length)); + Assert.Equal(0, await input.ReadAsync(chars.AsMemory(), default)); + Assert.Equal(0, await input.ReadBlockAsync(chars, 0, chars.Length)); + Assert.Equal(0, await input.ReadBlockAsync(chars.AsMemory(), default)); + Assert.Null(await input.ReadLineAsync()); + Assert.Null(await input.ReadLineAsync(default)); + Assert.Equal("", await input.ReadToEndAsync()); + Assert.Equal("", await input.ReadToEndAsync(default)); + input.Dispose(); + } + + [Theory] + [MemberData(nameof(NullReaders))] + public static async Task TestCanceledNullTextReaderAsync(TextReader input) + { + using CancellationTokenSource tokenSource = new CancellationTokenSource(); + tokenSource.Cancel(); + var token = tokenSource.Token; + var chars = new char[2]; + OperationCanceledException ex; + ex = await Assert.ThrowsAnyAsync(async () => await input.ReadAsync(chars.AsMemory(), token)); + Assert.Equal(token, ex.CancellationToken); + ex = await Assert.ThrowsAnyAsync(async () => await input.ReadBlockAsync(chars.AsMemory(), token)); + Assert.Equal(token, ex.CancellationToken); + ex = await Assert.ThrowsAnyAsync(async () => await input.ReadLineAsync(token)); + Assert.Equal(token, ex.CancellationToken); + ex = await Assert.ThrowsAnyAsync(async () => await input.ReadToEndAsync(token)); + Assert.Equal(token, ex.CancellationToken); input.Dispose(); } diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/StreamReader.cs b/src/libraries/System.Private.CoreLib/src/System/IO/StreamReader.cs index b5aca3d1dfbcf7..b18aff879a87e6 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/StreamReader.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/StreamReader.cs @@ -1386,7 +1386,7 @@ private void ThrowIfDisposed() // No data, class doesn't need to be serializable. // Note this class is threadsafe. - private sealed class NullStreamReader : StreamReader + internal sealed class NullStreamReader : StreamReader { public override Encoding CurrentEncoding => Encoding.Unicode; @@ -1395,35 +1395,46 @@ protected override void Dispose(bool disposing) // Do nothing - this is essentially unclosable. } - public override int Peek() - { - return -1; - } + public override int Peek() => -1; - public override int Read() - { - return -1; - } + public override int Read() => -1; - public override int Read(char[] buffer, int index, int count) - { - return 0; - } + public override int Read(char[] buffer, int index, int count) => 0; - public override string? ReadLine() - { - return null; - } + public override int Read(Span buffer) => 0; - public override string ReadToEnd() - { - return string.Empty; - } + public override Task ReadAsync(char[] buffer, int index, int count) => Task.FromResult(0); - internal override int ReadBuffer() - { - return 0; - } + public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken) => + cancellationToken.IsCancellationRequested ? ValueTask.FromCanceled(cancellationToken) : default; + + public override int ReadBlock(char[] buffer, int index, int count) => 0; + + public override int ReadBlock(Span buffer) => 0; + + public override Task ReadBlockAsync(char[] buffer, int index, int count) => Task.FromResult(0); + + public override ValueTask ReadBlockAsync(Memory buffer, CancellationToken cancellationToken) => + cancellationToken.IsCancellationRequested ? ValueTask.FromCanceled(cancellationToken) : default; + + public override string? ReadLine() => null; + + public override Task ReadLineAsync() => Task.FromResult(null); + + public override ValueTask ReadLineAsync(CancellationToken cancellationToken) => + cancellationToken.IsCancellationRequested ? ValueTask.FromCanceled(cancellationToken) : default; + + public override string ReadToEnd() => ""; + + public override Task ReadToEndAsync() => Task.FromResult(""); + + public override Task ReadToEndAsync(CancellationToken cancellationToken) => + cancellationToken.IsCancellationRequested ? Task.FromCanceled(cancellationToken) : Task.FromResult(""); + + internal override ValueTask ReadAsyncInternal(Memory buffer, CancellationToken cancellationToken) => + cancellationToken.IsCancellationRequested ? ValueTask.FromCanceled(cancellationToken) : default; + + internal override int ReadBuffer() => 0; } } } diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/TextReader.cs b/src/libraries/System.Private.CoreLib/src/System/IO/TextReader.cs index 583fb3c2652ae6..a0536bd022e8ee 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/TextReader.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/TextReader.cs @@ -19,7 +19,8 @@ namespace System.IO // There are methods on the Stream class for reading bytes. public abstract partial class TextReader : MarshalByRefObject, IDisposable { - public static readonly TextReader Null = new NullTextReader(); + // Create our own instance to avoid static field initialization order problems on Mono. + public static readonly TextReader Null = new StreamReader.NullStreamReader(); protected TextReader() { } @@ -338,21 +339,6 @@ internal async ValueTask ReadBlockAsyncInternal(Memory buffer, Cancel } #endregion - private sealed class NullTextReader : TextReader - { - public NullTextReader() { } - - public override int Read(char[] buffer, int index, int count) - { - return 0; - } - - public override string? ReadLine() - { - return null; - } - } - public static TextReader Synchronized(TextReader reader) { if (reader == null)