From 3a7c4f442d22ba7370bed4b49b772f01e096f274 Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Sun, 4 Dec 2022 23:18:09 +0100 Subject: [PATCH] fix Decode_Cancellation tests --- .../Image/ImageTests.Decode_Cancellation.cs | 42 ++++++++++++---- .../TestUtilities/PausedStream.cs | 50 +++++++++++++++++-- 2 files changed, 78 insertions(+), 14 deletions(-) diff --git a/tests/ImageSharp.Tests/Image/ImageTests.Decode_Cancellation.cs b/tests/ImageSharp.Tests/Image/ImageTests.Decode_Cancellation.cs index 7bd794c2c3..9e6ec003c5 100644 --- a/tests/ImageSharp.Tests/Image/ImageTests.Decode_Cancellation.cs +++ b/tests/ImageSharp.Tests/Image/ImageTests.Decode_Cancellation.cs @@ -16,7 +16,7 @@ public partial class ImageTests public Decode_Cancellation() => this.TopLevelConfiguration.StreamProcessingBufferSize = 128; - public static readonly TheoryData TestFiles = new() + private static readonly string[] TestFiles = new[] { TestImages.Png.BikeSmall, TestImages.Jpeg.Baseline.Jpeg420Small, @@ -28,17 +28,30 @@ public partial class ImageTests TestImages.Pbm.RgbPlainMagick }; + private static readonly double[] CancellationPercentages = new[] { 0, 0.5, 0.9 }; + + public static readonly object[][] TestFilesWithPercentages = TestFiles + .SelectMany(f => CancellationPercentages.Select(p => new object[] { f, p })) + .ToArray(); + [Theory] - [MemberData(nameof(TestFiles))] - public async Task IdentifyAsync_IsCancellable(string file) + [MemberData(nameof(TestFilesWithPercentages))] + public async Task IdentifyAsync_IsCancellable(string file, double percentageOfStreamReadToCancel) { CancellationTokenSource cts = new(); string path = Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, file); using PausedStream pausedStream = new(path); - pausedStream.OnWaiting(_ => + pausedStream.OnWaiting(s => { - cts.Cancel(); - pausedStream.Release(); + if (s.Position >= s.Length * percentageOfStreamReadToCancel) + { + cts.Cancel(); + pausedStream.Release(); + } + else + { + pausedStream.Next(); + } }); Configuration configuration = Configuration.CreateDefaultInstance(); @@ -52,16 +65,23 @@ public partial class ImageTests } [Theory] - [MemberData(nameof(TestFiles))] - public async Task DecodeAsync_IsCancellable(string file) + [MemberData(nameof(TestFilesWithPercentages))] + public async Task LoadAsync_IsCancellable(string file, double percentageOfStreamReadToCancel) { CancellationTokenSource cts = new(); string path = Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, file); using PausedStream pausedStream = new(path); - pausedStream.OnWaiting(_ => + pausedStream.OnWaiting(s => { - cts.Cancel(); - pausedStream.Release(); + if (s.Position >= s.Length * percentageOfStreamReadToCancel) + { + cts.Cancel(); + pausedStream.Release(); + } + else + { + pausedStream.Next(); + } }); Configuration configuration = Configuration.CreateDefaultInstance(); diff --git a/tests/ImageSharp.Tests/TestUtilities/PausedStream.cs b/tests/ImageSharp.Tests/TestUtilities/PausedStream.cs index 2d13de0745..cafe1c28e7 100644 --- a/tests/ImageSharp.Tests/TestUtilities/PausedStream.cs +++ b/tests/ImageSharp.Tests/TestUtilities/PausedStream.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Buffers; + namespace SixLabors.ImageSharp.Tests.TestUtilities; public class PausedStream : Stream @@ -85,7 +87,25 @@ public class PausedStream : Stream public override void Close() => this.Await(() => this.innerStream.Close()); - public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) => this.Await(() => this.innerStream.CopyToAsync(destination, bufferSize, cancellationToken)); + public override async Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) + { + // To make sure the copy operation is buffered and pausable, we should override innerStream's strategy + // with the default Stream copy logic based from System.IO.Stream: + // https://github.com/dotnet/runtime/blob/4f53c2f7e62df44f07cf410df8a0d439f42a0a71/src/libraries/System.Private.CoreLib/src/System/IO/Stream.cs#L104-L116 + byte[] buffer = ArrayPool.Shared.Rent(bufferSize); + try + { + int bytesRead; + while ((bytesRead = await this.ReadAsync(new Memory(buffer), cancellationToken).ConfigureAwait(false)) != 0) + { + await destination.WriteAsync(new ReadOnlyMemory(buffer, 0, bytesRead), cancellationToken).ConfigureAwait(false); + } + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } public override bool CanRead => this.innerStream.CanRead; @@ -115,9 +135,33 @@ public class PausedStream : Stream public override int ReadByte() => this.Await(() => this.innerStream.ReadByte()); - protected override void Dispose(bool disposing) => this.innerStream.Dispose(); + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + if (disposing) + { + this.innerStream.Dispose(); + } + } - public override void CopyTo(Stream destination, int bufferSize) => this.Await(() => this.innerStream.CopyTo(destination, bufferSize)); + public override void CopyTo(Stream destination, int bufferSize) + { + // See comments on CopyToAsync. + byte[] buffer = ArrayPool.Shared.Rent(bufferSize); + try + { + int bytesRead; + while ((bytesRead = this.Read(buffer, 0, buffer.Length)) != 0) + { + destination.Write(buffer, 0, bytesRead); + } + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } public override int Read(Span buffer) {