Browse Source

Stop using timing to test cancellation token compliance.

pull/1644/head
Scott Williams 5 years ago
parent
commit
b6f00c1597
  1. 83
      tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs
  2. 39
      tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs
  3. 20
      tests/ImageSharp.Tests/Image/ImageTests.SaveAsync.cs
  4. 51
      tests/ImageSharp.Tests/TestUtilities/AsyncLocalSwitchableFilesystem.cs
  5. 142
      tests/ImageSharp.Tests/TestUtilities/PausedStream.cs

83
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<InvalidMemoryOperationException>(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<TaskCanceledException>(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<TaskCanceledException>(() => Image.IdentifyAsync(file, cts.Token));
var testTask = Task.Run(async () =>
{
AsyncLocalSwitchableFilesystem.ConfigureFileSystemStream(pausedStream);
return await Assert.ThrowsAsync<TaskCanceledException>(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!

39
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<Rgba32>(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<Rgba32>(5000, 5000);
return await Assert.ThrowsAsync<TaskCanceledException>(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<TaskCanceledException>(() => image.SaveAsync(stream, encoder, cts.Token));
await testTask;
}
}
}

20
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<TaskCanceledException>(() =>
image.SaveAsync(asyncStream, encoder, cts.Token));
var testTask = Task.Run(async () =>
{
AsyncLocalSwitchableFilesystem.ConfigureFileSystemStream(pausedStream);
using var image = new Image<Rgba32>(5000, 5000);
return await Assert.ThrowsAsync<TaskCanceledException>(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;
}
}
}

51
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<IFileSystem> asyncLocal = new AsyncLocal<IFileSystem>();
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;
}
}
}

142
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<object> waitReached = new TaskCompletionSource<object>();
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<Task> action)
{
await Task.Yield();
this.Wait();
await action();
}
private async Task<T> Await<T>(Func<Task<T>> action)
{
await Task.Yield();
this.Wait();
return await action();
}
private T Await<T>(Func<T> 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<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => this.Await(() => this.innerStream.ReadAsync(buffer, offset, count, cancellationToken));
public override int Read(Span<byte> buffer)
{
this.Wait();
return this.innerStream.Read(buffer);
}
public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default) => this.Await(() => this.innerStream.ReadAsync(buffer, cancellationToken));
public override void Write(ReadOnlySpan<byte> 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<byte> 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();
}
}
Loading…
Cancel
Save