From b6f00c1597ee053f28612b5185b7b1218a4f050d Mon Sep 17 00:00:00 2001 From: Scott Williams Date: Fri, 28 May 2021 21:27:20 +0100 Subject: [PATCH 1/7] Stop using timing to test cancellation token compliance. --- .../Formats/Jpg/JpegDecoderTests.cs | 83 +++++----- .../Formats/Jpg/JpegEncoderTests.cs | 39 ++--- .../Image/ImageTests.SaveAsync.cs | 20 ++- .../AsyncLocalSwitchableFilesystem.cs | 51 +++++++ .../TestUtilities/PausedStream.cs | 142 ++++++++++++++++++ 5 files changed, 269 insertions(+), 66 deletions(-) create mode 100644 tests/ImageSharp.Tests/TestUtilities/AsyncLocalSwitchableFilesystem.cs create mode 100644 tests/ImageSharp.Tests/TestUtilities/PausedStream.cs diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs index 27d70fd18..1b561c20d 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs @@ -12,6 +12,7 @@ using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Tests.Formats.Jpg.Utils; +using SixLabors.ImageSharp.Tests.TestUtilities; using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; using Xunit; @@ -126,61 +127,53 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg Assert.IsType(ex.InnerException); } - [Theory] - [InlineData(TestImages.Jpeg.Baseline.Jpeg420Small, 0)] - [InlineData(TestImages.Jpeg.Issues.ExifGetString750Transform, 1)] - [InlineData(TestImages.Jpeg.Issues.ExifGetString750Transform, 15)] - [InlineData(TestImages.Jpeg.Issues.ExifGetString750Transform, 30)] - [InlineData(TestImages.Jpeg.Issues.BadRstProgressive518, 1)] - [InlineData(TestImages.Jpeg.Issues.BadRstProgressive518, 15)] - [InlineData(TestImages.Jpeg.Issues.BadRstProgressive518, 30)] - public async Task Decode_IsCancellable(string fileName, int cancellationDelayMs) + [Fact] + public async Task Decode_IsCancellable() { - // Decoding these huge files took 300ms on i7-8650U in 2020. 30ms should be safe for cancellation delay. - string hugeFile = Path.Combine( - TestEnvironment.InputImagesDirectoryFullPath, - fileName); - - const int numberOfRuns = 5; + var file = Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, TestImages.Jpeg.Baseline.Jpeg420Small); + using var pausedStream = new PausedStream(file); + var cts = new CancellationTokenSource(); - for (int i = 0; i < numberOfRuns; i++) + var testTask = Task.Run(async () => { - var cts = new CancellationTokenSource(); - if (cancellationDelayMs == 0) - { - cts.Cancel(); - } - else - { - cts.CancelAfter(cancellationDelayMs); - } + AsyncLocalSwitchableFilesystem.ConfigureFileSystemStream(pausedStream); - try - { - using Image image = await Image.LoadAsync(hugeFile, cts.Token); - } - catch (TaskCanceledException) + return await Assert.ThrowsAsync(async () => { - // Successfully observed a cancellation - return; - } - } + using Image image = await Image.LoadAsync("someFakeFile", cts.Token); + }); + }); + + await pausedStream.FirstWaitReached; + cts.Cancel(); - throw new Exception($"No cancellation happened out of {numberOfRuns} runs!"); + // allow testTask to try and continue now we know we have started but canceled + pausedStream.Release(); + + await testTask; } - [Theory(Skip = "Identify is too fast, doesn't work reliably.")] - [InlineData(TestImages.Jpeg.Baseline.Exif)] - [InlineData(TestImages.Jpeg.Progressive.Bad.ExifUndefType)] - public async Task Identify_IsCancellable(string fileName) + [Fact] + public async Task Identify_IsCancellable() { - string file = Path.Combine( - TestEnvironment.InputImagesDirectoryFullPath, - fileName); - + var file = Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, TestImages.Jpeg.Baseline.Jpeg420Small); + using var pausedStream = new PausedStream(file); var cts = new CancellationTokenSource(); - cts.CancelAfter(TimeSpan.FromTicks(1)); - await Assert.ThrowsAsync(() => Image.IdentifyAsync(file, cts.Token)); + + var testTask = Task.Run(async () => + { + AsyncLocalSwitchableFilesystem.ConfigureFileSystemStream(pausedStream); + + return await Assert.ThrowsAsync(async () => await Image.IdentifyAsync("someFakeFile", cts.Token)); + }); + + await pausedStream.FirstWaitReached; + cts.Cancel(); + + // allow testTask to try and continue now we know we have started but canceled + pausedStream.Release(); + + await testTask; } // DEBUG ONLY! diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs index 9a1d423a6..d7e77eabe 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs @@ -13,6 +13,7 @@ using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.Metadata.Profiles.Iptc; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Tests.TestUtilities; using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; using Xunit; @@ -310,28 +311,30 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg } [Theory] - [InlineData(JpegSubsample.Ratio420, 0)] - [InlineData(JpegSubsample.Ratio420, 3)] - [InlineData(JpegSubsample.Ratio420, 10)] - [InlineData(JpegSubsample.Ratio444, 0)] - [InlineData(JpegSubsample.Ratio444, 3)] - [InlineData(JpegSubsample.Ratio444, 10)] - public async Task Encode_IsCancellable(JpegSubsample subsample, int cancellationDelayMs) + [InlineData(JpegSubsample.Ratio420)] + [InlineData(JpegSubsample.Ratio444)] + public async Task Encode_IsCancellable(JpegSubsample subsample) { - using var image = new Image(5000, 5000); - using var stream = new MemoryStream(); + using var pausedStream = new PausedStream(new MemoryStream()); var cts = new CancellationTokenSource(); - if (cancellationDelayMs == 0) - { - cts.Cancel(); - } - else + + var testTask = Task.Run(async () => { - cts.CancelAfter(cancellationDelayMs); - } + using var image = new Image(5000, 5000); + return await Assert.ThrowsAsync(async () => + { + var encoder = new JpegEncoder() { Subsample = subsample }; + await image.SaveAsync(pausedStream, encoder, cts.Token); + }); + }); + + await pausedStream.FirstWaitReached; + cts.Cancel(); + + // allow testTask to try and continue now we know we have started but canceled + pausedStream.Release(); - var encoder = new JpegEncoder() { Subsample = subsample }; - await Assert.ThrowsAsync(() => image.SaveAsync(stream, encoder, cts.Token)); + await testTask; } } } diff --git a/tests/ImageSharp.Tests/Image/ImageTests.SaveAsync.cs b/tests/ImageSharp.Tests/Image/ImageTests.SaveAsync.cs index 825bd55e4..f34b74f89 100644 --- a/tests/ImageSharp.Tests/Image/ImageTests.SaveAsync.cs +++ b/tests/ImageSharp.Tests/Image/ImageTests.SaveAsync.cs @@ -139,11 +139,25 @@ namespace SixLabors.ImageSharp.Tests var encoder = new PngEncoder() { CompressionLevel = PngCompressionLevel.BestCompression }; using var stream = new MemoryStream(); var asyncStream = new AsyncStreamWrapper(stream, () => false); + var pausedStream = new PausedStream(asyncStream); + var cts = new CancellationTokenSource(); - cts.CancelAfter(TimeSpan.FromTicks(1)); - await Assert.ThrowsAnyAsync(() => - image.SaveAsync(asyncStream, encoder, cts.Token)); + var testTask = Task.Run(async () => + { + AsyncLocalSwitchableFilesystem.ConfigureFileSystemStream(pausedStream); + + using var image = new Image(5000, 5000); + return await Assert.ThrowsAsync(async () => await image.SaveAsync(pausedStream, encoder, cts.Token)); + }); + + await pausedStream.FirstWaitReached; + cts.Cancel(); + + // allow testTask to try and continue now we know we have started but canceled + pausedStream.Release(); + + await testTask; } } } diff --git a/tests/ImageSharp.Tests/TestUtilities/AsyncLocalSwitchableFilesystem.cs b/tests/ImageSharp.Tests/TestUtilities/AsyncLocalSwitchableFilesystem.cs new file mode 100644 index 000000000..d53af7a43 --- /dev/null +++ b/tests/ImageSharp.Tests/TestUtilities/AsyncLocalSwitchableFilesystem.cs @@ -0,0 +1,51 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.IO; +using System.Threading; +using SixLabors.ImageSharp.IO; + +namespace SixLabors.ImageSharp.Tests.TestUtilities +{ + public class AsyncLocalSwitchableFilesystem : IFileSystem + { + private static readonly LocalFileSystem LocalFile = new LocalFileSystem(); + + private static readonly AsyncLocalSwitchableFilesystem Instance = new AsyncLocalSwitchableFilesystem(); + + internal static void ConfigureDefaultFileSystem(IFileSystem fileSystem) + { + Configuration.Default.FileSystem = Instance; + Instance.FileSystem = fileSystem; + } + + internal static void ConfigureFileSystemStream(Stream stream) + { + Configuration.Default.FileSystem = Instance; + Instance.FileSystem = new SingleStreamFileSystem(stream); + } + + private readonly AsyncLocal asyncLocal = new AsyncLocal(); + + private IFileSystem FileSystem + { + get => this.asyncLocal.Value ?? LocalFile; + set => this.asyncLocal.Value = value; + } + + public Stream Create(string path) => this.FileSystem.Create(path); + + public Stream OpenRead(string path) => this.FileSystem.OpenRead(path); + + public class SingleStreamFileSystem : IFileSystem + { + private readonly Stream stream; + + public SingleStreamFileSystem(Stream stream) => this.stream = stream; + + Stream IFileSystem.Create(string path) => this.stream; + + Stream IFileSystem.OpenRead(string path) => this.stream; + } + } +} diff --git a/tests/ImageSharp.Tests/TestUtilities/PausedStream.cs b/tests/ImageSharp.Tests/TestUtilities/PausedStream.cs new file mode 100644 index 000000000..20c4cf0e3 --- /dev/null +++ b/tests/ImageSharp.Tests/TestUtilities/PausedStream.cs @@ -0,0 +1,142 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace SixLabors.ImageSharp.Tests.TestUtilities +{ + public class PausedStream : Stream + { + private readonly SemaphoreSlim slim = new SemaphoreSlim(0); + + private readonly CancellationTokenSource cancelationTokenSource = new CancellationTokenSource(); + private readonly TaskCompletionSource waitReached = new TaskCompletionSource(); + + private readonly Stream innerStream; + + public Task FirstWaitReached => this.waitReached.Task; + + public void Release() + { + this.slim.Release(); + this.cancelationTokenSource.Cancel(); + } + + public void Next() => this.slim.Release(); + + private void Wait() + { + this.waitReached.TrySetResult(null); + + if (this.cancelationTokenSource.IsCancellationRequested) + { + return; + } + + try + { + this.slim.Wait(this.cancelationTokenSource.Token); + } + catch (OperationCanceledException) + { + // ignore this as its just used to unlock any waits in progress + } + } + + private async Task Await(Func action) + { + await Task.Yield(); + this.Wait(); + await action(); + } + + private async Task Await(Func> action) + { + await Task.Yield(); + this.Wait(); + return await action(); + } + + private T Await(Func action) + { + this.Wait(); + return action(); + } + + private void Await(Action action) + { + this.Wait(); + action(); + } + + public PausedStream(byte[] data) + : this(new MemoryStream(data)) + { + } + + public PausedStream(string filePath) + : this(File.OpenRead(filePath)) + { + } + + public PausedStream(Stream innerStream) => this.innerStream = innerStream; + + public override bool CanTimeout => this.innerStream.CanTimeout; + + public override void Close() => this.Await(() => this.innerStream.Close()); + + public override void CopyTo(Stream destination, int bufferSize) => this.Await(() => this.innerStream.CopyTo(destination, bufferSize)); + + public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) => this.Await(() => this.innerStream.CopyToAsync(destination, bufferSize, cancellationToken)); + + public override bool CanRead => this.innerStream.CanRead; + + public override bool CanSeek => this.innerStream.CanSeek; + + public override bool CanWrite => this.innerStream.CanWrite; + + public override long Length => this.Await(() => this.innerStream.Length); + + public override long Position { get => this.Await(() => this.innerStream.Position); set => this.Await(() => this.innerStream.Position = value); } + + public override void Flush() => this.Await(() => this.innerStream.Flush()); + + public override int Read(byte[] buffer, int offset, int count) => this.Await(() => this.innerStream.Read(buffer, offset, count)); + + public override long Seek(long offset, SeekOrigin origin) => this.Await(() => this.innerStream.Seek(offset, origin)); + + public override void SetLength(long value) => this.Await(() => this.innerStream.SetLength(value)); + + public override void Write(byte[] buffer, int offset, int count) => this.Await(() => this.innerStream.Write(buffer, offset, count)); + + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => this.Await(() => this.innerStream.ReadAsync(buffer, offset, count, cancellationToken)); + + public override int Read(Span buffer) + { + this.Wait(); + return this.innerStream.Read(buffer); + } + + public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) => this.Await(() => this.innerStream.ReadAsync(buffer, cancellationToken)); + + public override void Write(ReadOnlySpan buffer) + { + this.Wait(); + this.innerStream.Write(buffer); + } + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => this.Await(() => this.innerStream.WriteAsync(buffer, offset, count, cancellationToken)); + + public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) => this.Await(() => this.innerStream.WriteAsync(buffer, cancellationToken)); + + public override void WriteByte(byte value) => this.Await(() => this.innerStream.WriteByte(value)); + + public override int ReadByte() => this.Await(() => this.innerStream.ReadByte()); + + protected override void Dispose(bool disposing) => this.innerStream.Dispose(); + public override ValueTask DisposeAsync() => this.innerStream.DisposeAsync(); + } +} From 2a8b1da925f13753f181966895702ac413a64e35 Mon Sep 17 00:00:00 2001 From: Scott Williams Date: Fri, 28 May 2021 21:32:34 +0100 Subject: [PATCH 2/7] style cop my old foe --- tests/ImageSharp.Tests/TestUtilities/PausedStream.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/ImageSharp.Tests/TestUtilities/PausedStream.cs b/tests/ImageSharp.Tests/TestUtilities/PausedStream.cs index 20c4cf0e3..bba4c61dc 100644 --- a/tests/ImageSharp.Tests/TestUtilities/PausedStream.cs +++ b/tests/ImageSharp.Tests/TestUtilities/PausedStream.cs @@ -137,6 +137,7 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities public override int ReadByte() => this.Await(() => this.innerStream.ReadByte()); protected override void Dispose(bool disposing) => this.innerStream.Dispose(); + public override ValueTask DisposeAsync() => this.innerStream.DisposeAsync(); } } From 5ccfec917e891e73904f9d541a864ae1ee4f416a Mon Sep 17 00:00:00 2001 From: Scott Williams Date: Fri, 28 May 2021 21:56:58 +0100 Subject: [PATCH 3/7] allow finer grained details logic on when to release the pause. --- .../Formats/Jpg/JpegDecoderTests.cs | 56 +++++++++---------- .../Formats/Jpg/JpegEncoderTests.cs | 33 ++++++----- .../Image/ImageTests.SaveAsync.cs | 19 ++----- .../TestUtilities/PausedStream.cs | 10 ++-- 4 files changed, 57 insertions(+), 61 deletions(-) diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs index 1b561c20d..478b0f3ff 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs @@ -127,53 +127,53 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg Assert.IsType(ex.InnerException); } - [Fact] - public async Task Decode_IsCancellable() + [Theory] + [InlineData(0)] + [InlineData(0.5)] + [InlineData(0.9)] + public async Task Decode_IsCancellable(int percentageOfStreamReadToCancel) { + var cts = new CancellationTokenSource(); var file = Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, TestImages.Jpeg.Baseline.Jpeg420Small); using var pausedStream = new PausedStream(file); - var cts = new CancellationTokenSource(); - - var testTask = Task.Run(async () => + pausedStream.OnWaiting(s => { - AsyncLocalSwitchableFilesystem.ConfigureFileSystemStream(pausedStream); - - return await Assert.ThrowsAsync(async () => + if (s.Position >= s.Length * percentageOfStreamReadToCancel) { - using Image image = await Image.LoadAsync("someFakeFile", cts.Token); - }); + cts.Cancel(); + pausedStream.Release(); + } + else + { + // allows this/next wait to unblock + pausedStream.Next(); + } }); - await pausedStream.FirstWaitReached; - cts.Cancel(); - - // allow testTask to try and continue now we know we have started but canceled - pausedStream.Release(); + AsyncLocalSwitchableFilesystem.ConfigureFileSystemStream(pausedStream); - await testTask; + await Assert.ThrowsAsync(async () => + { + using Image image = await Image.LoadAsync("someFakeFile", cts.Token); + }); } [Fact] public async Task Identify_IsCancellable() { - var file = Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, TestImages.Jpeg.Baseline.Jpeg420Small); - using var pausedStream = new PausedStream(file); var cts = new CancellationTokenSource(); - var testTask = Task.Run(async () => + var file = Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, TestImages.Jpeg.Baseline.Jpeg420Small); + using var pausedStream = new PausedStream(file); + pausedStream.OnWaiting(s => { - AsyncLocalSwitchableFilesystem.ConfigureFileSystemStream(pausedStream); - - return await Assert.ThrowsAsync(async () => await Image.IdentifyAsync("someFakeFile", cts.Token)); + cts.Cancel(); + pausedStream.Release(); }); - await pausedStream.FirstWaitReached; - cts.Cancel(); - - // allow testTask to try and continue now we know we have started but canceled - pausedStream.Release(); + AsyncLocalSwitchableFilesystem.ConfigureFileSystemStream(pausedStream); - await testTask; + await Assert.ThrowsAsync(async () => await Image.IdentifyAsync("someFakeFile", cts.Token)); } // DEBUG ONLY! diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs index d7e77eabe..3c48865c7 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs @@ -315,26 +315,29 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg [InlineData(JpegSubsample.Ratio444)] public async Task Encode_IsCancellable(JpegSubsample subsample) { - using var pausedStream = new PausedStream(new MemoryStream()); var cts = new CancellationTokenSource(); - - var testTask = Task.Run(async () => + using var pausedStream = new PausedStream(new MemoryStream()); + pausedStream.OnWaiting(s => { - using var image = new Image(5000, 5000); - return await Assert.ThrowsAsync(async () => + // after some writing + if (s.Position >= 500) + { + cts.Cancel(); + pausedStream.Release(); + } + else { - var encoder = new JpegEncoder() { Subsample = subsample }; - await image.SaveAsync(pausedStream, encoder, cts.Token); - }); + // allows this/next wait to unblock + pausedStream.Next(); + } }); - await pausedStream.FirstWaitReached; - cts.Cancel(); - - // allow testTask to try and continue now we know we have started but canceled - pausedStream.Release(); - - await testTask; + using var image = new Image(5000, 5000); + await Assert.ThrowsAsync(async () => + { + var encoder = new JpegEncoder() { Subsample = subsample }; + await image.SaveAsync(pausedStream, encoder, cts.Token); + }); } } } diff --git a/tests/ImageSharp.Tests/Image/ImageTests.SaveAsync.cs b/tests/ImageSharp.Tests/Image/ImageTests.SaveAsync.cs index f34b74f89..8bb121349 100644 --- a/tests/ImageSharp.Tests/Image/ImageTests.SaveAsync.cs +++ b/tests/ImageSharp.Tests/Image/ImageTests.SaveAsync.cs @@ -139,25 +139,16 @@ namespace SixLabors.ImageSharp.Tests var encoder = new PngEncoder() { CompressionLevel = PngCompressionLevel.BestCompression }; using var stream = new MemoryStream(); var asyncStream = new AsyncStreamWrapper(stream, () => false); - var pausedStream = new PausedStream(asyncStream); - var cts = new CancellationTokenSource(); - var testTask = Task.Run(async () => + var pausedStream = new PausedStream(asyncStream); + pausedStream.OnWaiting(s => { - AsyncLocalSwitchableFilesystem.ConfigureFileSystemStream(pausedStream); - - using var image = new Image(5000, 5000); - return await Assert.ThrowsAsync(async () => await image.SaveAsync(pausedStream, encoder, cts.Token)); + cts.Cancel(); + pausedStream.Release(); }); - await pausedStream.FirstWaitReached; - cts.Cancel(); - - // allow testTask to try and continue now we know we have started but canceled - pausedStream.Release(); - - await testTask; + await Assert.ThrowsAsync(async () => await image.SaveAsync(pausedStream, encoder, cts.Token)); } } } diff --git a/tests/ImageSharp.Tests/TestUtilities/PausedStream.cs b/tests/ImageSharp.Tests/TestUtilities/PausedStream.cs index bba4c61dc..c6902b06a 100644 --- a/tests/ImageSharp.Tests/TestUtilities/PausedStream.cs +++ b/tests/ImageSharp.Tests/TestUtilities/PausedStream.cs @@ -13,11 +13,13 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities private readonly SemaphoreSlim slim = new SemaphoreSlim(0); private readonly CancellationTokenSource cancelationTokenSource = new CancellationTokenSource(); - private readonly TaskCompletionSource waitReached = new TaskCompletionSource(); private readonly Stream innerStream; + private Action onWaitingCallback; - public Task FirstWaitReached => this.waitReached.Task; + public void OnWaiting(Action onWaitingCallback) => this.onWaitingCallback = onWaitingCallback; + + public void OnWaiting(Action onWaitingCallback) => this.OnWaiting(_ => onWaitingCallback()); public void Release() { @@ -29,13 +31,13 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities private void Wait() { - this.waitReached.TrySetResult(null); - if (this.cancelationTokenSource.IsCancellationRequested) { return; } + this.onWaitingCallback?.Invoke(this.innerStream); + try { this.slim.Wait(this.cancelationTokenSource.Token); From 0dd0f0f01ca31e725e47c99cab5e1b94efebdfde Mon Sep 17 00:00:00 2001 From: Scott Williams Date: Fri, 28 May 2021 22:09:56 +0100 Subject: [PATCH 4/7] skipping `DisposeAsync()` as it not in netcoreapp2.1 its not used anyway in our code so doesn't matter. --- tests/ImageSharp.Tests/TestUtilities/PausedStream.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/ImageSharp.Tests/TestUtilities/PausedStream.cs b/tests/ImageSharp.Tests/TestUtilities/PausedStream.cs index c6902b06a..5887c533d 100644 --- a/tests/ImageSharp.Tests/TestUtilities/PausedStream.cs +++ b/tests/ImageSharp.Tests/TestUtilities/PausedStream.cs @@ -139,7 +139,5 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities public override int ReadByte() => this.Await(() => this.innerStream.ReadByte()); protected override void Dispose(bool disposing) => this.innerStream.Dispose(); - - public override ValueTask DisposeAsync() => this.innerStream.DisposeAsync(); } } From 654901555d2d79b9d8f18372a296aec23b8638c0 Mon Sep 17 00:00:00 2001 From: Scott Williams Date: Fri, 28 May 2021 22:21:35 +0100 Subject: [PATCH 5/7] skip some overrides on full framework --- .../TestUtilities/PausedStream.cs | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/tests/ImageSharp.Tests/TestUtilities/PausedStream.cs b/tests/ImageSharp.Tests/TestUtilities/PausedStream.cs index 5887c533d..b6ab039b1 100644 --- a/tests/ImageSharp.Tests/TestUtilities/PausedStream.cs +++ b/tests/ImageSharp.Tests/TestUtilities/PausedStream.cs @@ -90,8 +90,6 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities public override void Close() => this.Await(() => this.innerStream.Close()); - public override void CopyTo(Stream destination, int bufferSize) => this.Await(() => this.innerStream.CopyTo(destination, bufferSize)); - public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) => this.Await(() => this.innerStream.CopyToAsync(destination, bufferSize, cancellationToken)); public override bool CanRead => this.innerStream.CanRead; @@ -116,6 +114,17 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => this.Await(() => this.innerStream.ReadAsync(buffer, offset, count, cancellationToken)); + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => this.Await(() => this.innerStream.WriteAsync(buffer, offset, count, cancellationToken)); + + public override void WriteByte(byte value) => this.Await(() => this.innerStream.WriteByte(value)); + + public override int ReadByte() => this.Await(() => this.innerStream.ReadByte()); + + protected override void Dispose(bool disposing) => this.innerStream.Dispose(); + +#if NETCOREAPP + public override void CopyTo(Stream destination, int bufferSize) => this.Await(() => this.innerStream.CopyTo(destination, bufferSize)); + public override int Read(Span buffer) { this.Wait(); @@ -130,14 +139,7 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities this.innerStream.Write(buffer); } - public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => this.Await(() => this.innerStream.WriteAsync(buffer, offset, count, cancellationToken)); - public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) => this.Await(() => this.innerStream.WriteAsync(buffer, cancellationToken)); - - public override void WriteByte(byte value) => this.Await(() => this.innerStream.WriteByte(value)); - - public override int ReadByte() => this.Await(() => this.innerStream.ReadByte()); - - protected override void Dispose(bool disposing) => this.innerStream.Dispose(); +#endif } } From e66e31947c6c50699e5dcce2263f550494c13e43 Mon Sep 17 00:00:00 2001 From: Scott Williams Date: Fri, 28 May 2021 22:27:56 +0100 Subject: [PATCH 6/7] rename semaphore --- tests/ImageSharp.Tests/TestUtilities/PausedStream.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/ImageSharp.Tests/TestUtilities/PausedStream.cs b/tests/ImageSharp.Tests/TestUtilities/PausedStream.cs index b6ab039b1..4d3646301 100644 --- a/tests/ImageSharp.Tests/TestUtilities/PausedStream.cs +++ b/tests/ImageSharp.Tests/TestUtilities/PausedStream.cs @@ -10,7 +10,7 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities { public class PausedStream : Stream { - private readonly SemaphoreSlim slim = new SemaphoreSlim(0); + private readonly SemaphoreSlim semaphore = new SemaphoreSlim(0); private readonly CancellationTokenSource cancelationTokenSource = new CancellationTokenSource(); @@ -23,11 +23,11 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities public void Release() { - this.slim.Release(); + this.semaphore.Release(); this.cancelationTokenSource.Cancel(); } - public void Next() => this.slim.Release(); + public void Next() => this.semaphore.Release(); private void Wait() { @@ -40,7 +40,7 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities try { - this.slim.Wait(this.cancelationTokenSource.Token); + this.semaphore.Wait(this.cancelationTokenSource.Token); } catch (OperationCanceledException) { From 695cfd3ef70e8a4a224999f39941856dd79a1321 Mon Sep 17 00:00:00 2001 From: Scott Williams Date: Fri, 28 May 2021 22:56:30 +0100 Subject: [PATCH 7/7] use overloads taking configuration --- .../Formats/Jpg/JpegDecoderTests.cs | 11 ++-- .../AsyncLocalSwitchableFilesystem.cs | 51 ------------------- .../TestUtilities/SingleStreamFileSystem.cs | 19 +++++++ 3 files changed, 25 insertions(+), 56 deletions(-) delete mode 100644 tests/ImageSharp.Tests/TestUtilities/AsyncLocalSwitchableFilesystem.cs create mode 100644 tests/ImageSharp.Tests/TestUtilities/SingleStreamFileSystem.cs diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs index 478b0f3ff..67df6a881 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs @@ -150,11 +150,11 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg } }); - AsyncLocalSwitchableFilesystem.ConfigureFileSystemStream(pausedStream); - + var config = Configuration.CreateDefaultInstance(); + config.FileSystem = new SingleStreamFileSystem(pausedStream); await Assert.ThrowsAsync(async () => { - using Image image = await Image.LoadAsync("someFakeFile", cts.Token); + using Image image = await Image.LoadAsync(config, "someFakeFile", cts.Token); }); } @@ -171,9 +171,10 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg pausedStream.Release(); }); - AsyncLocalSwitchableFilesystem.ConfigureFileSystemStream(pausedStream); + var config = Configuration.CreateDefaultInstance(); + config.FileSystem = new SingleStreamFileSystem(pausedStream); - await Assert.ThrowsAsync(async () => await Image.IdentifyAsync("someFakeFile", cts.Token)); + await Assert.ThrowsAsync(async () => await Image.IdentifyAsync(config, "someFakeFile", cts.Token)); } // DEBUG ONLY! diff --git a/tests/ImageSharp.Tests/TestUtilities/AsyncLocalSwitchableFilesystem.cs b/tests/ImageSharp.Tests/TestUtilities/AsyncLocalSwitchableFilesystem.cs deleted file mode 100644 index d53af7a43..000000000 --- a/tests/ImageSharp.Tests/TestUtilities/AsyncLocalSwitchableFilesystem.cs +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. - -using System.IO; -using System.Threading; -using SixLabors.ImageSharp.IO; - -namespace SixLabors.ImageSharp.Tests.TestUtilities -{ - public class AsyncLocalSwitchableFilesystem : IFileSystem - { - private static readonly LocalFileSystem LocalFile = new LocalFileSystem(); - - private static readonly AsyncLocalSwitchableFilesystem Instance = new AsyncLocalSwitchableFilesystem(); - - internal static void ConfigureDefaultFileSystem(IFileSystem fileSystem) - { - Configuration.Default.FileSystem = Instance; - Instance.FileSystem = fileSystem; - } - - internal static void ConfigureFileSystemStream(Stream stream) - { - Configuration.Default.FileSystem = Instance; - Instance.FileSystem = new SingleStreamFileSystem(stream); - } - - private readonly AsyncLocal asyncLocal = new AsyncLocal(); - - private IFileSystem FileSystem - { - get => this.asyncLocal.Value ?? LocalFile; - set => this.asyncLocal.Value = value; - } - - public Stream Create(string path) => this.FileSystem.Create(path); - - public Stream OpenRead(string path) => this.FileSystem.OpenRead(path); - - public class SingleStreamFileSystem : IFileSystem - { - private readonly Stream stream; - - public SingleStreamFileSystem(Stream stream) => this.stream = stream; - - Stream IFileSystem.Create(string path) => this.stream; - - Stream IFileSystem.OpenRead(string path) => this.stream; - } - } -} diff --git a/tests/ImageSharp.Tests/TestUtilities/SingleStreamFileSystem.cs b/tests/ImageSharp.Tests/TestUtilities/SingleStreamFileSystem.cs new file mode 100644 index 000000000..ddd1ec750 --- /dev/null +++ b/tests/ImageSharp.Tests/TestUtilities/SingleStreamFileSystem.cs @@ -0,0 +1,19 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.IO; +using SixLabors.ImageSharp.IO; + +namespace SixLabors.ImageSharp.Tests.TestUtilities +{ + internal class SingleStreamFileSystem : IFileSystem + { + private readonly Stream stream; + + public SingleStreamFileSystem(Stream stream) => this.stream = stream; + + Stream IFileSystem.Create(string path) => this.stream; + + Stream IFileSystem.OpenRead(string path) => this.stream; + } +}