Selaa lähdekoodia

Fix retryContext and its tests for concurrency

onevcat 1 vuosi sitten
vanhempi
commit
1605d11c9b

+ 1 - 1
Sources/General/KingfisherManager.swift

@@ -356,7 +356,7 @@ public class KingfisherManager: @unchecked Sendable {
                     retryStrategy.retry(context: context) { decision in
                     retryStrategy.retry(context: context) { decision in
                         switch decision {
                         switch decision {
                         case .retry(let userInfo):
                         case .retry(let userInfo):
-                            retryContext?.userInfo = userInfo
+                            context.userInfo = userInfo
                             startNewRetrieveTask(with: source, downloadTaskUpdated: downloadTaskUpdated)
                             startNewRetrieveTask(with: source, downloadTaskUpdated: downloadTaskUpdated)
                         case .stop:
                         case .stop:
                             failCurrentSource(currentSource, with: error)
                             failCurrentSource(currentSource, with: error)

+ 2 - 2
Sources/Networking/ImageDownloader.swift

@@ -522,7 +522,7 @@ open class ImageDownloader: @unchecked Sendable {
         with url: URL,
         with url: URL,
         options: KingfisherOptionsInfo? = nil,
         options: KingfisherOptionsInfo? = nil,
         progressBlock: DownloadProgressBlock? = nil,
         progressBlock: DownloadProgressBlock? = nil,
-        completionHandler: ((Result<ImageLoadingResult, KingfisherError>) -> Void)? = nil) -> DownloadTask?
+        completionHandler: (@Sendable (Result<ImageLoadingResult, KingfisherError>) -> Void)? = nil) -> DownloadTask?
     {
     {
         var info = KingfisherParsedOptionsInfo(options)
         var info = KingfisherParsedOptionsInfo(options)
         if let block = progressBlock {
         if let block = progressBlock {
@@ -547,7 +547,7 @@ open class ImageDownloader: @unchecked Sendable {
     open func downloadImage(
     open func downloadImage(
         with url: URL,
         with url: URL,
         options: KingfisherOptionsInfo? = nil,
         options: KingfisherOptionsInfo? = nil,
-        completionHandler: ((Result<ImageLoadingResult, KingfisherError>) -> Void)? = nil) -> DownloadTask
+        completionHandler: (@Sendable (Result<ImageLoadingResult, KingfisherError>) -> Void)? = nil) -> DownloadTask
     {
     {
         downloadImage(
         downloadImage(
             with: url,
             with: url,

+ 2 - 2
Sources/Networking/RetryStrategy.swift

@@ -82,7 +82,7 @@ public protocol RetryStrategy: Sendable {
     /// - Parameters:
     /// - Parameters:
     ///   - context: The retry context containing information of the current retry attempt.
     ///   - context: The retry context containing information of the current retry attempt.
     ///   - retryHandler: A block you need to call with a decision on whether the retry should happen or not.
     ///   - retryHandler: A block you need to call with a decision on whether the retry should happen or not.
-    func retry(context: RetryContext, retryHandler: @escaping (RetryDecision) -> Void)
+    func retry(context: RetryContext, retryHandler: @escaping @Sendable (RetryDecision) -> Void)
 }
 }
 
 
 /// A retry strategy that guides Kingfisher to perform retry operation with some delay.
 /// A retry strategy that guides Kingfisher to perform retry operation with some delay.
@@ -143,7 +143,7 @@ public struct DelayRetryStrategy: RetryStrategy {
         self.retryInterval = retryInterval
         self.retryInterval = retryInterval
     }
     }
 
 
-    public func retry(context: RetryContext, retryHandler: @escaping (RetryDecision) -> Void) {
+    public func retry(context: RetryContext, retryHandler: @escaping @Sendable (RetryDecision) -> Void) {
         // Retry count exceeded.
         // Retry count exceeded.
         guard context.retriedCount < maxRetryCount else {
         guard context.retriedCount < maxRetryCount else {
             retryHandler(.stop)
             retryHandler(.stop)

+ 11 - 0
Tests/KingfisherTests/KingfisherManagerTests.swift

@@ -1367,3 +1367,14 @@ actor ActorBox<T> {
         self.value = value
         self.value = value
     }
     }
 }
 }
+
+actor ActorArray<Element> {
+    var value: [Element]
+    init(_ value: [Element]) {
+        self.value = value
+    }
+    
+    func append(_ newElement: Element) {
+        value.append(newElement)
+    }
+}

+ 56 - 16
Tests/KingfisherTests/RetryStrategyTests.swift

@@ -103,12 +103,15 @@ class RetryStrategyTests: XCTestCase {
     }
     }
 
 
     func testDelayRetryStrategyExceededCount() {
     func testDelayRetryStrategyExceededCount() {
-
-        var blockCalled: [Bool] = []
+        let exp = expectation(description: #function)
+        let blockCalled: ActorArray<Bool> = ActorArray([])
 
 
         let source = Source.network(URL(string: "url")!)
         let source = Source.network(URL(string: "url")!)
         let retry = DelayRetryStrategy(maxRetryCount: 3, retryInterval: .seconds(0))
         let retry = DelayRetryStrategy(maxRetryCount: 3, retryInterval: .seconds(0))
 
 
+        let group = DispatchGroup()
+        
+        group.enter()
         let context1 = RetryContext(
         let context1 = RetryContext(
             source: source,
             source: source,
             error: .responseError(reason: .URLSessionError(error: E()))
             error: .responseError(reason: .URLSessionError(error: E()))
@@ -119,9 +122,13 @@ class RetryStrategyTests: XCTestCase {
                 return
                 return
             }
             }
             XCTAssertNil(userInfo)
             XCTAssertNil(userInfo)
-            blockCalled.append(true)
+            Task {
+                await blockCalled.append(true)
+                group.leave()
+            }
         }
         }
 
 
+        group.enter()
         let context2 = RetryContext(
         let context2 = RetryContext(
             source: source,
             source: source,
             error: .responseError(reason: .URLSessionError(error: E()))
             error: .responseError(reason: .URLSessionError(error: E()))
@@ -134,21 +141,34 @@ class RetryStrategyTests: XCTestCase {
                 XCTFail("The decision should be `stop`")
                 XCTFail("The decision should be `stop`")
                 return
                 return
             }
             }
-            blockCalled.append(true)
+            Task {
+                await blockCalled.append(true)
+                group.leave()
+            }
         }
         }
-
-        XCTAssertEqual(blockCalled.count, 2)
-        XCTAssertTrue(blockCalled.allSatisfy { $0 })
+        
+        group.notify(queue: .main) {
+            Task {
+                let result = await blockCalled.value
+                XCTAssertEqual(result.count, 2)
+                XCTAssertTrue(result.allSatisfy { $0 })
+                exp.fulfill()
+            }
+        }
+        waitForExpectations(timeout: 1, handler: nil)
     }
     }
 
 
     func testDelayRetryStrategyNotRetryForErrorReason() {
     func testDelayRetryStrategyNotRetryForErrorReason() {
+        let exp = expectation(description: #function)
         // Only non-user cancel error && response error should be retied.
         // Only non-user cancel error && response error should be retied.
-        var blockCalled: [Bool] = []
+        let blockCalled: ActorArray<Bool> = ActorArray([])
         let source = Source.network(URL(string: "url")!)
         let source = Source.network(URL(string: "url")!)
         let retry = DelayRetryStrategy(maxRetryCount: 3, retryInterval: .seconds(0))
         let retry = DelayRetryStrategy(maxRetryCount: 3, retryInterval: .seconds(0))
 
 
         let task = URLSession.shared.dataTask(with: URL(string: "url")!)
         let task = URLSession.shared.dataTask(with: URL(string: "url")!)
 
 
+        let group = DispatchGroup()
+        group.enter()
         let context1 = RetryContext(
         let context1 = RetryContext(
             source: source,
             source: source,
             error: .requestError(reason: .taskCancelled(task: .init(task: task), token: .init()))
             error: .requestError(reason: .taskCancelled(task: .init(task: task), token: .init()))
@@ -158,9 +178,13 @@ class RetryStrategyTests: XCTestCase {
                 XCTFail("The decision should be `stop` if user cancelled the task.")
                 XCTFail("The decision should be `stop` if user cancelled the task.")
                 return
                 return
             }
             }
-            blockCalled.append(true)
+            Task {
+                await blockCalled.append(true)
+                group.leave()
+            }
         }
         }
 
 
+        group.enter()
         let context2 = RetryContext(
         let context2 = RetryContext(
             source: source,
             source: source,
             error: .cacheError(reason: .imageNotExisting(key: "any_key"))
             error: .cacheError(reason: .imageNotExisting(key: "any_key"))
@@ -170,15 +194,25 @@ class RetryStrategyTests: XCTestCase {
                 XCTFail("The decision should be `stop` if the error type is not response error.")
                 XCTFail("The decision should be `stop` if the error type is not response error.")
                 return
                 return
             }
             }
-            blockCalled.append(true)
+            Task {
+                await blockCalled.append(true)
+                group.leave()
+            }
         }
         }
-
-        XCTAssertEqual(blockCalled.count, 2)
-        XCTAssertTrue(blockCalled.allSatisfy { $0 })
+        
+        group.notify(queue: .main) {
+            Task {
+                let result = await blockCalled.value
+                XCTAssertEqual(result.count, 2)
+                XCTAssertTrue(result.allSatisfy { $0 })
+                exp.fulfill()
+            }
+        }
+        waitForExpectations(timeout: 1, handler: nil)
     }
     }
 
 
     func testDelayRetryStrategyDidRetried() {
     func testDelayRetryStrategyDidRetried() {
-        var called = false
+        let called = ActorBox(false)
         let source = Source.network(URL(string: "url")!)
         let source = Source.network(URL(string: "url")!)
         let retry = DelayRetryStrategy(maxRetryCount: 3, retryInterval: .seconds(0))
         let retry = DelayRetryStrategy(maxRetryCount: 3, retryInterval: .seconds(0))
         let context = RetryContext(
         let context = RetryContext(
@@ -190,10 +224,16 @@ class RetryStrategyTests: XCTestCase {
                 XCTFail("The decision should be `retry`.")
                 XCTFail("The decision should be `retry`.")
                 return
                 return
             }
             }
-            called = true
+            Task {
+                await called.setValue(true)
+            }
         }
         }
 
 
-        XCTAssertTrue(called)
+        Task {
+            let result = await called.value
+            XCTAssertTrue(result)
+        }
+        
     }
     }
 }
 }