From b2e1de2b294fd62df6079ed623f75010394454a5 Mon Sep 17 00:00:00 2001 From: brennobemoura <37243584+brennobemoura@users.noreply.github.com> Date: Sun, 18 Oct 2026 08:26:43 -0300 Subject: [PATCH 1/3] Test that reusing or cancelling an image view interrupts the request The tests that exist showed that the completion does not fire once a load is superseded. These check the request itself is interrupted, for the UIKit and AppKit integrations. Co-Authored-By: Claude Sonnet 5.5 --- .../AppKit/NSImageViewRequestDLTests.swift | 70 +++++++++++++++++++ .../UIKit/UIImageViewRequestDLTests.swift | 70 +++++++++++++++++++ 2 files changed, 140 insertions(+) diff --git a/Tests/RequestDLTests/Image/AppKit/NSImageViewRequestDLTests.swift b/Tests/RequestDLTests/Image/AppKit/NSImageViewRequestDLTests.swift index 8c512e04..444b2cd6 100644 --- a/Tests/RequestDLTests/Image/AppKit/NSImageViewRequestDLTests.swift +++ b/Tests/RequestDLTests/Image/AppKit/NSImageViewRequestDLTests.swift @@ -7,6 +7,7 @@ import SwiftAsyncStream import Testing @_spi(Private) @testable import RequestDL +@testable import RequestDLTestSupport #if canImport(AppKit) && !targetEnvironment(macCatalyst) import AppKit @@ -107,6 +108,75 @@ struct NSImageViewRequestDLTests { // Then #expect(await !completed.value) } + + /// The completion not firing is not the same as the request being stopped: a cell that is + /// reused has to free the connection of the image it no longer shows. + @Test + func reusingTheImageViewInterruptsTheRequestOfThePreviousLoad() async throws { + // Given + let imageView = NSImageView() + let started = Flag() + let interrupted = Flag() + + imageView.rdl.setImage( + id: "interrupted-by-reuse-\(UUID())", + task: StubImageTask { + await started.set() + + do { + try await Task.sleep(nanoseconds: 60_000_000_000) + } catch { + await interrupted.set() + throw error + } + + return .init(head: .stub, payload: .onePixelPNG) + } + ) + + try await eventually { await started.value } + + // When + imageView.rdl.setImage( + id: "replacement-\(UUID())", + task: StubImageTask { .init(head: .stub, payload: .onePixelPNG) } + ) + + // Then + try await eventually { await interrupted.value } + } + + @Test + func cancelInterruptsTheRequestOfThePendingLoad() async throws { + // Given + let imageView = NSImageView() + let started = Flag() + let interrupted = Flag() + + imageView.rdl.setImage( + id: "interrupted-by-cancel-\(UUID())", + task: StubImageTask { + await started.set() + + do { + try await Task.sleep(nanoseconds: 60_000_000_000) + } catch { + await interrupted.set() + throw error + } + + return .init(head: .stub, payload: .onePixelPNG) + } + ) + + try await eventually { await started.value } + + // When + imageView.rdl.cancel() + + // Then + try await eventually { await interrupted.value } + } } // MARK: - Test helpers diff --git a/Tests/RequestDLTests/Image/UIKit/UIImageViewRequestDLTests.swift b/Tests/RequestDLTests/Image/UIKit/UIImageViewRequestDLTests.swift index e6ca843c..fe6c5cf4 100644 --- a/Tests/RequestDLTests/Image/UIKit/UIImageViewRequestDLTests.swift +++ b/Tests/RequestDLTests/Image/UIKit/UIImageViewRequestDLTests.swift @@ -7,6 +7,7 @@ import SwiftAsyncStream import Testing @_spi(Private) @testable import RequestDL +@testable import RequestDLTestSupport #if canImport(UIKit) && !os(watchOS) import UIKit @@ -107,6 +108,75 @@ struct UIImageViewRequestDLTests { // Then #expect(await !completed.value) } + + /// The completion not firing is not the same as the request being stopped: a cell that is + /// reused has to free the connection of the image it no longer shows. + @Test + func reusingTheImageViewInterruptsTheRequestOfThePreviousLoad() async throws { + // Given + let imageView = UIImageView() + let started = Flag() + let interrupted = Flag() + + imageView.rdl.setImage( + id: "interrupted-by-reuse-\(UUID())", + task: StubImageTask { + await started.set() + + do { + try await Task.sleep(nanoseconds: 60_000_000_000) + } catch { + await interrupted.set() + throw error + } + + return .init(head: .stub, payload: .onePixelPNG) + } + ) + + try await eventually { await started.value } + + // When + imageView.rdl.setImage( + id: "replacement-\(UUID())", + task: StubImageTask { .init(head: .stub, payload: .onePixelPNG) } + ) + + // Then + try await eventually { await interrupted.value } + } + + @Test + func cancelInterruptsTheRequestOfThePendingLoad() async throws { + // Given + let imageView = UIImageView() + let started = Flag() + let interrupted = Flag() + + imageView.rdl.setImage( + id: "interrupted-by-cancel-\(UUID())", + task: StubImageTask { + await started.set() + + do { + try await Task.sleep(nanoseconds: 60_000_000_000) + } catch { + await interrupted.set() + throw error + } + + return .init(head: .stub, payload: .onePixelPNG) + } + ) + + try await eventually { await started.value } + + // When + imageView.rdl.cancel() + + // Then + try await eventually { await interrupted.value } + } } // MARK: - Test helpers From 0b400f4bd39c7b349271808dfc9311c89edebe94 Mon Sep 17 00:00:00 2001 From: brennobemoura <37243584+brennobemoura@users.noreply.github.com> Date: Sun, 18 Oct 2026 11:17:57 -0300 Subject: [PATCH 2/3] Re-run the CI with the breaking-changes label Co-Authored-By: Claude Sonnet 5.5 From 21fdef2fdf9a2bdc218d95d21c306f97e0301525 Mon Sep 17 00:00:00 2001 From: brennobemoura <37243584+brennobemoura@users.noreply.github.com> Date: Sun, 18 Oct 2026 11:33:40 -0300 Subject: [PATCH 3/3] Bound the busy-client test by what a wait would cost, not by one second A caller that waited for a busy client to free up would wait for a 60 s request or for ever; a loaded Linux runner takes over a second (1.33 s and 1.02 s seen) on a call that only does bookkeeping. Co-Authored-By: Claude Sonnet 5.5 --- .../Client Manager/InternalsClientManagerTests.swift | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/Tests/RequestDLInternalsTests/Sources/Client/Client Manager/InternalsClientManagerTests.swift b/Tests/RequestDLInternalsTests/Sources/Client/Client Manager/InternalsClientManagerTests.swift index 6ce8514e..a64e69b1 100644 --- a/Tests/RequestDLInternalsTests/Sources/Client/Client Manager/InternalsClientManagerTests.swift +++ b/Tests/RequestDLInternalsTests/Sources/Client/Client Manager/InternalsClientManagerTests.swift @@ -287,12 +287,15 @@ struct InternalsClientManagerTests { sessionConfiguration: overflowing ) - // Then: served straight away, the ceiling overshot rather than enforced, and nothing - // in flight was torn down to get there. + // Then: served without waiting for a busy client, the ceiling overshot rather than + // enforced, and nothing in flight was torn down to get there. The bound is not a + // speed claim: a caller that waited for a client to free up would wait for the 60 s + // request that holds it, or for ever, and a loaded runner takes a second or more + // over a call that does nothing but bookkeeping. #if canImport(Darwin) - #expect(DispatchTime.now().uptimeNanoseconds - start < 1_000_000_000) + #expect(DispatchTime.now().uptimeNanoseconds - start < 30_000_000_000) #else - #expect(ContinuousClock.now - start < .seconds(1)) + #expect(ContinuousClock.now - start < .seconds(30)) #endif #expect(manager.count == maximumCount + 1) #expect(busy.allSatisfy { $0.isRunning })