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 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 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(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(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 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( TaskCreationOptions.RunContinuationsAsynchronously); var release = NewSignal(); var key = CreateSourceOperationKey("priority"); async Task 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(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(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); }