mirror of
https://github.com/AtomUI/AtomUI.git
synced 2026-10-22 17:35:30 +08:00
318 lines
11 KiB
C#
318 lines
11 KiB
C#
using AtomUI.Controls;
|
|
using Shouldly;
|
|
using Xunit;
|
|
|
|
namespace AtomUI.Controls.Shared.Tests.ImageLoading;
|
|
|
|
public class ImageRequestCoordinatorTests
|
|
{
|
|
[Fact]
|
|
public async Task Decoded_Waiters_Share_One_Operation_And_Acquire_Leases_Before_Release()
|
|
{
|
|
using var coordinator = new ImageRequestCoordinator();
|
|
var key = CreateDecodedOperationKey("shared");
|
|
var started = NewSignal();
|
|
var release = NewSignal();
|
|
var image = new TestImage();
|
|
var factoryCalls = 0;
|
|
|
|
async Task<ImageDecodedCacheEntry> Factory(
|
|
ImageRequestCoordinator.SharedOperationContext context,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
Interlocked.Increment(ref factoryCalls);
|
|
started.TrySetResult();
|
|
await release.Task.WaitAsync(cancellationToken);
|
|
var entry = CreateOperationEntry(image);
|
|
entry.RetainOperation();
|
|
return entry;
|
|
}
|
|
|
|
var firstTask = coordinator.GetDecodedAsync(
|
|
key,
|
|
new ImageRequestPriorityState(ImageRequestPriority.Normal),
|
|
Factory,
|
|
entry => entry.AcquireResult(
|
|
ImageLoadOrigin.Local,
|
|
ImageSourceValidation.Current,
|
|
contentId: null),
|
|
null,
|
|
CancellationToken.None);
|
|
await started.Task;
|
|
var secondTask = coordinator.GetDecodedAsync(
|
|
key,
|
|
new ImageRequestPriorityState(ImageRequestPriority.High),
|
|
Factory,
|
|
entry => entry.AcquireResult(
|
|
ImageLoadOrigin.Local,
|
|
ImageSourceValidation.Current,
|
|
contentId: null),
|
|
null,
|
|
CancellationToken.None);
|
|
release.TrySetResult();
|
|
|
|
using var first = await firstTask;
|
|
using var second = await secondTask;
|
|
|
|
factoryCalls.ShouldBe(1);
|
|
first.Image.ShouldBeSameAs(image);
|
|
second.Image.ShouldBeSameAs(image);
|
|
image.DisposeCount.ShouldBe(0);
|
|
first.Dispose();
|
|
image.DisposeCount.ShouldBe(0);
|
|
second.Dispose();
|
|
image.DisposeCount.ShouldBe(1);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Canceling_One_Waiter_Does_Not_Cancel_Shared_Work_For_Remaining_Waiter()
|
|
{
|
|
using var coordinator = new ImageRequestCoordinator();
|
|
using var firstCancellation = new CancellationTokenSource();
|
|
var key = CreateSourceOperationKey("cancel-one");
|
|
var started = NewSignal();
|
|
var release = NewSignal();
|
|
var underlyingCanceled = false;
|
|
|
|
async Task<ImageValidatedContent> Factory(
|
|
ImageRequestCoordinator.SharedOperationContext context,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
started.TrySetResult();
|
|
try
|
|
{
|
|
await release.Task.WaitAsync(cancellationToken);
|
|
return CreateValidated([1]);
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
underlyingCanceled = true;
|
|
throw;
|
|
}
|
|
}
|
|
|
|
var first = coordinator.GetSourceAsync(
|
|
key,
|
|
new ImageRequestPriorityState(ImageRequestPriority.Normal),
|
|
Factory,
|
|
null,
|
|
firstCancellation.Token);
|
|
await started.Task;
|
|
var second = coordinator.GetSourceAsync(
|
|
key,
|
|
new ImageRequestPriorityState(ImageRequestPriority.Normal),
|
|
Factory,
|
|
null,
|
|
CancellationToken.None);
|
|
|
|
firstCancellation.Cancel();
|
|
await Should.ThrowAsync<OperationCanceledException>(async () => await first);
|
|
underlyingCanceled.ShouldBeFalse();
|
|
|
|
release.TrySetResult();
|
|
(await second).Content.Bytes.ShouldBe([1]);
|
|
underlyingCanceled.ShouldBeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Last_Waiter_Cancellation_Cancels_Underlying_Operation()
|
|
{
|
|
using var coordinator = new ImageRequestCoordinator();
|
|
using var cancellation = new CancellationTokenSource();
|
|
var started = NewSignal();
|
|
var underlyingCanceled = NewSignal();
|
|
|
|
var task = coordinator.GetSourceAsync(
|
|
CreateSourceOperationKey("cancel-last"),
|
|
new ImageRequestPriorityState(ImageRequestPriority.Normal),
|
|
async (_, token) =>
|
|
{
|
|
started.TrySetResult();
|
|
try
|
|
{
|
|
await Task.Delay(Timeout.InfiniteTimeSpan, token);
|
|
throw new InvalidOperationException();
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
underlyingCanceled.TrySetResult();
|
|
throw;
|
|
}
|
|
},
|
|
null,
|
|
cancellation.Token);
|
|
await started.Task;
|
|
|
|
cancellation.Cancel();
|
|
|
|
await Should.ThrowAsync<OperationCanceledException>(async () => await task);
|
|
await underlyingCanceled.Task;
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Completed_Operation_Is_Not_Reused_By_A_New_Waiter()
|
|
{
|
|
using var coordinator = new ImageRequestCoordinator();
|
|
var calls = 0;
|
|
var key = CreateSourceOperationKey("completed");
|
|
|
|
Task<ImageValidatedContent> Factory(
|
|
ImageRequestCoordinator.SharedOperationContext context,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
return Task.FromResult(CreateValidated([
|
|
checked((byte)Interlocked.Increment(ref calls))
|
|
]));
|
|
}
|
|
|
|
var first = await coordinator.GetSourceAsync(
|
|
key,
|
|
new ImageRequestPriorityState(ImageRequestPriority.Normal),
|
|
Factory,
|
|
null,
|
|
CancellationToken.None);
|
|
var second = await coordinator.GetSourceAsync(
|
|
key,
|
|
new ImageRequestPriorityState(ImageRequestPriority.Normal),
|
|
Factory,
|
|
null,
|
|
CancellationToken.None);
|
|
|
|
first.Content.Bytes.ShouldBe([1]);
|
|
second.Content.Bytes.ShouldBe([2]);
|
|
calls.ShouldBe(2);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Shared_Operation_Priority_Tracks_Highest_Active_Waiter()
|
|
{
|
|
using var coordinator = new ImageRequestCoordinator();
|
|
using var criticalCancellation = new CancellationTokenSource();
|
|
var contextSignal = new TaskCompletionSource<ImageRequestCoordinator.SharedOperationContext>(
|
|
TaskCreationOptions.RunContinuationsAsynchronously);
|
|
var release = NewSignal();
|
|
var key = CreateSourceOperationKey("priority");
|
|
|
|
async Task<ImageValidatedContent> Factory(
|
|
ImageRequestCoordinator.SharedOperationContext context,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
contextSignal.TrySetResult(context);
|
|
await release.Task.WaitAsync(cancellationToken);
|
|
return CreateValidated([1]);
|
|
}
|
|
|
|
var preloadPriority = new ImageRequestPriorityState(ImageRequestPriority.Preload);
|
|
var preload = coordinator.GetSourceAsync(
|
|
key,
|
|
preloadPriority,
|
|
Factory,
|
|
null,
|
|
CancellationToken.None);
|
|
var context = await contextSignal.Task;
|
|
context.Priority.ShouldBe(ImageRequestPriority.Preload);
|
|
preloadPriority.Promote(ImageRequestPriority.High);
|
|
context.Priority.ShouldBe(ImageRequestPriority.High);
|
|
var critical = coordinator.GetSourceAsync(
|
|
key,
|
|
new ImageRequestPriorityState(ImageRequestPriority.Critical),
|
|
Factory,
|
|
null,
|
|
criticalCancellation.Token);
|
|
context.Priority.ShouldBe(ImageRequestPriority.Critical);
|
|
|
|
criticalCancellation.Cancel();
|
|
await Should.ThrowAsync<OperationCanceledException>(async () => await critical);
|
|
context.Priority.ShouldBe(ImageRequestPriority.High);
|
|
|
|
release.TrySetResult();
|
|
await preload;
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Dispose_Cancels_InFlight_Work_Without_Waiting_For_The_Factory_To_Return()
|
|
{
|
|
var coordinator = new ImageRequestCoordinator();
|
|
var started = NewSignal();
|
|
var canceled = NewSignal();
|
|
var release = NewSignal();
|
|
var loadTask = coordinator.GetSourceAsync(
|
|
CreateSourceOperationKey("dispose-non-blocking"),
|
|
new ImageRequestPriorityState(ImageRequestPriority.Normal),
|
|
async (_, token) =>
|
|
{
|
|
using var registration = token.Register(() => canceled.TrySetResult());
|
|
started.TrySetResult();
|
|
await release.Task;
|
|
return CreateValidated([1]);
|
|
},
|
|
null,
|
|
CancellationToken.None);
|
|
await started.Task;
|
|
|
|
var disposeTask = Task.Run(coordinator.Dispose, TestContext.Current.CancellationToken);
|
|
await canceled.Task;
|
|
var returnedBeforeFactory = false;
|
|
try
|
|
{
|
|
await disposeTask.WaitAsync(TimeSpan.FromSeconds(1), TestContext.Current.CancellationToken);
|
|
returnedBeforeFactory = true;
|
|
}
|
|
catch (TimeoutException)
|
|
{
|
|
}
|
|
finally
|
|
{
|
|
release.TrySetResult();
|
|
await disposeTask;
|
|
}
|
|
|
|
returnedBeforeFactory.ShouldBeTrue();
|
|
await Should.ThrowAsync<OperationCanceledException>(async () => await loadTask);
|
|
}
|
|
|
|
private static ImageDecodedCacheEntry CreateOperationEntry(TestImage image)
|
|
{
|
|
return new ImageDecodedCacheEntry(
|
|
image,
|
|
ownsImage: true,
|
|
10,
|
|
10,
|
|
10,
|
|
10,
|
|
400,
|
|
"image/png");
|
|
}
|
|
|
|
private static ImageValidatedContent CreateValidated(byte[] bytes)
|
|
{
|
|
return new ImageValidatedContent(
|
|
ImageLoadingTestSupport.CreateContent(bytes).MarkValidated(),
|
|
new ImageProbeResult(ImageContentFormat.Png, "image/png", 1, 1, false),
|
|
ImageSourceValidation.Current);
|
|
}
|
|
|
|
private static ImageSourceOperationKey CreateSourceOperationKey(string value)
|
|
{
|
|
return new ImageSourceOperationKey(
|
|
new ImageSourceKey(value, string.Empty),
|
|
ImageCacheReadPolicy.ValidateSource,
|
|
ImageCacheStoragePolicy.MemoryAndDisk,
|
|
string.Empty);
|
|
}
|
|
|
|
private static ImageDecodedOperationKey CreateDecodedOperationKey(string value)
|
|
{
|
|
return new ImageDecodedOperationKey(
|
|
new ImageDecodeKey(
|
|
string.Empty,
|
|
new ImageContentId(ImageCacheKey.Hash(value)),
|
|
new ImageDecodeSpec(10, 10, "codec")),
|
|
ImageCacheStoragePolicy.MemoryAndDisk,
|
|
string.Empty);
|
|
}
|
|
|
|
private static TaskCompletionSource NewSignal() =>
|
|
new(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
}
|