From 8bc6c5903b7f6b0dc67a2313109fdf9ed75a407f Mon Sep 17 00:00:00 2001 From: kean Date: Wed, 1 May 2024 22:03:59 -0400 Subject: [PATCH] Fix more concurrency warnings --- Nuke.xcodeproj/project.pbxproj | 2 +- Sources/Nuke/Caching/Cache.swift | 6 ++-- Sources/Nuke/ImageResponse.swift | 1 + Sources/Nuke/Internal/Graphics.swift | 2 +- Sources/Nuke/Internal/Log.swift | 4 ++- Sources/Nuke/Loading/DataLoader.swift | 2 +- .../ImagePipeline+Configuration.swift | 7 ++++- .../Nuke/Processing/ImageDecompression.swift | 6 ++-- .../NukeExtensions/ImageViewExtensions.swift | 6 ++-- Tests/MockImageProcessor.swift | 6 ++-- .../ImageViewExtensionsTests.swift | 20 +++++++++++-- .../ImageViewIntegrationTests.swift | 8 ++++-- .../ImageViewLoadingOptionsTests.swift | 21 ++++++++++++-- .../NukeExtensionsTestsHelpers.swift | 3 ++ Tests/NukeTests/ImageCacheTests.swift | 28 +++++++++++++++++++ .../ImagePipelineAsyncAwaitTests.swift | 9 +++--- Tests/XCTestCaseExtensions.swift | 12 ++++---- 17 files changed, 110 insertions(+), 33 deletions(-) diff --git a/Nuke.xcodeproj/project.pbxproj b/Nuke.xcodeproj/project.pbxproj index a5bf8a918..ce8243059 100644 --- a/Nuke.xcodeproj/project.pbxproj +++ b/Nuke.xcodeproj/project.pbxproj @@ -980,8 +980,8 @@ 0C0FD5D31CA47FE1002A78FB /* ImagePipeline.swift */, 0CF1754B22913F9800A8946E /* ImagePipeline+Configuration.swift */, 0C53C8B0263C968200E62D03 /* ImagePipeline+Delegate.swift */, - 0CBA07852852DA8B00CE29F4 /* ImagePipeline+Error.swift */, 0C78A2A6263F4E680051E0FF /* ImagePipeline+Cache.swift */, + 0CBA07852852DA8B00CE29F4 /* ImagePipeline+Error.swift */, ); path = Pipeline; sourceTree = ""; diff --git a/Sources/Nuke/Caching/Cache.swift b/Sources/Nuke/Caching/Cache.swift index b35a18994..f38f9e86e 100644 --- a/Sources/Nuke/Caching/Cache.swift +++ b/Sources/Nuke/Caching/Cache.swift @@ -56,7 +56,9 @@ final class Cache: @unchecked Sendable { self.memoryPressure.resume() #if os(iOS) || os(tvOS) || os(visionOS) - registerForEnterBackground() + Task { + await registerForEnterBackground() + } #endif } @@ -68,7 +70,7 @@ final class Cache: @unchecked Sendable { } #if os(iOS) || os(tvOS) || os(visionOS) - private func registerForEnterBackground() { + @MainActor private func registerForEnterBackground() { notificationObserver = NotificationCenter.default.addObserver(forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: nil) { [weak self] _ in self?.clearCacheOnEnterBackground() } diff --git a/Sources/Nuke/ImageResponse.swift b/Sources/Nuke/ImageResponse.swift index b595bc280..0999a8b68 100644 --- a/Sources/Nuke/ImageResponse.swift +++ b/Sources/Nuke/ImageResponse.swift @@ -11,6 +11,7 @@ import UIKit #if canImport(AppKit) import AppKit #endif + /// An image response that contains a fetched image and some metadata. public struct ImageResponse: @unchecked Sendable { /// An image container with an image and associated metadata. diff --git a/Sources/Nuke/Internal/Graphics.swift b/Sources/Nuke/Internal/Graphics.swift index 7909062b6..2aa95fd26 100644 --- a/Sources/Nuke/Internal/Graphics.swift +++ b/Sources/Nuke/Internal/Graphics.swift @@ -317,7 +317,7 @@ extension CGSize { enum Screen { #if os(iOS) || os(tvOS) /// Returns the current screen scale. - static let scale: CGFloat = UIScreen.main.scale + static let scale: CGFloat = UITraitCollection.current.displayScale #elseif os(watchOS) /// Returns the current screen scale. static let scale: CGFloat = WKInterfaceDevice.current().screenScale diff --git a/Sources/Nuke/Internal/Log.swift b/Sources/Nuke/Internal/Log.swift index 475b9a63b..cea2f8503 100644 --- a/Sources/Nuke/Internal/Log.swift +++ b/Sources/Nuke/Internal/Log.swift @@ -8,6 +8,7 @@ import os func signpost(_ object: AnyObject, _ name: StaticString, _ type: OSSignpostType, _ message: @autoclosure () -> String) { guard ImagePipeline.Configuration.isSignpostLoggingEnabled else { return } + let log = log.value let signpostId = OSSignpostID(log: log, object: object) os_signpost(type, log: log, name: name, signpostID: signpostId, "%{public}s", message()) } @@ -15,6 +16,7 @@ func signpost(_ object: AnyObject, _ name: StaticString, _ type: OSSignpostType, func signpost(_ name: StaticString, _ work: () throws -> T) rethrows -> T { guard ImagePipeline.Configuration.isSignpostLoggingEnabled else { return try work() } + let log = log.value let signpostId = OSSignpostID(log: log) os_signpost(.begin, log: log, name: name, signpostID: signpostId) let result = try work() @@ -22,7 +24,7 @@ func signpost(_ name: StaticString, _ work: () throws -> T) rethrows -> T { return result } -private let log = OSLog(subsystem: "com.github.kean.Nuke.ImagePipeline", category: "Image Loading") +private let log = Atomic(value: OSLog(subsystem: "com.github.kean.Nuke.ImagePipeline", category: "Image Loading")) private let byteFormatter = ByteCountFormatter() diff --git a/Sources/Nuke/Loading/DataLoader.swift b/Sources/Nuke/Loading/DataLoader.swift index 1af9c0c00..e2359ebe9 100644 --- a/Sources/Nuke/Loading/DataLoader.swift +++ b/Sources/Nuke/Loading/DataLoader.swift @@ -218,7 +218,7 @@ private final class _DataLoader: NSObject, URLSessionDataDelegate { // MARK: Internal - private final class _Handler { + private final class _Handler: @unchecked Sendable { let didReceiveData: (Data, URLResponse) -> Void let completion: (Error?) -> Void diff --git a/Sources/Nuke/Pipeline/ImagePipeline+Configuration.swift b/Sources/Nuke/Pipeline/ImagePipeline+Configuration.swift index 64e27b912..d5d0e599e 100644 --- a/Sources/Nuke/Pipeline/ImagePipeline+Configuration.swift +++ b/Sources/Nuke/Pipeline/ImagePipeline+Configuration.swift @@ -129,7 +129,12 @@ extension ImagePipeline { /// metrics in `os_signpost` Instrument. For more information see /// https://developer.apple.com/documentation/os/logging and /// https://developer.apple.com/videos/play/wwdc2018/405/. - public static var isSignpostLoggingEnabled = false + public static var isSignpostLoggingEnabled: Bool { + get { _isSignpostLoggingEnabled.value } + set { _isSignpostLoggingEnabled.value = newValue } + } + + private static let _isSignpostLoggingEnabled = Atomic(value: false) private var isCustomImageCacheProvided = false diff --git a/Sources/Nuke/Processing/ImageDecompression.swift b/Sources/Nuke/Processing/ImageDecompression.swift index 1e7e2e9fd..7a9d747f4 100644 --- a/Sources/Nuke/Processing/ImageDecompression.swift +++ b/Sources/Nuke/Processing/ImageDecompression.swift @@ -24,13 +24,13 @@ enum ImageDecompression { // MARK: Managing Decompression State - static var isDecompressionNeededAK: UInt8 = 0 + static let isDecompressionNeededAK = malloc(1)! static func setDecompressionNeeded(_ isDecompressionNeeded: Bool, for image: PlatformImage) { - objc_setAssociatedObject(image, &isDecompressionNeededAK, isDecompressionNeeded, .OBJC_ASSOCIATION_RETAIN) + objc_setAssociatedObject(image, isDecompressionNeededAK, isDecompressionNeeded, .OBJC_ASSOCIATION_RETAIN) } static func isDecompressionNeeded(for image: PlatformImage) -> Bool? { - objc_getAssociatedObject(image, &isDecompressionNeededAK) as? Bool + objc_getAssociatedObject(image, isDecompressionNeededAK) as? Bool } } diff --git a/Sources/NukeExtensions/ImageViewExtensions.swift b/Sources/NukeExtensions/ImageViewExtensions.swift index 8e2fa2d99..805b6787e 100644 --- a/Sources/NukeExtensions/ImageViewExtensions.swift +++ b/Sources/NukeExtensions/ImageViewExtensions.swift @@ -216,15 +216,15 @@ private final class ImageViewController { // MARK: - Associating Controller - static var controllerAK: UInt8 = 0 + static let controllerAK = malloc(1)! // Lazily create a controller for a given view and associate it with a view. static func controller(for view: ImageDisplayingView) -> ImageViewController { - if let controller = objc_getAssociatedObject(view, &ImageViewController.controllerAK) as? ImageViewController { + if let controller = objc_getAssociatedObject(view, controllerAK) as? ImageViewController { return controller } let controller = ImageViewController(view: view) - objc_setAssociatedObject(view, &ImageViewController.controllerAK, controller, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + objc_setAssociatedObject(view, controllerAK, controller, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) return controller } diff --git a/Tests/MockImageProcessor.swift b/Tests/MockImageProcessor.swift index 0f119c364..092d32e02 100644 --- a/Tests/MockImageProcessor.swift +++ b/Tests/MockImageProcessor.swift @@ -8,16 +8,16 @@ import Nuke extension PlatformImage { var nk_test_processorIDs: [String] { get { - return (objc_getAssociatedObject(self, &AssociatedKeys.ProcessorIDs) as? [String]) ?? [String]() + return (objc_getAssociatedObject(self, AssociatedKeys.processorId) as? [String]) ?? [String]() } set { - objc_setAssociatedObject(self, &AssociatedKeys.ProcessorIDs, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + objc_setAssociatedObject(self, AssociatedKeys.processorId, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } } } private enum AssociatedKeys { - static var ProcessorIDs: UInt8 = 0 + static let processorId = malloc(1)! } // MARK: - MockImageProcessor diff --git a/Tests/NukeExtensionsTests/ImageViewExtensionsTests.swift b/Tests/NukeExtensionsTests/ImageViewExtensionsTests.swift index e34a36cb2..40b671de2 100644 --- a/Tests/NukeExtensionsTests/ImageViewExtensionsTests.swift +++ b/Tests/NukeExtensionsTests/ImageViewExtensionsTests.swift @@ -11,7 +11,6 @@ import TVUIKit #if os(iOS) || os(tvOS) || os(macOS) || os(visionOS) -@MainActor class ImageViewExtensionsTests: XCTestCase { var imageView: _ImageView! var observer: ImagePipelineObserver! @@ -44,7 +43,8 @@ class ImageViewExtensionsTests: XCTestCase { } // MARK: - Loading - + + @MainActor func testImageLoaded() { // When requesting an image with request expectToLoadImage(with: Test.request, into: imageView) @@ -55,6 +55,7 @@ class ImageViewExtensionsTests: XCTestCase { } #if os(tvOS) + @MainActor func testImageLoadedToTVPosterView() { // Use local instance for this tvOS specific test for simplicity let posterView = TVPosterView() @@ -68,6 +69,7 @@ class ImageViewExtensionsTests: XCTestCase { } #endif + @MainActor func testImageLoadedWithURL() { // When requesting an image with URL let expectation = self.expectation(description: "Image loaded") @@ -80,6 +82,7 @@ class ImageViewExtensionsTests: XCTestCase { XCTAssertNotNil(imageView.image) } + @MainActor func testLoadImageWithNilRequest() { // WHEN imageView.image = Test.image @@ -96,6 +99,7 @@ class ImageViewExtensionsTests: XCTestCase { XCTAssertNil(imageView.image) } + @MainActor func testLoadImageWithNilRequestAndPlaceholder() { // GIVEN let failureImage = Test.image @@ -111,6 +115,7 @@ class ImageViewExtensionsTests: XCTestCase { // MARK: - Managing Tasks + @MainActor func testTaskReturned() { // When requesting an image let task = NukeExtensions.loadImage(with: Test.request, into: imageView) @@ -122,6 +127,7 @@ class ImageViewExtensionsTests: XCTestCase { XCTAssertEqual(task?.request.urlRequest, Test.request.urlRequest) } + @MainActor func testTaskIsNilWhenImageInMemoryCache() { // When the requested image is stored in memory cache let request = Test.request @@ -136,6 +142,7 @@ class ImageViewExtensionsTests: XCTestCase { // MARK: - Prepare For Reuse + @MainActor func testViewPreparedForReuse() { // Given an image view displaying an image imageView.image = Test.image @@ -147,6 +154,7 @@ class ImageViewExtensionsTests: XCTestCase { XCTAssertNil(imageView.image) } + @MainActor func testViewPreparedForReuseDisabled() { // Given an image view displaying an image let image = Test.image @@ -163,6 +171,7 @@ class ImageViewExtensionsTests: XCTestCase { // MARK: - Memory Cache + @MainActor func testMemoryCacheUsed() { // Given the requested image stored in memory cache let image = Test.image @@ -175,6 +184,7 @@ class ImageViewExtensionsTests: XCTestCase { XCTAssertEqual(imageView.image, image) } + @MainActor func testMemoryCacheDisabled() { // Given the requested image stored in memory cache imageCache[Test.request] = Test.container @@ -190,6 +200,7 @@ class ImageViewExtensionsTests: XCTestCase { // MARK: - Completion and Progress Closures + @MainActor func testCompletionCalled() { var didCallCompletion = false let expectation = self.expectation(description: "Image loaded") @@ -210,6 +221,7 @@ class ImageViewExtensionsTests: XCTestCase { wait() } + @MainActor func testCompletionCalledImageFromCache() { // GIVEN the requested image stored in memory cache imageCache[Test.request] = Test.container @@ -228,6 +240,7 @@ class ImageViewExtensionsTests: XCTestCase { XCTAssertTrue(didCallCompletion) } + @MainActor func testProgressHandlerCalled() { // GIVEN dataLoader.results[Test.url] = .success( @@ -252,6 +265,7 @@ class ImageViewExtensionsTests: XCTestCase { // MARK: - Cancellation + @MainActor func testRequestCancelled() { dataLoader.isSuspended = true @@ -268,6 +282,7 @@ class ImageViewExtensionsTests: XCTestCase { wait() } + @MainActor func testRequestCancelledWhenNewRequestStarted() { dataLoader.isSuspended = true @@ -283,6 +298,7 @@ class ImageViewExtensionsTests: XCTestCase { wait() } + @MainActor func testRequestCancelledWhenTargetGetsDeallocated() { dataLoader.isSuspended = true diff --git a/Tests/NukeExtensionsTests/ImageViewIntegrationTests.swift b/Tests/NukeExtensionsTests/ImageViewIntegrationTests.swift index 72d50b287..38b64892d 100644 --- a/Tests/NukeExtensionsTests/ImageViewIntegrationTests.swift +++ b/Tests/NukeExtensionsTests/ImageViewIntegrationTests.swift @@ -8,7 +8,6 @@ import XCTest #if os(iOS) || os(tvOS) || os(macOS) || os(visionOS) -@MainActor class ImageViewIntegrationTests: XCTestCase { var imageView: _ImageView! var pipeline: ImagePipeline! @@ -44,6 +43,7 @@ class ImageViewIntegrationTests: XCTestCase { // MARK: - Loading + @MainActor func testImageLoaded() { // When expectToLoadImage(with: request, into: imageView) @@ -52,7 +52,8 @@ class ImageViewIntegrationTests: XCTestCase { // Then XCTAssertNotNil(imageView.image) } - + + @MainActor func testImageLoadedWithURL() { // When let expectation = self.expectation(description: "Image loaded") @@ -67,6 +68,7 @@ class ImageViewIntegrationTests: XCTestCase { // MARK: - Loading with Invalid URL + @MainActor func testLoadImageWithInvalidURLString() { // WHEN let expectation = self.expectation(description: "Image loaded") @@ -80,6 +82,7 @@ class ImageViewIntegrationTests: XCTestCase { XCTAssertNil(imageView.image) } + @MainActor func testLoadingWithNilURL() { // GIVEN var urlRequest = URLRequest(url: Test.url) @@ -123,6 +126,7 @@ class ImageViewIntegrationTests: XCTestCase { var recordedData = [Data?]() } + @MainActor func _testThatAttachedDataIsPassed() throws { // GIVEN pipeline = pipeline.reconfigured { diff --git a/Tests/NukeExtensionsTests/ImageViewLoadingOptionsTests.swift b/Tests/NukeExtensionsTests/ImageViewLoadingOptionsTests.swift index de66f9ceb..2cce4be80 100644 --- a/Tests/NukeExtensionsTests/ImageViewLoadingOptionsTests.swift +++ b/Tests/NukeExtensionsTests/ImageViewLoadingOptionsTests.swift @@ -8,7 +8,6 @@ import XCTest #if os(iOS) || os(tvOS) || os(macOS) || os(visionOS) -@MainActor class ImageViewLoadingOptionsTests: XCTestCase { var mockCache: MockImageCache! var dataLoader: MockDataLoader! @@ -38,6 +37,7 @@ class ImageViewLoadingOptionsTests: XCTestCase { // MARK: - Transition + @MainActor func testCustomTransitionPerformed() { // Given var options = ImageLoadingOptions() @@ -58,6 +58,7 @@ class ImageViewLoadingOptionsTests: XCTestCase { } // Tests https://github.com/kean/Nuke/issues/206 + @MainActor func testImageIsDisplayedFadeInTransition() { // Given options with .fadeIn transition let options = ImageLoadingOptions(transition: .fadeIn(duration: 10)) @@ -72,6 +73,7 @@ class ImageViewLoadingOptionsTests: XCTestCase { // MARK: - Placeholder + @MainActor func testPlaceholderDisplayed() { // Given var options = ImageLoadingOptions() @@ -87,6 +89,7 @@ class ImageViewLoadingOptionsTests: XCTestCase { // MARK: - Failure Image + @MainActor func testFailureImageDisplayed() { // Given dataLoader.results[Test.url] = .failure( @@ -104,7 +107,8 @@ class ImageViewLoadingOptionsTests: XCTestCase { // Then XCTAssertEqual(imageView.image, failureImage) } - + + @MainActor func testFailureImageTransitionRun() { // Given dataLoader.results[Test.url] = .failure( @@ -137,6 +141,7 @@ class ImageViewLoadingOptionsTests: XCTestCase { // MARK: - Content Modes + @MainActor func testPlaceholderAndSuccessContentModesApplied() { // Given var options = ImageLoadingOptions() @@ -156,6 +161,7 @@ class ImageViewLoadingOptionsTests: XCTestCase { XCTAssertEqual(imageView.contentMode, .scaleAspectFill) } + @MainActor func testSuccessContentModeAppliedWhenFromMemoryCache() { // Given var options = ImageLoadingOptions() @@ -174,6 +180,7 @@ class ImageViewLoadingOptionsTests: XCTestCase { XCTAssertEqual(imageView.contentMode, .scaleAspectFill) } + @MainActor func testFailureContentModeApplied() { // Given var options = ImageLoadingOptions() @@ -202,6 +209,7 @@ class ImageViewLoadingOptionsTests: XCTestCase { // MARK: - Tint Colors + @MainActor func testPlaceholderAndSuccessTintColorApplied() { // Given var options = ImageLoadingOptions() @@ -221,7 +229,8 @@ class ImageViewLoadingOptionsTests: XCTestCase { XCTAssertEqual(imageView.tintColor, .blue) XCTAssertEqual(imageView.image?.renderingMode, .alwaysTemplate) } - + + @MainActor func testSuccessTintColorAppliedWhenFromMemoryCache() { // Given var options = ImageLoadingOptions() @@ -241,6 +250,7 @@ class ImageViewLoadingOptionsTests: XCTestCase { XCTAssertEqual(imageView.image?.renderingMode, .alwaysTemplate) } + @MainActor func testFailureTintColorApplied() { // Given var options = ImageLoadingOptions() @@ -268,6 +278,7 @@ class ImageViewLoadingOptionsTests: XCTestCase { // MARK: - Pipeline + @MainActor func testCustomPipelineUsed() { // Given let dataLoader = MockDataLoader() @@ -292,6 +303,7 @@ class ImageViewLoadingOptionsTests: XCTestCase { // MARK: - Shared Options + @MainActor func testSharedOptionsUsed() { // Given var options = ImageLoadingOptions.shared @@ -311,6 +323,7 @@ class ImageViewLoadingOptionsTests: XCTestCase { // MARK: - Cache Policy + @MainActor func testReloadIgnoringCachedData() { // When the requested image is stored in memory cache var request = Test.request @@ -329,6 +342,7 @@ class ImageViewLoadingOptionsTests: XCTestCase { // MARK: - Misc #if os(iOS) || os(tvOS) || os(visionOS) + @MainActor func testTransitionCrossDissolve() { // GIVEN var options = ImageLoadingOptions() @@ -352,6 +366,7 @@ class ImageViewLoadingOptionsTests: XCTestCase { } #endif + @MainActor func testSettingDefaultProcessor() { // GIVEN var options = ImageLoadingOptions() diff --git a/Tests/NukeExtensionsTests/NukeExtensionsTestsHelpers.swift b/Tests/NukeExtensionsTests/NukeExtensionsTestsHelpers.swift index 04ab77b56..55d7ead44 100644 --- a/Tests/NukeExtensionsTests/NukeExtensionsTestsHelpers.swift +++ b/Tests/NukeExtensionsTests/NukeExtensionsTestsHelpers.swift @@ -34,13 +34,16 @@ extension XCTestCase { } extension ImageLoadingOptions { + @MainActor private static var stack = [ImageLoadingOptions]() + @MainActor static func pushShared(_ shared: ImageLoadingOptions) { stack.append(ImageLoadingOptions.shared) ImageLoadingOptions.shared = shared } + @MainActor static func popShared() { ImageLoadingOptions.shared = stack.removeLast() } diff --git a/Tests/NukeTests/ImageCacheTests.swift b/Tests/NukeTests/ImageCacheTests.swift index ce90cd839..f856e077f 100644 --- a/Tests/NukeTests/ImageCacheTests.swift +++ b/Tests/NukeTests/ImageCacheTests.swift @@ -24,11 +24,13 @@ class ImageCacheTests: XCTestCase, @unchecked Sendable { // MARK: - Basics + @MainActor func testCacheCreation() { XCTAssertEqual(cache.totalCount, 0) XCTAssertNil(cache[Test.request]) } + @MainActor func testThatImageIsStored() { // When cache[Test.request] = Test.container @@ -40,6 +42,7 @@ class ImageCacheTests: XCTestCase, @unchecked Sendable { // MARK: - Subscript + @MainActor func testThatImageIsStoredUsingSubscript() { // When cache[Test.request] = Test.container @@ -50,6 +53,7 @@ class ImageCacheTests: XCTestCase, @unchecked Sendable { // MARK: - Count + @MainActor func testThatTotalCountChanges() { XCTAssertEqual(cache.totalCount, 0) @@ -66,6 +70,7 @@ class ImageCacheTests: XCTestCase, @unchecked Sendable { XCTAssertEqual(cache.totalCount, 0) } + @MainActor func testThatCountLimitChanges() { // When cache.countLimit = 1 @@ -74,6 +79,7 @@ class ImageCacheTests: XCTestCase, @unchecked Sendable { XCTAssertEqual(cache.countLimit, 1) } + @MainActor func testThatTTLChanges() { //when cache.ttl = 1 @@ -82,6 +88,7 @@ class ImageCacheTests: XCTestCase, @unchecked Sendable { XCTAssertEqual(cache.ttl, 1) } + @MainActor func testThatItemsAreRemoveImmediatelyWhenCountLimitIsReached() { // Given cache.countLimit = 1 @@ -95,6 +102,7 @@ class ImageCacheTests: XCTestCase, @unchecked Sendable { XCTAssertNotNil(cache[request2]) } + @MainActor func testTrimToCount() { // Given cache[request1] = Test.container @@ -108,6 +116,7 @@ class ImageCacheTests: XCTestCase, @unchecked Sendable { XCTAssertNotNil(cache[request2]) } + @MainActor func testThatImagesAreRemovedOnCountLimitChange() { // Given cache.countLimit = 2 @@ -127,10 +136,12 @@ class ImageCacheTests: XCTestCase, @unchecked Sendable { #if !os(macOS) + @MainActor func testDefaultImageCost() { XCTAssertEqual(cache.cost(for: ImageContainer(image: Test.image)), 1228800) } + @MainActor func testThatTotalCostChanges() { let imageCost = cache.cost(for: ImageContainer(image: Test.image)) XCTAssertEqual(cache.totalCost, 0) @@ -148,6 +159,7 @@ class ImageCacheTests: XCTestCase, @unchecked Sendable { XCTAssertEqual(cache.totalCost, 0) } + @MainActor func testThatCostLimitChanged() { // Given let cost = cache.cost(for: ImageContainer(image: Test.image)) @@ -159,6 +171,7 @@ class ImageCacheTests: XCTestCase, @unchecked Sendable { XCTAssertEqual(cache.costLimit, Int(Double(cost) * 1.5)) } + @MainActor func testThatItemsAreRemoveImmediatelyWhenCostLimitIsReached() { // Given let cost = cache.cost(for: ImageContainer(image: Test.image)) @@ -173,6 +186,7 @@ class ImageCacheTests: XCTestCase, @unchecked Sendable { XCTAssertNotNil(cache[request2]) } + @MainActor func testEntryCostLimitEntryStored() { // Given let container = ImageContainer(image: Test.image) @@ -188,6 +202,7 @@ class ImageCacheTests: XCTestCase, @unchecked Sendable { XCTAssertEqual(cache.totalCount, 1) } + @MainActor func testEntryCostLimitEntryNotStored() { // Given let container = ImageContainer(image: Test.image) @@ -203,6 +218,7 @@ class ImageCacheTests: XCTestCase, @unchecked Sendable { XCTAssertEqual(cache.totalCount, 0) } + @MainActor func testTrimToCost() { // Given cache.costLimit = Int.max @@ -219,6 +235,7 @@ class ImageCacheTests: XCTestCase, @unchecked Sendable { XCTAssertNotNil(cache[request2]) } + @MainActor func testThatImagesAreRemovedOnCostLimitChange() { // Given let cost = cache.cost(for: ImageContainer(image: Test.image)) @@ -235,6 +252,7 @@ class ImageCacheTests: XCTestCase, @unchecked Sendable { XCTAssertNotNil(cache[request2]) } + @MainActor func testImageContainerWithoutAssociatedDataCost() { // Given let data = Test.data(name: "cat", extension: "gif") @@ -245,6 +263,7 @@ class ImageCacheTests: XCTestCase, @unchecked Sendable { XCTAssertEqual(cache.cost(for: container), 558000) } + @MainActor func testImageContainerWithAssociatedDataCost() { // Given let data = Test.data(name: "cat", extension: "gif") @@ -259,6 +278,7 @@ class ImageCacheTests: XCTestCase, @unchecked Sendable { // MARK: LRU + @MainActor func testThatLeastRecentItemsAreRemoved() { // Given let cost = cache.cost(for: ImageContainer(image: Test.image)) @@ -274,6 +294,7 @@ class ImageCacheTests: XCTestCase, @unchecked Sendable { XCTAssertNotNil(cache[request3]) } + @MainActor func testThatItemsAreTouched() { // Given let cost = cache.cost(for: ImageContainer(image: Test.image)) @@ -294,6 +315,7 @@ class ImageCacheTests: XCTestCase, @unchecked Sendable { // MARK: Misc + @MainActor func testRemoveAll() { // GIVEN cache[request1] = Test.container @@ -308,6 +330,8 @@ class ImageCacheTests: XCTestCase, @unchecked Sendable { } #if os(iOS) || os(tvOS) || os(visionOS) + @MainActor + @MainActor func testThatSomeImagesAreRemovedOnDidEnterBackground() async { // GIVEN cache.costLimit = Int.max @@ -328,6 +352,7 @@ class ImageCacheTests: XCTestCase, @unchecked Sendable { await task.value } + @MainActor func testThatSomeImagesAreRemovedBasedOnCostOnDidEnterBackground() async { // GIVEN let cost = cache.cost(for: ImageContainer(image: Test.image)) @@ -357,6 +382,7 @@ class InternalCacheTTLTests: XCTestCase { // MARK: TTL + @MainActor func testTTL() { // Given cache.set(1, forKey: 1, cost: 1, ttl: 0.05) // 50 ms @@ -369,6 +395,7 @@ class InternalCacheTTLTests: XCTestCase { XCTAssertNil(cache.value(forKey: 1)) } + @MainActor func testDefaultTTLIsUsed() { // Given cache.conf.ttl = 0.05// 50 ms @@ -382,6 +409,7 @@ class InternalCacheTTLTests: XCTestCase { XCTAssertNil(cache.value(forKey: 1)) } + @MainActor func testDefaultToNonExpiringEntries() { // Given cache.set(1, forKey: 1, cost: 1) diff --git a/Tests/NukeTests/ImagePipelineTests/ImagePipelineAsyncAwaitTests.swift b/Tests/NukeTests/ImagePipelineTests/ImagePipelineAsyncAwaitTests.swift index 73be09b20..03d150c43 100644 --- a/Tests/NukeTests/ImagePipelineTests/ImagePipelineAsyncAwaitTests.swift +++ b/Tests/NukeTests/ImagePipelineTests/ImagePipelineAsyncAwaitTests.swift @@ -65,6 +65,7 @@ class ImagePipelineAsyncAwaitTests: XCTestCase, @unchecked Sendable { XCTAssertEqual(image.sizeInPixels, CGSize(width: 640, height: 480)) } + @MainActor @available(macOS 12, iOS 15, tvOS 15, watchOS 9, *) func testAsyncImageTaskEvents() async throws { // GIVEN @@ -73,6 +74,10 @@ class ImagePipelineAsyncAwaitTests: XCTestCase, @unchecked Sendable { $0.dataLoader = dataLoader $0.isProgressiveDecodingEnabled = true } + pipeline.queue.suspend() + DispatchQueue.main.async { + self.pipeline.queue.resume() // Make sure we subscribe after a delay + } // WHEN let task = pipeline.imageTask(with: Test.url) @@ -90,10 +95,6 @@ class ImagePipelineAsyncAwaitTests: XCTestCase, @unchecked Sendable { } // THEN - guard recordedPreviews.count == 2 else { - return XCTFail("Unexpected number of previews") - } - XCTAssertEqual(recordedEvents, [ .progress(.init(completed: 13152, total: 39456)), .preview(recordedPreviews[0]), diff --git a/Tests/XCTestCaseExtensions.swift b/Tests/XCTestCaseExtensions.swift index f710fcaf0..5f1debab1 100644 --- a/Tests/XCTestCaseExtensions.swift +++ b/Tests/XCTestCaseExtensions.swift @@ -34,11 +34,11 @@ extension XCTestCase { return record } - private static var cancellablesAK: UInt8 = 0 + private static let cancellablesAK = malloc(1)! fileprivate var cancellables: [AnyCancellable] { - get { (objc_getAssociatedObject(self, &XCTestCase.cancellablesAK) as? [AnyCancellable]) ?? [] } - set { objc_setAssociatedObject(self, &XCTestCase.cancellablesAK, newValue, .OBJC_ASSOCIATION_RETAIN) } + get { (objc_getAssociatedObject(self, XCTestCase.cancellablesAK) as? [AnyCancellable]) ?? [] } + set { objc_setAssociatedObject(self, XCTestCase.cancellablesAK, newValue, .OBJC_ASSOCIATION_RETAIN) } } } @@ -98,14 +98,14 @@ extension XCTestCase { observations.append(observation) } - private static var observationsAK: UInt8 = 0 + private static let observationsAK = malloc(1)! private var observations: [NSKeyValueObservation] { get { - return (objc_getAssociatedObject(self, &XCTestCase.observationsAK) as? [NSKeyValueObservation]) ?? [] + return (objc_getAssociatedObject(self, XCTestCase.observationsAK) as? [NSKeyValueObservation]) ?? [] } set { - objc_setAssociatedObject(self, &XCTestCase.observationsAK, newValue, .OBJC_ASSOCIATION_RETAIN) + objc_setAssociatedObject(self, XCTestCase.observationsAK, newValue, .OBJC_ASSOCIATION_RETAIN) } } }