Skip to content

Commit f2deb47

Browse files
Merge pull request #1644 from SixLabors/sw/iscancellable-tests
Stop using timeouts to test cancellation token compliance.
2 parents 841f896 + 695cfd3 commit f2deb47

File tree

5 files changed

+232
-63
lines changed

5 files changed

+232
-63
lines changed

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

Lines changed: 35 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
using SixLabors.ImageSharp.Memory;
1313
using SixLabors.ImageSharp.PixelFormats;
1414
using SixLabors.ImageSharp.Tests.Formats.Jpg.Utils;
15+
using SixLabors.ImageSharp.Tests.TestUtilities;
1516
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
1617

1718
using Xunit;
@@ -127,60 +128,53 @@ public async Task DecodeAsync_DegenerateMemoryRequest_ShouldTranslateTo_ImageFor
127128
}
128129

129130
[Theory]
130-
[InlineData(TestImages.Jpeg.Baseline.Jpeg420Small, 0)]
131-
[InlineData(TestImages.Jpeg.Issues.ExifGetString750Transform, 1)]
132-
[InlineData(TestImages.Jpeg.Issues.ExifGetString750Transform, 15)]
133-
[InlineData(TestImages.Jpeg.Issues.ExifGetString750Transform, 30)]
134-
[InlineData(TestImages.Jpeg.Issues.BadRstProgressive518, 1)]
135-
[InlineData(TestImages.Jpeg.Issues.BadRstProgressive518, 15)]
136-
[InlineData(TestImages.Jpeg.Issues.BadRstProgressive518, 30)]
137-
public async Task Decode_IsCancellable(string fileName, int cancellationDelayMs)
131+
[InlineData(0)]
132+
[InlineData(0.5)]
133+
[InlineData(0.9)]
134+
public async Task Decode_IsCancellable(int percentageOfStreamReadToCancel)
138135
{
139-
// Decoding these huge files took 300ms on i7-8650U in 2020. 30ms should be safe for cancellation delay.
140-
string hugeFile = Path.Combine(
141-
TestEnvironment.InputImagesDirectoryFullPath,
142-
fileName);
143-
144-
const int numberOfRuns = 5;
145-
146-
for (int i = 0; i < numberOfRuns; i++)
136+
var cts = new CancellationTokenSource();
137+
var file = Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, TestImages.Jpeg.Baseline.Jpeg420Small);
138+
using var pausedStream = new PausedStream(file);
139+
pausedStream.OnWaiting(s =>
147140
{
148-
var cts = new CancellationTokenSource();
149-
if (cancellationDelayMs == 0)
141+
if (s.Position >= s.Length * percentageOfStreamReadToCancel)
150142
{
151143
cts.Cancel();
144+
pausedStream.Release();
152145
}
153146
else
154147
{
155-
cts.CancelAfter(cancellationDelayMs);
156-
}
157-
158-
try
159-
{
160-
using Image image = await Image.LoadAsync(hugeFile, cts.Token);
161-
}
162-
catch (TaskCanceledException)
163-
{
164-
// Successfully observed a cancellation
165-
return;
148+
// allows this/next wait to unblock
149+
pausedStream.Next();
166150
}
167-
}
151+
});
168152

169-
throw new Exception($"No cancellation happened out of {numberOfRuns} runs!");
153+
var config = Configuration.CreateDefaultInstance();
154+
config.FileSystem = new SingleStreamFileSystem(pausedStream);
155+
await Assert.ThrowsAsync<TaskCanceledException>(async () =>
156+
{
157+
using Image image = await Image.LoadAsync(config, "someFakeFile", cts.Token);
158+
});
170159
}
171160

172-
[Theory(Skip = "Identify is too fast, doesn't work reliably.")]
173-
[InlineData(TestImages.Jpeg.Baseline.Exif)]
174-
[InlineData(TestImages.Jpeg.Progressive.Bad.ExifUndefType)]
175-
public async Task Identify_IsCancellable(string fileName)
161+
[Fact]
162+
public async Task Identify_IsCancellable()
176163
{
177-
string file = Path.Combine(
178-
TestEnvironment.InputImagesDirectoryFullPath,
179-
fileName);
180-
181164
var cts = new CancellationTokenSource();
182-
cts.CancelAfter(TimeSpan.FromTicks(1));
183-
await Assert.ThrowsAsync<TaskCanceledException>(() => Image.IdentifyAsync(file, cts.Token));
165+
166+
var file = Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, TestImages.Jpeg.Baseline.Jpeg420Small);
167+
using var pausedStream = new PausedStream(file);
168+
pausedStream.OnWaiting(s =>
169+
{
170+
cts.Cancel();
171+
pausedStream.Release();
172+
});
173+
174+
var config = Configuration.CreateDefaultInstance();
175+
config.FileSystem = new SingleStreamFileSystem(pausedStream);
176+
177+
await Assert.ThrowsAsync<TaskCanceledException>(async () => await Image.IdentifyAsync(config, "someFakeFile", cts.Token));
184178
}
185179

186180
// DEBUG ONLY!

tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
using SixLabors.ImageSharp.Metadata.Profiles.Iptc;
1414
using SixLabors.ImageSharp.PixelFormats;
1515
using SixLabors.ImageSharp.Processing;
16+
using SixLabors.ImageSharp.Tests.TestUtilities;
1617
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
1718

1819
using Xunit;
@@ -309,29 +310,34 @@ public void Encode_PreservesIccProfile()
309310
Assert.Equal(values.Entries, actual.Entries);
310311
}
311312

312-
[Theory(Skip = "TODO: Too Flaky")]
313-
[InlineData(JpegSubsample.Ratio420, 0)]
314-
[InlineData(JpegSubsample.Ratio420, 3)]
315-
[InlineData(JpegSubsample.Ratio420, 10)]
316-
[InlineData(JpegSubsample.Ratio444, 0)]
317-
[InlineData(JpegSubsample.Ratio444, 3)]
318-
[InlineData(JpegSubsample.Ratio444, 10)]
319-
public async Task Encode_IsCancellable(JpegSubsample subsample, int cancellationDelayMs)
313+
[Theory]
314+
[InlineData(JpegSubsample.Ratio420)]
315+
[InlineData(JpegSubsample.Ratio444)]
316+
public async Task Encode_IsCancellable(JpegSubsample subsample)
320317
{
321-
using var image = new Image<Rgba32>(5000, 5000);
322-
using var stream = new MemoryStream();
323318
var cts = new CancellationTokenSource();
324-
if (cancellationDelayMs == 0)
325-
{
326-
cts.Cancel();
327-
}
328-
else
319+
using var pausedStream = new PausedStream(new MemoryStream());
320+
pausedStream.OnWaiting(s =>
329321
{
330-
cts.CancelAfter(cancellationDelayMs);
331-
}
322+
// after some writing
323+
if (s.Position >= 500)
324+
{
325+
cts.Cancel();
326+
pausedStream.Release();
327+
}
328+
else
329+
{
330+
// allows this/next wait to unblock
331+
pausedStream.Next();
332+
}
333+
});
332334

333-
var encoder = new JpegEncoder() { Subsample = subsample };
334-
await Assert.ThrowsAsync<TaskCanceledException>(() => image.SaveAsync(stream, encoder, cts.Token));
335+
using var image = new Image<Rgba32>(5000, 5000);
336+
await Assert.ThrowsAsync<TaskCanceledException>(async () =>
337+
{
338+
var encoder = new JpegEncoder() { Subsample = subsample };
339+
await image.SaveAsync(pausedStream, encoder, cts.Token);
340+
});
335341
}
336342
}
337343
}

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

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -140,10 +140,15 @@ public async Task SaveAsync_WithNonSeekableStream_IsCancellable()
140140
using var stream = new MemoryStream();
141141
var asyncStream = new AsyncStreamWrapper(stream, () => false);
142142
var cts = new CancellationTokenSource();
143-
cts.CancelAfter(TimeSpan.FromTicks(1));
144143

145-
await Assert.ThrowsAnyAsync<TaskCanceledException>(() =>
146-
image.SaveAsync(asyncStream, encoder, cts.Token));
144+
var pausedStream = new PausedStream(asyncStream);
145+
pausedStream.OnWaiting(s =>
146+
{
147+
cts.Cancel();
148+
pausedStream.Release();
149+
});
150+
151+
await Assert.ThrowsAsync<TaskCanceledException>(async () => await image.SaveAsync(pausedStream, encoder, cts.Token));
147152
}
148153
}
149154
}
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Apache License, Version 2.0.
3+
4+
using System;
5+
using System.IO;
6+
using System.Threading;
7+
using System.Threading.Tasks;
8+
9+
namespace SixLabors.ImageSharp.Tests.TestUtilities
10+
{
11+
public class PausedStream : Stream
12+
{
13+
private readonly SemaphoreSlim semaphore = new SemaphoreSlim(0);
14+
15+
private readonly CancellationTokenSource cancelationTokenSource = new CancellationTokenSource();
16+
17+
private readonly Stream innerStream;
18+
private Action<Stream> onWaitingCallback;
19+
20+
public void OnWaiting(Action<Stream> onWaitingCallback) => this.onWaitingCallback = onWaitingCallback;
21+
22+
public void OnWaiting(Action onWaitingCallback) => this.OnWaiting(_ => onWaitingCallback());
23+
24+
public void Release()
25+
{
26+
this.semaphore.Release();
27+
this.cancelationTokenSource.Cancel();
28+
}
29+
30+
public void Next() => this.semaphore.Release();
31+
32+
private void Wait()
33+
{
34+
if (this.cancelationTokenSource.IsCancellationRequested)
35+
{
36+
return;
37+
}
38+
39+
this.onWaitingCallback?.Invoke(this.innerStream);
40+
41+
try
42+
{
43+
this.semaphore.Wait(this.cancelationTokenSource.Token);
44+
}
45+
catch (OperationCanceledException)
46+
{
47+
// ignore this as its just used to unlock any waits in progress
48+
}
49+
}
50+
51+
private async Task Await(Func<Task> action)
52+
{
53+
await Task.Yield();
54+
this.Wait();
55+
await action();
56+
}
57+
58+
private async Task<T> Await<T>(Func<Task<T>> action)
59+
{
60+
await Task.Yield();
61+
this.Wait();
62+
return await action();
63+
}
64+
65+
private T Await<T>(Func<T> action)
66+
{
67+
this.Wait();
68+
return action();
69+
}
70+
71+
private void Await(Action action)
72+
{
73+
this.Wait();
74+
action();
75+
}
76+
77+
public PausedStream(byte[] data)
78+
: this(new MemoryStream(data))
79+
{
80+
}
81+
82+
public PausedStream(string filePath)
83+
: this(File.OpenRead(filePath))
84+
{
85+
}
86+
87+
public PausedStream(Stream innerStream) => this.innerStream = innerStream;
88+
89+
public override bool CanTimeout => this.innerStream.CanTimeout;
90+
91+
public override void Close() => this.Await(() => this.innerStream.Close());
92+
93+
public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) => this.Await(() => this.innerStream.CopyToAsync(destination, bufferSize, cancellationToken));
94+
95+
public override bool CanRead => this.innerStream.CanRead;
96+
97+
public override bool CanSeek => this.innerStream.CanSeek;
98+
99+
public override bool CanWrite => this.innerStream.CanWrite;
100+
101+
public override long Length => this.Await(() => this.innerStream.Length);
102+
103+
public override long Position { get => this.Await(() => this.innerStream.Position); set => this.Await(() => this.innerStream.Position = value); }
104+
105+
public override void Flush() => this.Await(() => this.innerStream.Flush());
106+
107+
public override int Read(byte[] buffer, int offset, int count) => this.Await(() => this.innerStream.Read(buffer, offset, count));
108+
109+
public override long Seek(long offset, SeekOrigin origin) => this.Await(() => this.innerStream.Seek(offset, origin));
110+
111+
public override void SetLength(long value) => this.Await(() => this.innerStream.SetLength(value));
112+
113+
public override void Write(byte[] buffer, int offset, int count) => this.Await(() => this.innerStream.Write(buffer, offset, count));
114+
115+
public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => this.Await(() => this.innerStream.ReadAsync(buffer, offset, count, cancellationToken));
116+
117+
public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => this.Await(() => this.innerStream.WriteAsync(buffer, offset, count, cancellationToken));
118+
119+
public override void WriteByte(byte value) => this.Await(() => this.innerStream.WriteByte(value));
120+
121+
public override int ReadByte() => this.Await(() => this.innerStream.ReadByte());
122+
123+
protected override void Dispose(bool disposing) => this.innerStream.Dispose();
124+
125+
#if NETCOREAPP
126+
public override void CopyTo(Stream destination, int bufferSize) => this.Await(() => this.innerStream.CopyTo(destination, bufferSize));
127+
128+
public override int Read(Span<byte> buffer)
129+
{
130+
this.Wait();
131+
return this.innerStream.Read(buffer);
132+
}
133+
134+
public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default) => this.Await(() => this.innerStream.ReadAsync(buffer, cancellationToken));
135+
136+
public override void Write(ReadOnlySpan<byte> buffer)
137+
{
138+
this.Wait();
139+
this.innerStream.Write(buffer);
140+
}
141+
142+
public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default) => this.Await(() => this.innerStream.WriteAsync(buffer, cancellationToken));
143+
#endif
144+
}
145+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Apache License, Version 2.0.
3+
4+
using System.IO;
5+
using SixLabors.ImageSharp.IO;
6+
7+
namespace SixLabors.ImageSharp.Tests.TestUtilities
8+
{
9+
internal class SingleStreamFileSystem : IFileSystem
10+
{
11+
private readonly Stream stream;
12+
13+
public SingleStreamFileSystem(Stream stream) => this.stream = stream;
14+
15+
Stream IFileSystem.Create(string path) => this.stream;
16+
17+
Stream IFileSystem.OpenRead(string path) => this.stream;
18+
}
19+
}

0 commit comments

Comments
 (0)