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 }) 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