From 518032b920d322a27efb3cb564dfb6f4d72c8b75 Mon Sep 17 00:00:00 2001 From: TheSquid Date: Wed, 3 Jan 2024 13:29:25 +0100 Subject: [PATCH] Added cancelation token support for the operation of asynchronously reading a string from a stream. --- CodeJam.Main.Tests/IO/StreamTests.cs | 101 +++++++++++++++++++++++++++ CodeJam.Main/IO/StreamExtensions.cs | 27 +++++++ 2 files changed, 128 insertions(+) create mode 100644 CodeJam.Main.Tests/IO/StreamTests.cs diff --git a/CodeJam.Main.Tests/IO/StreamTests.cs b/CodeJam.Main.Tests/IO/StreamTests.cs new file mode 100644 index 00000000..260a236e --- /dev/null +++ b/CodeJam.Main.Tests/IO/StreamTests.cs @@ -0,0 +1,101 @@ +#if NET45_OR_GREATER || TARGETS_NETSTANDARD || TARGETS_NETCOREAPP // PUBLIC_API_CHANGES + +using NUnit.Framework; + +using System; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace CodeJam.IO +{ + [TestFixture(Category = "Assertions")] + [SuppressMessage("ReSharper", "NotResolvedInText")] + public class StreamTests + { + [Test] + public void TestStreamReadingCanceled() + { + using var file = new FileStreamSlowWrapper(1, 3, 0); + var input = Encoding.UTF8.GetBytes(Guid.NewGuid().ToString()); + + file.Write(input, 0, input.Length); + file.Flush(); + + var cancellationTokenSource = new CancellationTokenSource(); + var cancellationToken = cancellationTokenSource.Token; + + file.Position = 0; + var task = file.ReadAsStringAsync(Encoding.UTF8, cancellationToken); + + cancellationTokenSource.Cancel(); + Assert.ThrowsAsync(() => task); + } + + internal class FileStreamSlowWrapper : Stream + { + private FileStream fileStream; + + private TimeSpan readDelay; + + private TimeSpan writeDelay; + + public FileStreamSlowWrapper( + int bufferSize = 4096, + double readDelaySeconds = 0, + double writeDelaySeconds = 0) + { + readDelay = TimeSpan.FromSeconds(readDelaySeconds); + writeDelay = TimeSpan.FromSeconds(writeDelaySeconds); + + var filePath = System.IO.Path.Combine( + System.IO.Path.GetTempPath(), + Guid.NewGuid() + ".tmp"); + fileStream = new FileStream( + filePath, + FileMode.CreateNew, + FileAccess.ReadWrite, + FileShare.Read, + bufferSize, + FileOptions.DeleteOnClose); + } + + public override bool CanRead => fileStream.CanRead; + + public override bool CanSeek => fileStream.CanSeek; + + public override bool CanWrite => fileStream.CanWrite; + + public override long Length => fileStream.Length; + + public override long Position { get => fileStream.Position; set => fileStream.Position = value; } + + public override void Flush() => fileStream.Flush(); + + public override long Seek(long offset, SeekOrigin origin) => fileStream.Seek(offset, origin); + + public override void SetLength(long value) => fileStream.SetLength(value); + + public override void Close() => fileStream.Close(); + + protected override void Dispose(bool disposing) => fileStream.Dispose(); + + public override int Read(byte[] buffer, int offset, int count) + { + Task.Delay(readDelay).Wait(); + return fileStream.Read(buffer, offset, count); + } + + public override void Write(byte[] buffer, int offset, int count) + { + Task.Delay(writeDelay).Wait(); + fileStream.Write(buffer, offset, count); + } + } + } +} + +#endif \ No newline at end of file diff --git a/CodeJam.Main/IO/StreamExtensions.cs b/CodeJam.Main/IO/StreamExtensions.cs index 1201b661..eaccf77e 100644 --- a/CodeJam.Main/IO/StreamExtensions.cs +++ b/CodeJam.Main/IO/StreamExtensions.cs @@ -2,6 +2,7 @@ using System; using System.IO; using System.Text; +using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; @@ -110,6 +111,32 @@ public static async Task ReadAsStringAsync( return await reader.ReadToEndAsync().ConfigureAwait(false); } + /// + /// Returns content of the stream as a string. + /// + /// The stream to read. + /// The character encoding to use. + /// Token of cancellation of the operation. + public static async Task ReadAsStringAsync( + this Stream stream, + Encoding? encoding = null, + CancellationToken cancellationToken = default) + { + Code.NotNull(stream, nameof(stream)); + using var register = cancellationToken.Register(() => stream.Close()); + + try + { + using var reader = stream.ToStreamReader(encoding, true); + return await reader.ReadToEndAsync().ConfigureAwait(false); + } + catch + { + if (!cancellationToken.IsCancellationRequested) throw; + throw new OperationCanceledException(cancellationToken); + } + } + /// /// Returns content of the stream as a byte array. ///