ImageDownloaderTests.swift 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780
  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 url = testURLs[0]
  122. stub(url, data: testImageData)
  123. let downloadTaskCalled = ActorBox(false)
  124. let asyncModifier = AsyncURLModifier(url: url, onDownloadTaskStarted: { task in
  125. XCTAssertNotNil(task)
  126. Task {
  127. await downloadTaskCalled.setValue(true)
  128. }
  129. })
  130. let someURL = URL(string: "some_strange_url")!
  131. let task = downloader.downloadImage(with: someURL, options: [.requestModifier(asyncModifier)]) { result in
  132. XCTAssertNotNil(result.value)
  133. XCTAssertEqual(result.value?.url, url)
  134. Task {
  135. let result = await downloadTaskCalled.value
  136. XCTAssertTrue(result)
  137. exp.fulfill()
  138. }
  139. }
  140. // The returned task is nil since the download is not starting immediately.
  141. XCTAssertFalse(task.isInitialized)
  142. waitForExpectations(timeout: 3, handler: nil)
  143. }
  144. func testDownloadWithModifyingRequestToNil() {
  145. let nilModifier = AnyModifier { _ in
  146. return nil
  147. }
  148. let exp = expectation(description: #function)
  149. let someURL = URL(string: "some_strange_url")!
  150. downloader.downloadImage(with: someURL, options: [.requestModifier(nilModifier)]) { result in
  151. XCTAssertNotNil(result.error)
  152. guard case .requestError(reason: .emptyRequest) = result.error! else {
  153. XCTFail()
  154. fatalError()
  155. }
  156. exp.fulfill()
  157. }
  158. waitForExpectations(timeout: 3, handler: nil)
  159. }
  160. func testServerInvalidStatusCode() {
  161. let exp = expectation(description: #function)
  162. let url = testURLs[0]
  163. stub(url, data: testImageData, statusCode: 404)
  164. downloader.downloadImage(with: url) { result in
  165. XCTAssertNotNil(result.error)
  166. XCTAssertTrue(result.error!.isInvalidResponseStatusCode(404))
  167. exp.fulfill()
  168. }
  169. waitForExpectations(timeout: 3, handler: nil)
  170. }
  171. func testDownloadResultErrorAndRetry() {
  172. let exp = expectation(description: #function)
  173. let url = testURLs[0]
  174. stub(url, errorCode: -1)
  175. downloader.downloadImage(with: url) { result in
  176. XCTAssertNotNil(result.error)
  177. LSNocilla.sharedInstance().clearStubs()
  178. stub(url, data: testImageData)
  179. // Retry the download
  180. self.downloader.downloadImage(with: url) { result in
  181. XCTAssertNil(result.error)
  182. exp.fulfill()
  183. }
  184. }
  185. waitForExpectations(timeout: 3, handler: nil)
  186. }
  187. func testDownloadEmptyURL() {
  188. let exp = expectation(description: #function)
  189. let modifier = URLModifier(url: nil)
  190. let url = URL(string: "http://onevcat.com")!
  191. downloader.downloadImage(
  192. with: url,
  193. options: [.requestModifier(modifier)],
  194. progressBlock: { received, totalSize in XCTFail("The progress block should not be called.") })
  195. {
  196. result in
  197. XCTAssertNotNil(result.error)
  198. if case .requestError(reason: .invalidURL(let request)) = result.error! {
  199. XCTAssertNil(request.url)
  200. } else {
  201. XCTFail()
  202. }
  203. exp.fulfill()
  204. }
  205. waitForExpectations(timeout: 3, handler: nil)
  206. }
  207. func testDownloadTaskProperty() {
  208. let task = downloader.downloadImage(with: URL(string: "1234")!)
  209. XCTAssertNotNil(task, "The task should exist.")
  210. }
  211. func testCancelDownloadTask() {
  212. let exp = expectation(description: #function)
  213. let url = testURLs[0]
  214. let stub = delayedStub(url, data: testImageData, length: 123)
  215. let task = downloader.downloadImage(
  216. with: url,
  217. progressBlock: { _, _ in XCTFail() })
  218. {
  219. result in
  220. XCTAssertNotNil(result.error)
  221. XCTAssertTrue(result.error!.isTaskCancelled)
  222. delay(0.1) { exp.fulfill() }
  223. }
  224. XCTAssertTrue(task.isInitialized)
  225. task.cancel()
  226. _ = stub.go()
  227. waitForExpectations(timeout: 3, handler: nil)
  228. }
  229. func testCancelDownloadTaskAsync() async throws {
  230. let url = testURLs[0]
  231. let stub = delayedStub(url, data: testImageData, length: 123)
  232. let checker = CallingChecker()
  233. try await checker.checkCancelBehavior(stub: stub) {
  234. _ = try await self.downloader.downloadImage(with: url)
  235. }
  236. }
  237. func testCancelOneDownloadTask() {
  238. let exp = expectation(description: #function)
  239. let url = testURLs[0]
  240. let stub = delayedStub(url, data: testImageData)
  241. let group = DispatchGroup()
  242. group.enter()
  243. let task1 = downloader.downloadImage(with: url) { result in
  244. XCTAssertNotNil(result.error)
  245. group.leave()
  246. }
  247. group.enter()
  248. _ = downloader.downloadImage(with: url) { result in
  249. XCTAssertNotNil(result.value?.image)
  250. group.leave()
  251. }
  252. task1.cancel()
  253. delay(0.1) { _ = stub.go() }
  254. group.notify(queue: .main) {
  255. delay(0.1) { exp.fulfill() }
  256. }
  257. waitForExpectations(timeout: 3, handler: nil)
  258. }
  259. func testCancelAllDownloadTasks() {
  260. let exp = expectation(description: #function)
  261. let url1 = testURLs[0]
  262. let stub1 = delayedStub(url1, data: testImageData)
  263. let url2 = testURLs[1]
  264. let stub2 = delayedStub(url2, data: testImageData)
  265. let group = DispatchGroup()
  266. let urls = [url1, url1, url2]
  267. urls.forEach {
  268. group.enter()
  269. downloader.downloadImage(with: $0) { result in
  270. XCTAssertNotNil(result.error)
  271. XCTAssertTrue(result.error!.isTaskCancelled)
  272. group.leave()
  273. }
  274. }
  275. delay(0.1) {
  276. self.downloader.cancelAll()
  277. _ = stub1.go()
  278. _ = stub2.go()
  279. }
  280. group.notify(queue: .main) {
  281. delay(0.1) { exp.fulfill() }
  282. }
  283. waitForExpectations(timeout: 3, handler: nil)
  284. }
  285. func testCancelDownloadTaskForURL() {
  286. let exp = expectation(description: #function)
  287. let url1 = testURLs[0]
  288. let stub1 = delayedStub(url1, data: testImageData)
  289. let url2 = testURLs[1]
  290. let stub2 = delayedStub(url2, data: testImageData)
  291. let group = DispatchGroup()
  292. group.enter()
  293. downloader.downloadImage(with: url1) { result in
  294. XCTAssertNotNil(result.error)
  295. XCTAssertTrue(result.error!.isTaskCancelled)
  296. group.leave()
  297. }
  298. group.enter()
  299. downloader.downloadImage(with: url1) { result in
  300. XCTAssertNotNil(result.error)
  301. XCTAssertTrue(result.error!.isTaskCancelled)
  302. group.leave()
  303. }
  304. group.enter()
  305. downloader.downloadImage(with: url2) { result in
  306. XCTAssertNotNil(result.value)
  307. group.leave()
  308. }
  309. delay(0.1) {
  310. self.downloader.cancel(url: url1)
  311. _ = stub1.go()
  312. _ = stub2.go()
  313. }
  314. group.notify(queue: .main) {
  315. delay(0.1) { exp.fulfill() }
  316. }
  317. waitForExpectations(timeout: 3, handler: nil)
  318. }
  319. // Issue 532 https://github.com/onevcat/Kingfisher/issues/532#issuecomment-305644311
  320. func testCancelThenRestartSameDownload() {
  321. let exp = expectation(description: #function)
  322. let url = testURLs[0]
  323. let stub = delayedStub(url, data: testImageData, length: 123)
  324. let group = DispatchGroup()
  325. group.enter()
  326. let downloadTask = downloader.downloadImage(
  327. with: url,
  328. progressBlock: { _, _ in XCTFail()})
  329. {
  330. result in
  331. XCTAssertNotNil(result.error)
  332. XCTAssertTrue(result.error!.isTaskCancelled)
  333. group.leave()
  334. }
  335. XCTAssertTrue(downloadTask.isInitialized)
  336. downloadTask.cancel()
  337. _ = stub.go()
  338. group.enter()
  339. downloader.downloadImage(with: url) {
  340. result in
  341. XCTAssertNotNil(result.value)
  342. if let error = result.error {
  343. print(error)
  344. }
  345. group.leave()
  346. }
  347. group.notify(queue: .main) {
  348. delay(0.1) { exp.fulfill() }
  349. }
  350. waitForExpectations(timeout: 3, handler: nil)
  351. }
  352. func testDownloadTaskNilWithNilURL() {
  353. let modifier = URLModifier(url: nil)
  354. let downloadTask = downloader.downloadImage(with: URL(string: "url")!, options: [.requestModifier(modifier)])
  355. XCTAssertFalse(downloadTask.isInitialized)
  356. }
  357. func testDownloadWithProcessor() {
  358. let exp = expectation(description: #function)
  359. let url = testURLs[0]
  360. stub(url, data: testImageData)
  361. let p = RoundCornerImageProcessor(cornerRadius: 40)
  362. let roundCornered = testImage.kf.image(withRoundRadius: 40, fit: testImage.kf.size)
  363. downloader.downloadImage(with: url, options: [.processor(p)]) { result in
  364. XCTAssertNotNil(result.value)
  365. let image = result.value!.image
  366. XCTAssertFalse(image.renderEqual(to: testImage))
  367. XCTAssertTrue(image.renderEqual(to: roundCornered))
  368. XCTAssertEqual(result.value!.originalData, testImageData)
  369. exp.fulfill()
  370. }
  371. waitForExpectations(timeout: 3, handler: nil)
  372. }
  373. func testDownloadWithDifferentProcessors() {
  374. let exp = expectation(description: #function)
  375. let url = testURLs[0]
  376. let stub = delayedStub(url, data: testImageData)
  377. let p1 = RoundCornerImageProcessor(cornerRadius: 40)
  378. let roundCornered = testImage.kf.image(withRoundRadius: 40, fit: testImage.kf.size)
  379. let p2 = BlurImageProcessor(blurRadius: 3.0)
  380. let blurred = testImage.kf.blurred(withRadius: 3.0)
  381. let group = DispatchGroup()
  382. group.enter()
  383. let task1 = downloader.downloadImage(with: url, options: [.processor(p1)]) { result in
  384. XCTAssertTrue(result.value!.image.renderEqual(to: roundCornered))
  385. group.leave()
  386. }
  387. group.enter()
  388. let task2 = downloader.downloadImage(with: url, options: [.processor(p2)]) { result in
  389. XCTAssertTrue(result.value!.image.renderEqual(to: blurred))
  390. group.leave()
  391. }
  392. XCTAssertNotNil(task1)
  393. XCTAssertEqual(task1.sessionTask?.task, task2.sessionTask?.task)
  394. _ = stub.go()
  395. group.notify(queue: .main, execute: exp.fulfill)
  396. waitForExpectations(timeout: 3, handler: nil)
  397. }
  398. func testDownloadedDataCouldBeModified() {
  399. let exp = expectation(description: #function)
  400. let url = testURLs[0]
  401. stub(url, data: testImageData)
  402. let modifier = URLNilDataModifier()
  403. downloader.delegate = modifier
  404. downloader.downloadImage(with: url) { result in
  405. XCTAssertNil(result.value)
  406. XCTAssertNotNil(result.error)
  407. if case .responseError(reason: .dataModifyingFailed) = result.error! {
  408. } else {
  409. XCTFail()
  410. }
  411. self.downloader.delegate = nil
  412. // hold delegate
  413. _ = modifier
  414. exp.fulfill()
  415. }
  416. waitForExpectations(timeout: 3, handler: nil)
  417. }
  418. func testDownloadedDataCouldBeModifiedWithTask() {
  419. let exp = expectation(description: #function)
  420. let url = testURLs[0]
  421. stub(url, data: testImageData)
  422. let modifier = TaskNilDataModifier()
  423. downloader.delegate = modifier
  424. downloader.downloadImage(with: url) { result in
  425. XCTAssertNil(result.value)
  426. XCTAssertNotNil(result.error)
  427. if case .responseError(reason: .dataModifyingFailed) = result.error! {
  428. } else {
  429. XCTFail()
  430. }
  431. self.downloader.delegate = nil
  432. // hold delegate
  433. _ = modifier
  434. exp.fulfill()
  435. }
  436. waitForExpectations(timeout: 3, handler: nil)
  437. }
  438. #if os(iOS) || os(tvOS) || os(watchOS) || os(visionOS)
  439. func testModifierShouldOnlyApplyForFinalResultWhenDownload() {
  440. let exp = expectation(description: #function)
  441. let url = testURLs[0]
  442. stub(url, data: testImageData)
  443. let modifierCalled = ActorBox(false)
  444. let modifier = AnyImageModifier { image in
  445. Task {
  446. await modifierCalled.setValue(true)
  447. }
  448. return image.withRenderingMode(.alwaysTemplate)
  449. }
  450. downloader.downloadImage(with: url, options: [.imageModifier(modifier)]) { result in
  451. XCTAssertEqual(result.value?.image.renderingMode, .automatic)
  452. Task {
  453. let called = await modifierCalled.value
  454. XCTAssertFalse(called)
  455. exp.fulfill()
  456. }
  457. }
  458. waitForExpectations(timeout: 3, handler: nil)
  459. }
  460. #endif
  461. func testDownloadTaskTakePriorityOption() {
  462. let exp = expectation(description: #function)
  463. let url = testURLs[0]
  464. stub(url, data: testImageData)
  465. let task = downloader.downloadImage(with: url, options: [.downloadPriority(URLSessionTask.highPriority)])
  466. {
  467. _ in
  468. exp.fulfill()
  469. }
  470. XCTAssertEqual(task.sessionTask?.task.priority, URLSessionTask.highPriority)
  471. waitForExpectations(timeout: 3, handler: nil)
  472. }
  473. func testSessionDelegate() {
  474. class ExtensionDelegate: SessionDelegate, @unchecked Sendable {
  475. //'exp' only for test
  476. public let exp: XCTestExpectation
  477. init(_ expectation:XCTestExpectation) {
  478. exp = expectation
  479. }
  480. override func urlSession(_ session: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics) {
  481. exp.fulfill()
  482. }
  483. }
  484. downloader.sessionDelegate = ExtensionDelegate(expectation(description: #function))
  485. let url = testURLs[0]
  486. stub(url, data: testImageData)
  487. downloader.downloadImage(with: url) { result in
  488. XCTAssertNotNil(result.value)
  489. }
  490. waitForExpectations(timeout: 3, handler: nil)
  491. }
  492. func testDownloaderReceiveResponsePass() {
  493. let exp = expectation(description: #function)
  494. let url = testURLs[0]
  495. stub(url, data: testImageData, headers: ["someKey": "someValue"])
  496. let handler = TaskResponseCompletion()
  497. let obj = NSObject()
  498. handler.onReceiveResponse.delegate(on: obj) { (obj, response) in
  499. guard let httpResponse = response as? HTTPURLResponse else {
  500. XCTFail("Should be an HTTP response.")
  501. return .cancel
  502. }
  503. XCTAssertEqual(httpResponse.statusCode, 200)
  504. XCTAssertEqual(httpResponse.url, url)
  505. XCTAssertEqual(httpResponse.allHeaderFields["someKey"] as? String, "someValue")
  506. return .allow
  507. }
  508. downloader.delegate = handler
  509. downloader.downloadImage(with: url) { result in
  510. XCTAssertNotNil(result.value)
  511. XCTAssertNil(result.error)
  512. self.downloader.delegate = nil
  513. // hold delegate
  514. _ = handler
  515. exp.fulfill()
  516. }
  517. waitForExpectations(timeout: 3, handler: nil)
  518. }
  519. func testDownloaderReceiveResponseFailure() {
  520. let exp = expectation(description: #function)
  521. let url = testURLs[0]
  522. stub(url, data: testImageData, headers: ["someKey": "someValue"])
  523. let handler = TaskResponseCompletion()
  524. let obj = NSObject()
  525. handler.onReceiveResponse.delegate(on: obj) { (obj, response) in
  526. guard let httpResponse = response as? HTTPURLResponse else {
  527. XCTFail("Should be an HTTP response.")
  528. return .cancel
  529. }
  530. XCTAssertEqual(httpResponse.statusCode, 200)
  531. XCTAssertEqual(httpResponse.url, url)
  532. XCTAssertEqual(httpResponse.allHeaderFields["someKey"] as? String, "someValue")
  533. return .cancel
  534. }
  535. downloader.delegate = handler
  536. downloader.downloadImage(with: url) { result in
  537. XCTAssertNil(result.value)
  538. XCTAssertNotNil(result.error)
  539. if case .responseError(reason: .cancelledByDelegate) = result.error! {
  540. } else {
  541. XCTFail()
  542. }
  543. self.downloader.delegate = nil
  544. // hold delegate
  545. _ = handler
  546. exp.fulfill()
  547. }
  548. waitForExpectations(timeout: 3, handler: nil)
  549. }
  550. func testDownloadingLivePhotoResources() async throws {
  551. let url = testURLs[0]
  552. stub(url, data: testImageData)
  553. let result = try await downloader.downloadLivePhotoResource(with: url, options: .init(.empty))
  554. XCTAssertEqual(result.originalData, testImageData)
  555. XCTAssertEqual(result.url, url)
  556. }
  557. func testConcurrentDownloadSameURL() {
  558. let exp = expectation(description: #function)
  559. // Given
  560. let url = testURLs[0]
  561. stub(url, data: testImageData)
  562. let callbackLock = NSLock()
  563. var callbackCount = 0
  564. let expectedCount = 10
  565. exp.expectedFulfillmentCount = expectedCount
  566. // When
  567. DispatchQueue.concurrentPerform(iterations: expectedCount) { index in
  568. downloader.downloadImage(with: url) { result in
  569. switch result {
  570. case .success(let imageResult):
  571. XCTAssertNotNil(imageResult.image)
  572. XCTAssertEqual(imageResult.url, url)
  573. XCTAssertEqual(imageResult.originalData, testImageData)
  574. callbackLock.lock()
  575. callbackCount += 1
  576. callbackLock.unlock()
  577. exp.fulfill()
  578. case .failure(let error):
  579. XCTFail("Download should succeed: \(error)")
  580. exp.fulfill()
  581. }
  582. }
  583. }
  584. // Then
  585. waitForExpectations(timeout: 3) { _ in
  586. callbackLock.lock()
  587. let finalCount = callbackCount
  588. callbackLock.unlock()
  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. }