ImageDownloaderTests.swift 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782
  1. //
  2. // ImageDownloaderTests.swift
  3. // Kingfisher
  4. //
  5. // Created by Wei Wang on 15/4/10.
  6. //
  7. // Copyright (c) 2019 Wei Wang <onevcat@gmail.com>
  8. //
  9. // Permission is hereby granted, free of charge, to any person obtaining a copy
  10. // of this software and associated documentation files (the "Software"), to deal
  11. // in the Software without restriction, including without limitation the rights
  12. // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  13. // copies of the Software, and to permit persons to whom the Software is
  14. // furnished to do so, subject to the following conditions:
  15. //
  16. // The above copyright notice and this permission notice shall be included in
  17. // all copies or substantial portions of the Software.
  18. //
  19. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  20. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  21. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  22. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  23. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  24. // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  25. // THE SOFTWARE.
  26. import XCTest
  27. @testable import Kingfisher
  28. class ImageDownloaderTests: XCTestCase {
  29. var downloader: ImageDownloader!
  30. override class func setUp() {
  31. super.setUp()
  32. LSNocilla.sharedInstance().start()
  33. }
  34. override class func tearDown() {
  35. LSNocilla.sharedInstance().stop()
  36. super.tearDown()
  37. }
  38. override func setUp() {
  39. super.setUp()
  40. downloader = ImageDownloader(name: "test")
  41. }
  42. override func tearDown() {
  43. LSNocilla.sharedInstance().clearStubs()
  44. downloader = nil
  45. super.tearDown()
  46. }
  47. func testDownloadAnImage() {
  48. let exp = expectation(description: #function)
  49. let url = testURLs[0]
  50. stub(url, data: testImageData)
  51. downloader.downloadImage(with: url) { result in
  52. XCTAssertNotNil(result.value)
  53. exp.fulfill()
  54. }
  55. waitForExpectations(timeout: 3, handler: nil)
  56. }
  57. func testDownloadAnImageAsync() async throws {
  58. let url = testURLs[0]
  59. stub(url, data: testImageData)
  60. let result = try await downloader.downloadImage(with: url, options: .empty)
  61. XCTAssertEqual(result.originalData, testImageData)
  62. }
  63. func testDownloadMultipleImages() {
  64. let exp = expectation(description: #function)
  65. let group = DispatchGroup()
  66. for url in testURLs {
  67. group.enter()
  68. stub(url, data: testImageData)
  69. downloader.downloadImage(with: url) { result in
  70. XCTAssertNotNil(result.value)
  71. group.leave()
  72. }
  73. }
  74. group.notify(queue: .main, execute: exp.fulfill)
  75. waitForExpectations(timeout: 3, handler: nil)
  76. }
  77. func testDownloadMultipleImagesAsync() async throws {
  78. try await withThrowingTaskGroup(of: ImageLoadingResult.self) { group in
  79. for url in testURLs {
  80. stub(url, data: testImageData)
  81. group.addTask {
  82. try await self.downloader.downloadImage(with: url)
  83. }
  84. }
  85. for try await result in group {
  86. XCTAssertEqual(result.originalData, testImageData)
  87. }
  88. }
  89. }
  90. func testDownloadAnImageWithMultipleCallback() {
  91. let exp = expectation(description: #function)
  92. let group = DispatchGroup()
  93. let url = testURLs[0]
  94. stub(url, data: testImageData)
  95. for _ in 0...5 {
  96. group.enter()
  97. downloader.downloadImage(with: url) { result in
  98. XCTAssertNotNil(result.value)
  99. group.leave()
  100. }
  101. }
  102. group.notify(queue: .main, execute: exp.fulfill)
  103. waitForExpectations(timeout: 5, handler: nil)
  104. }
  105. func testDownloadWithModifyingRequest() {
  106. let exp = expectation(description: #function)
  107. let url = testURLs[0]
  108. stub(url, data: testImageData)
  109. let modifier = URLModifier(url: url)
  110. let someURL = URL(string: "some_strange_url")!
  111. let task = downloader.downloadImage(with: someURL, options: [.requestModifier(modifier)]) { result in
  112. XCTAssertNotNil(result.value)
  113. XCTAssertEqual(result.value?.url, url)
  114. exp.fulfill()
  115. }
  116. XCTAssertTrue(task.isInitialized)
  117. waitForExpectations(timeout: 3, handler: nil)
  118. }
  119. func testDownloadWithAsyncModifyingRequest() {
  120. let exp = expectation(description: #function)
  121. let downloadTaskStarted = LockIsolated(false)
  122. let url = testURLs[0]
  123. stub(url, data: testImageData)
  124. let asyncModifier = AsyncURLModifier(url: url, onDownloadTaskStarted: { task in
  125. XCTAssertNotNil(task)
  126. downloadTaskStarted.setValue(true)
  127. })
  128. let someURL = URL(string: "some_strange_url")!
  129. let task = downloader.downloadImage(with: someURL, options: [.requestModifier(asyncModifier)]) { result in
  130. XCTAssertNotNil(result.value)
  131. XCTAssertEqual(result.value?.url, url)
  132. XCTAssertTrue(downloadTaskStarted.value)
  133. exp.fulfill()
  134. }
  135. // The returned task is nil since the download is not starting immediately.
  136. XCTAssertFalse(task.isInitialized)
  137. waitForExpectations(timeout: 3, handler: nil)
  138. }
  139. func testDownloadWithModifyingRequestToNil() {
  140. let nilModifier = AnyModifier { _ in
  141. return nil
  142. }
  143. let exp = expectation(description: #function)
  144. let someURL = URL(string: "some_strange_url")!
  145. downloader.downloadImage(with: someURL, options: [.requestModifier(nilModifier)]) { result in
  146. XCTAssertNotNil(result.error)
  147. guard case .requestError(reason: .emptyRequest) = result.error! else {
  148. XCTFail()
  149. fatalError()
  150. }
  151. exp.fulfill()
  152. }
  153. waitForExpectations(timeout: 3, handler: nil)
  154. }
  155. func testServerInvalidStatusCode() {
  156. let exp = expectation(description: #function)
  157. let url = testURLs[0]
  158. stub(url, data: testImageData, statusCode: 404)
  159. downloader.downloadImage(with: url) { result in
  160. XCTAssertNotNil(result.error)
  161. XCTAssertTrue(result.error!.isInvalidResponseStatusCode(404))
  162. exp.fulfill()
  163. }
  164. waitForExpectations(timeout: 3, handler: nil)
  165. }
  166. func testDownloadResultErrorAndRetry() {
  167. let exp = expectation(description: #function)
  168. let url = testURLs[0]
  169. stub(url, errorCode: -1)
  170. downloader.downloadImage(with: url) { result in
  171. XCTAssertNotNil(result.error)
  172. LSNocilla.sharedInstance().clearStubs()
  173. stub(url, data: testImageData)
  174. // Retry the download
  175. self.downloader.downloadImage(with: url) { result in
  176. XCTAssertNil(result.error)
  177. exp.fulfill()
  178. }
  179. }
  180. waitForExpectations(timeout: 3, handler: nil)
  181. }
  182. func testDownloadEmptyURL() {
  183. let exp = expectation(description: #function)
  184. let modifier = URLModifier(url: nil)
  185. let url = URL(string: "http://onevcat.com")!
  186. downloader.downloadImage(
  187. with: url,
  188. options: [.requestModifier(modifier)],
  189. progressBlock: { received, totalSize in XCTFail("The progress block should not be called.") })
  190. {
  191. result in
  192. XCTAssertNotNil(result.error)
  193. if case .requestError(reason: .invalidURL(let request)) = result.error! {
  194. XCTAssertNil(request.url)
  195. } else {
  196. XCTFail()
  197. }
  198. exp.fulfill()
  199. }
  200. waitForExpectations(timeout: 3, handler: nil)
  201. }
  202. func testDownloadTaskProperty() {
  203. let task = downloader.downloadImage(with: URL(string: "1234")!)
  204. XCTAssertNotNil(task, "The task should exist.")
  205. }
  206. func testCancelDownloadTask() {
  207. let exp = expectation(description: #function)
  208. let url = testURLs[0]
  209. let stub = delayedStub(url, data: testImageData, length: 123)
  210. let task = downloader.downloadImage(
  211. with: url,
  212. progressBlock: { _, _ in XCTFail() })
  213. {
  214. result in
  215. XCTAssertNotNil(result.error)
  216. XCTAssertTrue(result.error!.isTaskCancelled)
  217. delay(0.1) { exp.fulfill() }
  218. }
  219. XCTAssertTrue(task.isInitialized)
  220. task.cancel()
  221. _ = stub.go()
  222. waitForExpectations(timeout: 3, handler: nil)
  223. }
  224. func testCancelDownloadTaskAsync() async throws {
  225. let url = testURLs[0]
  226. let stub = delayedStub(url, data: testImageData, length: 123)
  227. let checker = CallingChecker()
  228. try await checker.checkCancelBehavior(stub: stub) {
  229. _ = try await self.downloader.downloadImage(with: url)
  230. }
  231. }
  232. func testCancelOneDownloadTask() {
  233. let exp = expectation(description: #function)
  234. let url = testURLs[0]
  235. let stub = delayedStub(url, data: testImageData)
  236. let group = DispatchGroup()
  237. group.enter()
  238. let task1 = downloader.downloadImage(with: url) { result in
  239. XCTAssertNotNil(result.error)
  240. group.leave()
  241. }
  242. group.enter()
  243. _ = downloader.downloadImage(with: url) { result in
  244. XCTAssertNotNil(result.value?.image)
  245. group.leave()
  246. }
  247. task1.cancel()
  248. delay(0.1) { _ = stub.go() }
  249. group.notify(queue: .main) {
  250. delay(0.1) { exp.fulfill() }
  251. }
  252. waitForExpectations(timeout: 3, handler: nil)
  253. }
  254. func testCancelAllDownloadTasks() {
  255. let exp = expectation(description: #function)
  256. let url1 = testURLs[0]
  257. let stub1 = delayedStub(url1, data: testImageData)
  258. let url2 = testURLs[1]
  259. let stub2 = delayedStub(url2, data: testImageData)
  260. let group = DispatchGroup()
  261. let urls = [url1, url1, url2]
  262. urls.forEach {
  263. group.enter()
  264. downloader.downloadImage(with: $0) { result in
  265. XCTAssertNotNil(result.error)
  266. XCTAssertTrue(result.error!.isTaskCancelled)
  267. group.leave()
  268. }
  269. }
  270. delay(0.1) {
  271. self.downloader.cancelAll()
  272. _ = stub1.go()
  273. _ = stub2.go()
  274. }
  275. group.notify(queue: .main) {
  276. delay(0.1) { exp.fulfill() }
  277. }
  278. waitForExpectations(timeout: 3, handler: nil)
  279. }
  280. func testCancelDownloadTaskForURL() {
  281. let exp = expectation(description: #function)
  282. let url1 = testURLs[0]
  283. let stub1 = delayedStub(url1, data: testImageData)
  284. let url2 = testURLs[1]
  285. let stub2 = delayedStub(url2, data: testImageData)
  286. let group = DispatchGroup()
  287. group.enter()
  288. downloader.downloadImage(with: url1) { result in
  289. XCTAssertNotNil(result.error)
  290. XCTAssertTrue(result.error!.isTaskCancelled)
  291. group.leave()
  292. }
  293. group.enter()
  294. downloader.downloadImage(with: url1) { result in
  295. XCTAssertNotNil(result.error)
  296. XCTAssertTrue(result.error!.isTaskCancelled)
  297. group.leave()
  298. }
  299. group.enter()
  300. downloader.downloadImage(with: url2) { result in
  301. XCTAssertNotNil(result.value)
  302. group.leave()
  303. }
  304. delay(0.1) {
  305. self.downloader.cancel(url: url1)
  306. _ = stub1.go()
  307. _ = stub2.go()
  308. }
  309. group.notify(queue: .main) {
  310. delay(0.1) { exp.fulfill() }
  311. }
  312. waitForExpectations(timeout: 3, handler: nil)
  313. }
  314. // Issue 532 https://github.com/onevcat/Kingfisher/issues/532#issuecomment-305644311
  315. func testCancelThenRestartSameDownload() {
  316. let exp = expectation(description: #function)
  317. let url = testURLs[0]
  318. let stub = delayedStub(url, data: testImageData, length: 123)
  319. let group = DispatchGroup()
  320. group.enter()
  321. let downloadTask = downloader.downloadImage(
  322. with: url,
  323. progressBlock: { _, _ in XCTFail()})
  324. {
  325. result in
  326. XCTAssertNotNil(result.error)
  327. XCTAssertTrue(result.error!.isTaskCancelled)
  328. group.leave()
  329. }
  330. XCTAssertTrue(downloadTask.isInitialized)
  331. downloadTask.cancel()
  332. _ = stub.go()
  333. group.enter()
  334. downloader.downloadImage(with: url) {
  335. result in
  336. XCTAssertNotNil(result.value)
  337. if let error = result.error {
  338. print(error)
  339. }
  340. group.leave()
  341. }
  342. group.notify(queue: .main) {
  343. delay(0.1) { exp.fulfill() }
  344. }
  345. waitForExpectations(timeout: 3, handler: nil)
  346. }
  347. func testDownloadTaskNilWithNilURL() {
  348. let modifier = URLModifier(url: nil)
  349. let downloadTask = downloader.downloadImage(with: URL(string: "url")!, options: [.requestModifier(modifier)])
  350. XCTAssertFalse(downloadTask.isInitialized)
  351. }
  352. func testDownloadWithProcessor() {
  353. let exp = expectation(description: #function)
  354. let url = testURLs[0]
  355. stub(url, data: testImageData)
  356. let p = RoundCornerImageProcessor(cornerRadius: 40)
  357. let roundCornered = testImage.kf.image(withRoundRadius: 40, fit: testImage.kf.size)
  358. downloader.downloadImage(with: url, options: [.processor(p)]) { result in
  359. XCTAssertNotNil(result.value)
  360. let image = result.value!.image
  361. XCTAssertFalse(image.renderEqual(to: testImage))
  362. XCTAssertTrue(image.renderEqual(to: roundCornered))
  363. XCTAssertEqual(result.value!.originalData, testImageData)
  364. exp.fulfill()
  365. }
  366. waitForExpectations(timeout: 3, handler: nil)
  367. }
  368. func testDownloadWithDifferentProcessors() {
  369. let exp = expectation(description: #function)
  370. let url = testURLs[0]
  371. let stub = delayedStub(url, data: testImageData)
  372. let p1 = RoundCornerImageProcessor(cornerRadius: 40)
  373. let roundCornered = testImage.kf.image(withRoundRadius: 40, fit: testImage.kf.size)
  374. let p2 = BlurImageProcessor(blurRadius: 3.0)
  375. let blurred = testImage.kf.blurred(withRadius: 3.0)
  376. let group = DispatchGroup()
  377. group.enter()
  378. let task1 = downloader.downloadImage(with: url, options: [.processor(p1)]) { result in
  379. XCTAssertTrue(result.value!.image.renderEqual(to: roundCornered))
  380. group.leave()
  381. }
  382. group.enter()
  383. let task2 = downloader.downloadImage(with: url, options: [.processor(p2)]) { result in
  384. XCTAssertTrue(result.value!.image.renderEqual(to: blurred))
  385. group.leave()
  386. }
  387. XCTAssertNotNil(task1)
  388. XCTAssertEqual(task1.sessionTask?.task, task2.sessionTask?.task)
  389. _ = stub.go()
  390. group.notify(queue: .main, execute: exp.fulfill)
  391. waitForExpectations(timeout: 3, handler: nil)
  392. }
  393. func testDownloadedDataCouldBeModified() {
  394. let exp = expectation(description: #function)
  395. let url = testURLs[0]
  396. stub(url, data: testImageData)
  397. let modifier = URLNilDataModifier()
  398. downloader.delegate = modifier
  399. downloader.downloadImage(with: url) { result in
  400. XCTAssertNil(result.value)
  401. XCTAssertNotNil(result.error)
  402. if case .responseError(reason: .dataModifyingFailed) = result.error! {
  403. } else {
  404. XCTFail()
  405. }
  406. self.downloader.delegate = nil
  407. // hold delegate
  408. _ = modifier
  409. exp.fulfill()
  410. }
  411. waitForExpectations(timeout: 3, handler: nil)
  412. }
  413. func testDownloadedDataCouldBeModifiedWithTask() {
  414. let exp = expectation(description: #function)
  415. let url = testURLs[0]
  416. stub(url, data: testImageData)
  417. let modifier = TaskNilDataModifier()
  418. downloader.delegate = modifier
  419. downloader.downloadImage(with: url) { result in
  420. XCTAssertNil(result.value)
  421. XCTAssertNotNil(result.error)
  422. if case .responseError(reason: .dataModifyingFailed) = result.error! {
  423. } else {
  424. XCTFail()
  425. }
  426. self.downloader.delegate = nil
  427. // hold delegate
  428. _ = modifier
  429. exp.fulfill()
  430. }
  431. waitForExpectations(timeout: 3, handler: nil)
  432. }
  433. #if os(iOS) || os(tvOS) || os(watchOS) || os(visionOS)
  434. func testModifierShouldOnlyApplyForFinalResultWhenDownload() {
  435. let exp = expectation(description: #function)
  436. let url = testURLs[0]
  437. stub(url, data: testImageData)
  438. let modifierCalled = LockIsolated(false)
  439. let modifier = AnyImageModifier { image in
  440. modifierCalled.setValue(true)
  441. return image.withRenderingMode(.alwaysTemplate)
  442. }
  443. downloader.downloadImage(with: url, options: [.imageModifier(modifier)]) { result in
  444. XCTAssertEqual(result.value?.image.renderingMode, .automatic)
  445. XCTAssertFalse(modifierCalled.value)
  446. exp.fulfill()
  447. }
  448. waitForExpectations(timeout: 3, handler: nil)
  449. }
  450. #endif
  451. func testDownloadTaskTakePriorityOption() {
  452. let exp = expectation(description: #function)
  453. let url = testURLs[0]
  454. stub(url, data: testImageData)
  455. let task = downloader.downloadImage(with: url, options: [.downloadPriority(URLSessionTask.highPriority)])
  456. {
  457. _ in
  458. exp.fulfill()
  459. }
  460. XCTAssertEqual(task.sessionTask?.task.priority, URLSessionTask.highPriority)
  461. waitForExpectations(timeout: 3, handler: nil)
  462. }
  463. func testSessionDelegate() {
  464. class ExtensionDelegate: SessionDelegate, @unchecked Sendable {
  465. //'exp' only for test
  466. public let exp: XCTestExpectation
  467. init(_ expectation:XCTestExpectation) {
  468. exp = expectation
  469. }
  470. override func urlSession(_ session: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics) {
  471. exp.fulfill()
  472. }
  473. }
  474. downloader.sessionDelegate = ExtensionDelegate(expectation(description: #function))
  475. let url = testURLs[0]
  476. stub(url, data: testImageData)
  477. downloader.downloadImage(with: url) { result in
  478. XCTAssertNotNil(result.value)
  479. }
  480. waitForExpectations(timeout: 3, handler: nil)
  481. }
  482. func testDownloaderReceiveResponsePass() {
  483. let exp = expectation(description: #function)
  484. let url = testURLs[0]
  485. stub(url, data: testImageData, headers: ["someKey": "someValue"])
  486. let handler = TaskResponseCompletion()
  487. let obj = NSObject()
  488. handler.onReceiveResponse.delegate(on: obj) { (obj, response) in
  489. guard let httpResponse = response as? HTTPURLResponse else {
  490. XCTFail("Should be an HTTP response.")
  491. return .cancel
  492. }
  493. XCTAssertEqual(httpResponse.statusCode, 200)
  494. XCTAssertEqual(httpResponse.url, url)
  495. XCTAssertEqual(httpResponse.allHeaderFields["someKey"] as? String, "someValue")
  496. return .allow
  497. }
  498. downloader.delegate = handler
  499. downloader.downloadImage(with: url) { result in
  500. XCTAssertNotNil(result.value)
  501. XCTAssertNil(result.error)
  502. self.downloader.delegate = nil
  503. // hold delegate
  504. _ = handler
  505. exp.fulfill()
  506. }
  507. waitForExpectations(timeout: 3, handler: nil)
  508. }
  509. func testDownloaderReceiveResponseFailure() {
  510. let exp = expectation(description: #function)
  511. let url = testURLs[0]
  512. stub(url, data: testImageData, headers: ["someKey": "someValue"])
  513. let handler = TaskResponseCompletion()
  514. let obj = NSObject()
  515. handler.onReceiveResponse.delegate(on: obj) { (obj, response) in
  516. guard let httpResponse = response as? HTTPURLResponse else {
  517. XCTFail("Should be an HTTP response.")
  518. return .cancel
  519. }
  520. XCTAssertEqual(httpResponse.statusCode, 200)
  521. XCTAssertEqual(httpResponse.url, url)
  522. XCTAssertEqual(httpResponse.allHeaderFields["someKey"] as? String, "someValue")
  523. return .cancel
  524. }
  525. downloader.delegate = handler
  526. downloader.downloadImage(with: url) { result in
  527. XCTAssertNil(result.value)
  528. XCTAssertNotNil(result.error)
  529. if case .responseError(reason: .cancelledByDelegate) = result.error! {
  530. } else {
  531. XCTFail()
  532. }
  533. self.downloader.delegate = nil
  534. // hold delegate
  535. _ = handler
  536. exp.fulfill()
  537. }
  538. waitForExpectations(timeout: 3, handler: nil)
  539. }
  540. func testDownloadingLivePhotoResources() async throws {
  541. let url = testURLs[0]
  542. stub(url, data: testImageData)
  543. let result = try await downloader.downloadLivePhotoResource(with: url, options: .init(.empty))
  544. XCTAssertEqual(result.originalData, testImageData)
  545. XCTAssertEqual(result.url, url)
  546. }
  547. func testConcurrentDownloadSameURL() {
  548. let exp = expectation(description: #function)
  549. // Given
  550. let url = testURLs[0]
  551. stub(url, data: testImageData)
  552. final class CallbackCounter: @unchecked Sendable {
  553. private let lock = NSLock()
  554. private var value = 0
  555. func increment() {
  556. lock.lock()
  557. value += 1
  558. lock.unlock()
  559. }
  560. func read() -> Int {
  561. lock.lock()
  562. let current = value
  563. lock.unlock()
  564. return current
  565. }
  566. }
  567. let callbackCounter = CallbackCounter()
  568. let expectedCount = 10
  569. exp.expectedFulfillmentCount = expectedCount
  570. // When
  571. DispatchQueue.concurrentPerform(iterations: expectedCount) { index in
  572. downloader.downloadImage(with: url) { result in
  573. switch result {
  574. case .success(let imageResult):
  575. XCTAssertNotNil(imageResult.image)
  576. XCTAssertEqual(imageResult.url, url)
  577. XCTAssertEqual(imageResult.originalData, testImageData)
  578. callbackCounter.increment()
  579. exp.fulfill()
  580. case .failure(let error):
  581. XCTFail("Download should succeed: \(error)")
  582. exp.fulfill()
  583. }
  584. }
  585. }
  586. // Then
  587. waitForExpectations(timeout: 3) { _ in
  588. let finalCount = callbackCounter.read()
  589. XCTAssertEqual(finalCount, expectedCount, "All \(expectedCount) concurrent requests should receive callbacks")
  590. }
  591. }
  592. }
  593. class URLNilDataModifier: ImageDownloaderDelegate {
  594. func imageDownloader(_ downloader: ImageDownloader, didDownload data: Data, for url: URL) -> Data? {
  595. return nil
  596. }
  597. }
  598. class TaskNilDataModifier: ImageDownloaderDelegate {
  599. func imageDownloader(_ downloader: ImageDownloader, didDownload data: Data, with dataTask: SessionDataTask) -> Data? {
  600. return nil
  601. }
  602. }
  603. class TaskResponseCompletion: ImageDownloaderDelegate {
  604. let onReceiveResponse = Delegate<URLResponse, URLSession.ResponseDisposition>()
  605. func imageDownloader(_ downloader: ImageDownloader, didReceive response: URLResponse) async -> URLSession.ResponseDisposition {
  606. return onReceiveResponse.call(response)!
  607. }
  608. }
  609. final class URLModifier: ImageDownloadRequestModifier {
  610. let url: URL?
  611. init(url: URL?) {
  612. self.url = url
  613. }
  614. func modified(for request: URLRequest) -> URLRequest? {
  615. var r = request
  616. r.url = url
  617. return r
  618. }
  619. }
  620. final class AsyncURLModifier: AsyncImageDownloadRequestModifier {
  621. let url: URL?
  622. let onDownloadTaskStarted: (@Sendable (DownloadTask?) -> Void)?
  623. init(url: URL?, onDownloadTaskStarted: (@Sendable (DownloadTask?) -> Void)?) {
  624. self.url = url
  625. self.onDownloadTaskStarted = onDownloadTaskStarted
  626. }
  627. func modified(for request: URLRequest) async -> URLRequest? {
  628. var r = request
  629. r.url = url
  630. // Simulate an async action
  631. try? await Task.sleep(nanoseconds: 1_000_000)
  632. return r
  633. }
  634. }