Browse Source

Merge pull request #1644 from SixLabors/sw/iscancellable-tests

Stop using timeouts to test cancellation token compliance.
pull/1655/head
James Jackson-South 5 years ago
committed by GitHub
parent
commit
f2deb47dec
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 76
      tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs
  2. 44
      tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs
  3. 11
      tests/ImageSharp.Tests/Image/ImageTests.SaveAsync.cs
  4. 145
      tests/ImageSharp.Tests/TestUtilities/PausedStream.cs
  5. 19
      tests/ImageSharp.Tests/TestUtilities/SingleStreamFileSystem.cs

76
tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs

@ -12,6 +12,7 @@ using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Tests.Formats.Jpg.Utils; using SixLabors.ImageSharp.Tests.Formats.Jpg.Utils;
using SixLabors.ImageSharp.Tests.TestUtilities;
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
using Xunit; using Xunit;
@ -127,60 +128,53 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
} }
[Theory] [Theory]
[InlineData(TestImages.Jpeg.Baseline.Jpeg420Small, 0)] [InlineData(0)]
[InlineData(TestImages.Jpeg.Issues.ExifGetString750Transform, 1)] [InlineData(0.5)]
[InlineData(TestImages.Jpeg.Issues.ExifGetString750Transform, 15)] [InlineData(0.9)]
[InlineData(TestImages.Jpeg.Issues.ExifGetString750Transform, 30)] public async Task Decode_IsCancellable(int percentageOfStreamReadToCancel)
[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)
{ {
// Decoding these huge files took 300ms on i7-8650U in 2020. 30ms should be safe for cancellation delay. var cts = new CancellationTokenSource();
string hugeFile = Path.Combine( var file = Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, TestImages.Jpeg.Baseline.Jpeg420Small);
TestEnvironment.InputImagesDirectoryFullPath, using var pausedStream = new PausedStream(file);
fileName); pausedStream.OnWaiting(s =>
const int numberOfRuns = 5;
for (int i = 0; i < numberOfRuns; i++)
{ {
var cts = new CancellationTokenSource(); if (s.Position >= s.Length * percentageOfStreamReadToCancel)
if (cancellationDelayMs == 0)
{ {
cts.Cancel(); cts.Cancel();
pausedStream.Release();
} }
else else
{ {
cts.CancelAfter(cancellationDelayMs); // allows this/next wait to unblock
} pausedStream.Next();
try
{
using Image image = await Image.LoadAsync(hugeFile, cts.Token);
}
catch (TaskCanceledException)
{
// Successfully observed a cancellation
return;
} }
} });
throw new Exception($"No cancellation happened out of {numberOfRuns} runs!"); var config = Configuration.CreateDefaultInstance();
config.FileSystem = new SingleStreamFileSystem(pausedStream);
await Assert.ThrowsAsync<TaskCanceledException>(async () =>
{
using Image image = await Image.LoadAsync(config, "someFakeFile", cts.Token);
});
} }
[Theory(Skip = "Identify is too fast, doesn't work reliably.")] [Fact]
[InlineData(TestImages.Jpeg.Baseline.Exif)] public async Task Identify_IsCancellable()
[InlineData(TestImages.Jpeg.Progressive.Bad.ExifUndefType)]
public async Task Identify_IsCancellable(string fileName)
{ {
string file = Path.Combine(
TestEnvironment.InputImagesDirectoryFullPath,
fileName);
var cts = new CancellationTokenSource(); var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromTicks(1));
await Assert.ThrowsAsync<TaskCanceledException>(() => Image.IdentifyAsync(file, cts.Token)); var file = Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, TestImages.Jpeg.Baseline.Jpeg420Small);
using var pausedStream = new PausedStream(file);
pausedStream.OnWaiting(s =>
{
cts.Cancel();
pausedStream.Release();
});
var config = Configuration.CreateDefaultInstance();
config.FileSystem = new SingleStreamFileSystem(pausedStream);
await Assert.ThrowsAsync<TaskCanceledException>(async () => await Image.IdentifyAsync(config, "someFakeFile", cts.Token));
} }
// DEBUG ONLY! // DEBUG ONLY!

44
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.Metadata.Profiles.Iptc;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Tests.TestUtilities;
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
using Xunit; using Xunit;
@ -309,29 +310,34 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
Assert.Equal(values.Entries, actual.Entries); Assert.Equal(values.Entries, actual.Entries);
} }
[Theory(Skip = "TODO: Too Flaky")] [Theory]
[InlineData(JpegSubsample.Ratio420, 0)] [InlineData(JpegSubsample.Ratio420)]
[InlineData(JpegSubsample.Ratio420, 3)] [InlineData(JpegSubsample.Ratio444)]
[InlineData(JpegSubsample.Ratio420, 10)] public async Task Encode_IsCancellable(JpegSubsample subsample)
[InlineData(JpegSubsample.Ratio444, 0)]
[InlineData(JpegSubsample.Ratio444, 3)]
[InlineData(JpegSubsample.Ratio444, 10)]
public async Task Encode_IsCancellable(JpegSubsample subsample, int cancellationDelayMs)
{ {
using var image = new Image<Rgba32>(5000, 5000);
using var stream = new MemoryStream();
var cts = new CancellationTokenSource(); var cts = new CancellationTokenSource();
if (cancellationDelayMs == 0) using var pausedStream = new PausedStream(new MemoryStream());
{ pausedStream.OnWaiting(s =>
cts.Cancel();
}
else
{ {
cts.CancelAfter(cancellationDelayMs); // after some writing
} if (s.Position >= 500)
{
cts.Cancel();
pausedStream.Release();
}
else
{
// allows this/next wait to unblock
pausedStream.Next();
}
});
var encoder = new JpegEncoder() { Subsample = subsample }; using var image = new Image<Rgba32>(5000, 5000);
await Assert.ThrowsAsync<TaskCanceledException>(() => image.SaveAsync(stream, encoder, cts.Token)); await Assert.ThrowsAsync<TaskCanceledException>(async () =>
{
var encoder = new JpegEncoder() { Subsample = subsample };
await image.SaveAsync(pausedStream, encoder, cts.Token);
});
} }
} }
} }

11
tests/ImageSharp.Tests/Image/ImageTests.SaveAsync.cs

@ -140,10 +140,15 @@ namespace SixLabors.ImageSharp.Tests
using var stream = new MemoryStream(); using var stream = new MemoryStream();
var asyncStream = new AsyncStreamWrapper(stream, () => false); var asyncStream = new AsyncStreamWrapper(stream, () => false);
var cts = new CancellationTokenSource(); var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromTicks(1));
await Assert.ThrowsAnyAsync<TaskCanceledException>(() => var pausedStream = new PausedStream(asyncStream);
image.SaveAsync(asyncStream, encoder, cts.Token)); pausedStream.OnWaiting(s =>
{
cts.Cancel();
pausedStream.Release();
});
await Assert.ThrowsAsync<TaskCanceledException>(async () => await image.SaveAsync(pausedStream, encoder, cts.Token));
} }
} }
} }

145
tests/ImageSharp.Tests/TestUtilities/PausedStream.cs

@ -0,0 +1,145 @@
// 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 semaphore = new SemaphoreSlim(0);
private readonly CancellationTokenSource cancelationTokenSource = new CancellationTokenSource();
private readonly Stream innerStream;
private Action<Stream> onWaitingCallback;
public void OnWaiting(Action<Stream> onWaitingCallback) => this.onWaitingCallback = onWaitingCallback;
public void OnWaiting(Action onWaitingCallback) => this.OnWaiting(_ => onWaitingCallback());
public void Release()
{
this.semaphore.Release();
this.cancelationTokenSource.Cancel();
}
public void Next() => this.semaphore.Release();
private void Wait()
{
if (this.cancelationTokenSource.IsCancellationRequested)
{
return;
}
this.onWaitingCallback?.Invoke(this.innerStream);
try
{
this.semaphore.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 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 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<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 ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default) => this.Await(() => this.innerStream.WriteAsync(buffer, cancellationToken));
#endif
}
}

19
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;
}
}
Loading…
Cancel
Save