diff --git a/src/Magick.NET/Helpers/AsyncStreamWrapper.cs b/src/Magick.NET/Helpers/AsyncStreamWrapper.cs index b982cf4099..a21a23d057 100644 --- a/src/Magick.NET/Helpers/AsyncStreamWrapper.cs +++ b/src/Magick.NET/Helpers/AsyncStreamWrapper.cs @@ -59,7 +59,18 @@ public async Task ReadAsync(Action action, CancellationToken cancellationToken) TaskScheduler.Default); var readTask = ReadAsync(cancellationToken); - await Task.WhenAll(actionTask, readTask).ConfigureAwait(false); + try + { + await Task.WhenAll(actionTask, readTask).ConfigureAwait(false); + } + catch when (_exceptionThrown) + { + // The native operation failed because a stream operation failed. When that failure was + // caused by a cancellation the exception of the native operation (e.g. a corrupt image + // exception from the coder) should be replaced with an OperationCanceledException. + cancellationToken.ThrowIfCancellationRequested(); + throw; + } if (_exceptionThrown) cancellationToken.ThrowIfCancellationRequested(); @@ -88,7 +99,19 @@ public async Task WriteAsync(Action action, CancellationToken cancellationToken) var readTask = ReadAsync(cancellationToken); var writeTask = WriteAsync(cancellationToken); - await Task.WhenAll(actionTask, readTask, writeTask).ConfigureAwait(false); + try + { + await Task.WhenAll(actionTask, readTask, writeTask).ConfigureAwait(false); + } + catch when (_exceptionThrown) + { + // The native operation failed because a stream operation failed. When that failure was + // caused by a cancellation the exception of the native operation (e.g. "Output file + // write error --- out of disk space?" from the jpeg coder) should be replaced with + // an OperationCanceledException. + cancellationToken.ThrowIfCancellationRequested(); + throw; + } if (_exceptionThrown) cancellationToken.ThrowIfCancellationRequested(); diff --git a/tests/Magick.NET.Tests/Helpers/AsyncStreamWrapperTest/TheReadAsyncMethod.cs b/tests/Magick.NET.Tests/Helpers/AsyncStreamWrapperTest/TheReadAsyncMethod.cs index f46cf388a4..f4a36d53eb 100644 --- a/tests/Magick.NET.Tests/Helpers/AsyncStreamWrapperTest/TheReadAsyncMethod.cs +++ b/tests/Magick.NET.Tests/Helpers/AsyncStreamWrapperTest/TheReadAsyncMethod.cs @@ -116,5 +116,28 @@ unsafe void ReadSync() await Assert.ThrowsAsync(() => wrapper.ReadAsync(ReadSync, cancellationTokenSource.Token)); } + + [Fact] + public async Task ShouldThrowExceptionWhenOperationIsCancelledAndActionThrowsException() + { + using var stream = new MemoryStream(); + using var wrapper = AsyncStreamWrapper.CreateForReading(stream); + + using var cancellationTokenSource = new CancellationTokenSource(); + cancellationTokenSource.Cancel(); + + unsafe void ReadSync() + { + var buffer = new byte[10]; + fixed (byte* p = buffer) + { + var count = wrapper.Read((IntPtr)p, (UIntPtr)10, IntPtr.Zero); + if (count == -1) + throw new InvalidOperationException("Simulates the exception the native code throws when a read fails (e.g. a corrupt image exception)."); + } + } + + await Assert.ThrowsAsync(() => wrapper.ReadAsync(ReadSync, cancellationTokenSource.Token)); + } } } diff --git a/tests/Magick.NET.Tests/Helpers/AsyncStreamWrapperTest/TheWriteAsyncMethod.cs b/tests/Magick.NET.Tests/Helpers/AsyncStreamWrapperTest/TheWriteAsyncMethod.cs index d80efd52d0..aee3accdef 100644 --- a/tests/Magick.NET.Tests/Helpers/AsyncStreamWrapperTest/TheWriteAsyncMethod.cs +++ b/tests/Magick.NET.Tests/Helpers/AsyncStreamWrapperTest/TheWriteAsyncMethod.cs @@ -113,5 +113,28 @@ unsafe void WriteSync() await Assert.ThrowsAsync(() => wrapper.WriteAsync(WriteSync, cancellationTokenSource.Token)); } + + [Fact] + public async Task ShouldThrowExceptionWhenOperationIsCancelledAndActionThrowsException() + { + using var stream = new MemoryStream(); + using var wrapper = AsyncStreamWrapper.CreateForWriting(stream); + + using var cancellationTokenSource = new CancellationTokenSource(); + cancellationTokenSource.Cancel(); + + unsafe void WriteSync() + { + var buffer = new byte[5]; + fixed (byte* p = buffer) + { + var count = wrapper.Write((IntPtr)p, (UIntPtr)5, IntPtr.Zero); + if (count == -1) + throw new InvalidOperationException("Simulates the exception the native code throws when a write fails (e.g. the jpeg coder)."); + } + } + + await Assert.ThrowsAsync(() => wrapper.WriteAsync(WriteSync, cancellationTokenSource.Token)); + } } } diff --git a/tests/Magick.NET.Tests/MagickImageTests/TheReadAsyncMethod.cs b/tests/Magick.NET.Tests/MagickImageTests/TheReadAsyncMethod.cs index 6fab5e1fcd..183218d7d1 100644 --- a/tests/Magick.NET.Tests/MagickImageTests/TheReadAsyncMethod.cs +++ b/tests/Magick.NET.Tests/MagickImageTests/TheReadAsyncMethod.cs @@ -4,6 +4,7 @@ using System; using System.IO; using System.Text; +using System.Threading; using System.Threading.Tasks; using ImageMagick; using Xunit; @@ -198,6 +199,17 @@ public async Task ShouldThrowExceptionWhenStreamIsNull() await Assert.ThrowsAsync("stream", () => image.ReadAsync((Stream)null!, TestContext.Current.CancellationToken)); } + [Fact] + public async Task ShouldThrowOperationCanceledExceptionWhenOperationIsCancelled() + { + using var image = new MagickImage(); + using var stream = File.OpenRead(Files.ImageMagickJPG); + using var cancellationTokenSource = new CancellationTokenSource(); + cancellationTokenSource.Cancel(); + + await Assert.ThrowsAsync(() => image.ReadAsync(stream, cancellationTokenSource.Token)); + } + [Fact] public async Task ShouldThrowExceptionWhenStreamIsEmpty() { diff --git a/tests/Magick.NET.Tests/MagickImageTests/TheWriteAsyncMethod.cs b/tests/Magick.NET.Tests/MagickImageTests/TheWriteAsyncMethod.cs index f8bb670643..ea3574e550 100644 --- a/tests/Magick.NET.Tests/MagickImageTests/TheWriteAsyncMethod.cs +++ b/tests/Magick.NET.Tests/MagickImageTests/TheWriteAsyncMethod.cs @@ -3,6 +3,7 @@ using System; using System.IO; +using System.Threading; using System.Threading.Tasks; using ImageMagick; using ImageMagick.Formats; @@ -225,6 +226,19 @@ public async Task ShouldThrowExceptionWhenStreamIsNull() await Assert.ThrowsAsync("stream", () => image.WriteAsync((Stream)null!, TestContext.Current.CancellationToken)); } + + [Fact] + public async Task ShouldThrowOperationCanceledExceptionWhenOperationIsCancelled() + { + using var image = new MagickImage(MagickColors.Red, 100, 100); + image.Format = MagickFormat.Jpeg; + + using var stream = new MemoryStream(); + using var cancellationTokenSource = new CancellationTokenSource(); + cancellationTokenSource.Cancel(); + + await Assert.ThrowsAsync(() => image.WriteAsync(stream, cancellationTokenSource.Token)); + } } public class WithStreamAndMagickFormat