| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079 |
- //
- // ImageCacheTests.swift
- // Kingfisher
- //
- // Created by Wei Wang on 15/4/10.
- //
- // Copyright (c) 2019 Wei Wang <onevcat@gmail.com>
- //
- // Permission is hereby granted, free of charge, to any person obtaining a copy
- // of this software and associated documentation files (the "Software"), to deal
- // in the Software without restriction, including without limitation the rights
- // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- // copies of the Software, and to permit persons to whom the Software is
- // furnished to do so, subject to the following conditions:
- //
- // The above copyright notice and this permission notice shall be included in
- // all copies or substantial portions of the Software.
- //
- // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- // THE SOFTWARE.
- import XCTest
- @testable import Kingfisher
- class ImageCacheTests: XCTestCase {
- var cache: ImageCache!
- var observer: NSObjectProtocol!
-
- override func setUp() {
- super.setUp()
- let uuid = UUID().uuidString
- let cacheName = "test-\(uuid)"
- cache = ImageCache(name: cacheName)
- }
-
- override func tearDown() {
- clearCaches([cache])
- cache = nil
- if let o = observer {
- NotificationCenter.default.removeObserver(o)
- observer = nil
- }
- super.tearDown()
- }
-
- func testInvalidCustomCachePath() {
- let customPath = "/path/to/image/cache"
- let url = URL(fileURLWithPath: customPath)
- XCTAssertThrowsError(try ImageCache(name: "test", cacheDirectoryURL: url)) { error in
- guard case KingfisherError.cacheError(reason: .cannotCreateDirectory(let path, _)) = error else {
- XCTFail("Should be KingfisherError with cacheError reason.")
- return
- }
- XCTAssertEqual(path, customPath + "/com.onevcat.Kingfisher.ImageCache.test")
- }
- }
- func testCustomCachePath() {
- let cacheURL = try! FileManager.default.url(
- for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
- let subFolder = cacheURL.appendingPathComponent("temp")
- let customPath = subFolder.path
- let cache = try! ImageCache(name: "test", cacheDirectoryURL: subFolder)
- XCTAssertEqual(
- cache.diskStorage.directoryURL.path,
- (customPath as NSString).appendingPathComponent("com.onevcat.Kingfisher.ImageCache.test"))
- clearCaches([cache])
- }
-
- func testCustomCachePathByBlock() {
- let cache = try! ImageCache(name: "test", cacheDirectoryURL: nil, diskCachePathClosure: { (url, path) -> URL in
- let modifiedPath = path + "-modified"
- return url.appendingPathComponent(modifiedPath, isDirectory: true)
- })
- let cacheURL = try! FileManager.default.url(
- for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
- XCTAssertEqual(
- cache.diskStorage.directoryURL.path,
- (cacheURL.path as NSString).appendingPathComponent("com.onevcat.Kingfisher.ImageCache.test-modified"))
- clearCaches([cache])
- }
-
- func testMaxCachePeriodInSecond() {
- cache.diskStorage.config.expiration = .seconds(1)
- XCTAssertEqual(cache.diskStorage.config.expiration.timeInterval, 1)
- }
-
- func testMaxMemorySize() {
- cache.memoryStorage.config.totalCostLimit = 1
- XCTAssert(cache.memoryStorage.config.totalCostLimit == 1, "maxMemoryCost should be able to be set.")
- }
-
- func testMaxDiskCacheSize() {
- cache.diskStorage.config.sizeLimit = 1
- XCTAssert(cache.diskStorage.config.sizeLimit == 1, "maxDiskCacheSize should be able to be set.")
- }
-
- func testClearDiskCache() {
- let exp = expectation(description: #function)
- let key = testKeys[0]
- cache.store(testImage, original: testImageData, forKey: key, toDisk: true) { _ in
- self.cache.clearMemoryCache()
- let cacheResult = self.cache.imageCachedType(forKey: key)
- XCTAssertTrue(cacheResult.cached)
- XCTAssertEqual(cacheResult, .disk)
-
- self.cache.clearDiskCache {
- let cacheResult = self.cache.imageCachedType(forKey: key)
- XCTAssertFalse(cacheResult.cached)
- exp.fulfill()
- }
- }
- waitForExpectations(timeout: 3, handler:nil)
- }
-
- func testClearDiskCacheAsync() async throws {
- let key = testKeys[0]
- try await cache.store(testImage, original: testImageData, forKey: key, toDisk: true)
- cache.clearMemoryCache()
- var cacheResult = self.cache.imageCachedType(forKey: key)
- XCTAssertTrue(cacheResult.cached)
- XCTAssertEqual(cacheResult, .disk)
-
- await cache.clearDiskCache()
- cacheResult = cache.imageCachedType(forKey: key)
- XCTAssertFalse(cacheResult.cached)
- }
-
- func testClearMemoryCache() {
- let exp = expectation(description: #function)
- let key = testKeys[0]
- cache.store(testImage, original: testImageData, forKey: key, toDisk: true) { _ in
- self.cache.clearMemoryCache()
- self.cache.retrieveImage(forKey: key) { result in
- XCTAssertNotNil(result.value?.image)
- XCTAssertEqual(result.value?.cacheType, .disk)
- exp.fulfill()
- }
- }
- waitForExpectations(timeout: 3, handler: nil)
- }
-
- func testClearMemoryCacheAsync() async throws {
- let key = testKeys[0]
- try await cache.store(testImage, original: testImageData, forKey: key, toDisk: true)
- cache.clearMemoryCache()
- let result = try await cache.retrieveImage(forKey: key)
- XCTAssertNotNil(result.image)
- XCTAssertEqual(result.cacheType, .disk)
- }
-
- func testNoImageFound() {
- let exp = expectation(description: #function)
- cache.retrieveImage(forKey: testKeys[0]) { result in
- XCTAssertNotNil(result.value)
- XCTAssertNil(result.value!.image)
- exp.fulfill()
- }
- waitForExpectations(timeout: 3, handler: nil)
- }
-
- func testNoImageFoundAsync() async throws {
- let result = try await cache.retrieveImage(forKey: testKeys[0])
- XCTAssertNil(result.image)
- }
- func testCachedFileDoesNotExist() {
- let URLString = testKeys[0]
- let url = URL(string: URLString)!
- let exists = cache.imageCachedType(forKey: url.cacheKey).cached
- XCTAssertFalse(exists)
- }
-
- func testStoreImageInMemory() {
- let exp = expectation(description: #function)
- let key = testKeys[0]
- cache.store(testImage, forKey: key, toDisk: false) { _ in
- self.cache.retrieveImage(forKey: key) { result in
- XCTAssertNotNil(result.value?.image)
- XCTAssertEqual(result.value?.cacheType, .memory)
- exp.fulfill()
- }
- }
- waitForExpectations(timeout: 3, handler: nil)
- }
-
- func testStoreImageInMemoryAsync() async throws {
- let key = testKeys[0]
- try await cache.store(testImage, forKey: key, toDisk: false)
- let result = try await cache.retrieveImage(forKey: key)
- XCTAssertNotNil(result.image)
- XCTAssertEqual(result.cacheType, .memory)
- }
- func testStoreGIFToDiskWithNilOriginalShouldPreserveGIFFormat() {
- struct TestProcessor: ImageProcessor {
- let identifier: String = "com.onevcat.KingfisherTests.TestProcessor"
- func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {
- switch item {
- case .image(let image): return image
- case .data(let data): return DefaultImageProcessor.default.process(item: .data(data), options: options)
- }
- }
- }
- let exp = expectation(description: #function)
- let image = KingfisherWrapper<KFCrossPlatformImage>.animatedImage(data: testImageGIFData, options: .init())!
- XCTAssertEqual(image.kf.gifRepresentation()?.kf.imageFormat, .GIF)
- let options = KingfisherParsedOptionsInfo([.processor(TestProcessor())])
- let key = "test-gif"
- cache.store(image, original: nil, forKey: key, options: options, toDisk: true) { _ in
- do {
- let storedKey = key.computedKey(with: TestProcessor().identifier)
- let storedData = try self.cache.diskStorage.value(forKey: storedKey)
- XCTAssertEqual(storedData?.kf.imageFormat, .GIF)
- } catch {
- XCTFail("Unexpected error: \(error)")
- }
- exp.fulfill()
- }
- waitForExpectations(timeout: 3, handler: nil)
- }
- func testCopyKingfisherStateShouldKeepEmbeddedGIFDataForDiskCache() {
- struct TestProcessor: ImageProcessor {
- let identifier: String = "com.onevcat.KingfisherTests.TestProcessor.CopyState"
- func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {
- switch item {
- case .image(let image):
- #if os(macOS)
- guard let cgImage = image.kf.cgImage else { return image }
- let newImage = KFCrossPlatformImage(cgImage: cgImage, size: image.kf.size)
- image.kf.copyKingfisherState(to: newImage)
- return newImage
- #else
- guard let cgImage = image.cgImage else { return image }
- let newImage = KFCrossPlatformImage(cgImage: cgImage, scale: image.scale, orientation: image.imageOrientation)
- image.kf.copyKingfisherState(to: newImage)
- return newImage
- #endif
- case .data(let data):
- return DefaultImageProcessor.default.process(item: .data(data), options: options)
- }
- }
- }
- let exp = expectation(description: #function)
- let image = KingfisherWrapper<KFCrossPlatformImage>.animatedImage(data: testImageGIFData, options: .init())!
- XCTAssertEqual(image.kf.gifRepresentation()?.kf.imageFormat, .GIF)
- let options = KingfisherParsedOptionsInfo([.processor(TestProcessor())])
- let key = "test-gif-copy-state"
- cache.store(image, original: nil, forKey: key, options: options, toDisk: true) { _ in
- do {
- let storedKey = key.computedKey(with: TestProcessor().identifier)
- let storedData = try self.cache.diskStorage.value(forKey: storedKey)
- XCTAssertEqual(storedData?.kf.imageFormat, .GIF)
- } catch {
- XCTFail("Unexpected error: \(error)")
- }
- exp.fulfill()
- }
- waitForExpectations(timeout: 3, handler: nil)
- }
-
- func testStoreMultipleImages() {
- let exp = expectation(description: #function)
- storeMultipleImages {
- let diskCachePath = self.cache.diskStorage.directoryURL.path
- var files: [String] = []
- do {
- files = try FileManager.default.contentsOfDirectory(atPath: diskCachePath)
- } catch _ {
- XCTFail()
- }
- XCTAssertEqual(files.count, testKeys.count)
- exp.fulfill()
- }
- waitForExpectations(timeout: 3, handler: nil)
- }
-
- func testStoreMultipleImagesAsync() async throws {
- await storeMultipleImages()
-
- let diskCachePath = cache.diskStorage.directoryURL.path
- let files = try FileManager.default.contentsOfDirectory(atPath: diskCachePath)
- XCTAssertEqual(files.count, testKeys.count)
- }
-
- func testCachedFileExists() {
- let exp = expectation(description: #function)
- let key = testKeys[0]
- let url = URL(string: key)!
-
- let exists = cache.imageCachedType(forKey: url.cacheKey).cached
- XCTAssertFalse(exists)
-
- cache.retrieveImage(forKey: key) { result in
- switch result {
- case .success(let value):
- XCTAssertNil(value.image)
- XCTAssertEqual(value.cacheType, .none)
- case .failure:
- XCTFail()
- return
- }
- self.cache.store(testImage, forKey: key, toDisk: true) { _ in
- self.cache.retrieveImage(forKey: key) { result in
- XCTAssertNotNil(result.value?.image)
- XCTAssertEqual(result.value?.cacheType, .memory)
- self.cache.clearMemoryCache()
- self.cache.retrieveImage(forKey: key) { result in
- XCTAssertNotNil(result.value?.image)
- XCTAssertEqual(result.value?.cacheType, .disk)
- exp.fulfill()
- }
- }
- }
- }
-
- waitForExpectations(timeout: 3, handler: nil)
- }
-
- func testCachedFileExistsAsync() async throws {
- let key = testKeys[0]
- let url = URL(string: key)!
-
- let exists = cache.imageCachedType(forKey: url.cacheKey).cached
- XCTAssertFalse(exists)
-
- var result = try await cache.retrieveImage(forKey: key)
- XCTAssertNil(result.image)
- XCTAssertEqual(result.cacheType, .none)
-
- try await cache.store(testImage, forKey: key, toDisk: true)
-
- result = try await cache.retrieveImage(forKey: key)
- XCTAssertNotNil(result.image)
- XCTAssertEqual(result.cacheType, .memory)
-
- cache.clearMemoryCache()
-
- result = try await cache.retrieveImage(forKey: key)
- XCTAssertNotNil(result.image)
- XCTAssertEqual(result.cacheType, .disk)
- }
- func testCachedFileWithCustomPathExtensionExists() {
- cache.diskStorage.config.pathExtension = "jpg"
- let exp = expectation(description: #function)
-
- let key = testKeys[0]
- let url = URL(string: key)!
- cache.store(testImage, forKey: key, toDisk: true) { _ in
- let cachePath = self.cache.cachePath(forKey: url.cacheKey)
- XCTAssertTrue(cachePath.hasSuffix(".jpg"))
- exp.fulfill()
- }
-
- waitForExpectations(timeout: 3, handler: nil)
- }
- func testCachedFileWithCustomPathExtensionExistsAsync() async throws {
- cache.diskStorage.config.pathExtension = "jpg"
- let key = testKeys[0]
- let url = URL(string: key)!
- try await cache.store(testImage, forKey: key, toDisk: true)
- let cachePath = self.cache.cachePath(forKey: url.cacheKey)
- XCTAssertTrue(cachePath.hasSuffix(".jpg"))
- }
-
- @MainActor func testCachedImageIsFetchedSynchronouslyFromTheMemoryCache() {
- cache.store(testImage, forKey: testKeys[0], toDisk: false)
- var image: KFCrossPlatformImage? = nil
- cache.retrieveImage(forKey: testKeys[0]) { result in
- MainActor.assumeIsolated {
- image = try? result.get().image
- }
- }
- XCTAssertEqual(testImage, image)
- }
-
- func testCachedImageIsFetchedSynchronouslyFromTheMemoryCacheAsync() async throws {
- try await cache.store(testImage, forKey: testKeys[0], toDisk: false)
- let result = try await cache.retrieveImage(forKey: testKeys[0])
- XCTAssertEqual(testImage, result.image)
- }
- func testIsImageCachedForKey() {
- let exp = expectation(description: #function)
- let key = testKeys[0]
- XCTAssertFalse(cache.imageCachedType(forKey: key).cached)
- cache.store(testImage, original: testImageData, forKey: key, toDisk: true) { _ in
- XCTAssertTrue(self.cache.imageCachedType(forKey: key).cached)
- exp.fulfill()
- }
- waitForExpectations(timeout: 3, handler: nil)
- }
-
- func testIsImageCachedForKeyAsync() async throws {
- let key = testKeys[0]
- XCTAssertFalse(cache.imageCachedType(forKey: key).cached)
- try await cache.store(testImage, original: testImageData, forKey: key, toDisk: true)
- XCTAssertTrue(cache.imageCachedType(forKey: key).cached)
- }
-
- func testCleanDiskCacheNotification() {
- let exp = expectation(description: #function)
- let key = testKeys[0]
- cache.diskStorage.config.expiration = .seconds(0.1)
- let selfCache = self.cache
- cache.store(testImage, original: testImageData, forKey: key, toDisk: true) { _ in
- self.observer = NotificationCenter.default.addObserver(
- forName: .KingfisherDidCleanDiskCache,
- object: self.cache,
- queue: .main
- ) { noti in
- let receivedCache = noti.object as? ImageCache
- XCTAssertNotNil(receivedCache)
- XCTAssertTrue(receivedCache === selfCache)
-
- guard let hashes = noti.userInfo?[KingfisherDiskCacheCleanedHashKey] as? [String] else {
- XCTFail("Notification should contains Strings in key 'KingfisherDiskCacheCleanedHashKey'")
- exp.fulfill()
- return
- }
-
- XCTAssertEqual(hashes.count, 1)
- XCTAssertEqual(hashes.first!, selfCache!.hash(forKey: key))
- exp.fulfill()
- }
- delay(2) { // File writing in disk cache has an approximate (round) creating time. 1 second is not enough.
- self.cache.cleanExpiredDiskCache()
- }
- }
- waitForExpectations(timeout: 5, handler: nil)
- }
-
- func testCannotRetrieveCacheWithProcessorIdentifier() {
- let exp = expectation(description: #function)
- let key = testKeys[0]
- let p = RoundCornerImageProcessor(cornerRadius: 40)
- cache.store(testImage, original: testImageData, forKey: key, toDisk: true) { _ in
- self.cache.retrieveImage(forKey: key, options: [.processor(p)]) { result in
- XCTAssertNotNil(result.value)
- XCTAssertNil(result.value!.image)
- exp.fulfill()
- }
- }
- waitForExpectations(timeout: 3, handler: nil)
- }
-
- func testCannotRetrieveCacheWithProcessorIdentifierAsync() async throws {
- let key = testKeys[0]
- let p = RoundCornerImageProcessor(cornerRadius: 40)
- try await cache.store(testImage, original: testImageData, forKey: key, toDisk: true)
- let result = try await cache.retrieveImage(forKey: key, options: [.processor(p)])
- XCTAssertNotNil(result)
- XCTAssertNil(result.image)
- }
- func testRetrieveCacheWithProcessorIdentifier() {
- let exp = expectation(description: #function)
- let key = testKeys[0]
- let p = RoundCornerImageProcessor(cornerRadius: 40)
- cache.store(
- testImage,
- original: testImageData,
- forKey: key,
- processorIdentifier: p.identifier,
- toDisk: true)
- {
- _ in
- self.cache.retrieveImage(forKey: key, options: [.processor(p)]) { result in
- XCTAssertNotNil(result.value?.image)
- exp.fulfill()
- }
- }
- waitForExpectations(timeout: 3, handler: nil)
- }
-
- func testRetrieveCacheWithProcessorIdentifierAsync() async throws {
- let key = testKeys[0]
- let p = RoundCornerImageProcessor(cornerRadius: 40)
- try await cache.store(
- testImage,
- original: testImageData,
- forKey: key,
- processorIdentifier: p.identifier,
- toDisk: true
- )
- let result = try await cache.retrieveImage(forKey: key, options: [.processor(p)])
- XCTAssertNotNil(result.image)
- }
- func testDefaultCache() {
- let exp = expectation(description: #function)
- let key = testKeys[0]
- let cache = ImageCache.default
- cache.store(testImage, forKey: key) { _ in
- XCTAssertTrue(cache.memoryStorage.isCached(forKey: key))
- XCTAssertTrue(cache.diskStorage.isCached(forKey: key))
- cleanDefaultCache()
- exp.fulfill()
- }
- waitForExpectations(timeout: 3, handler: nil)
- }
-
- func testDefaultCacheAsync() async throws {
- let key = testKeys[0]
- let cache = ImageCache.default
- try await cache.store(testImage, forKey: key)
- XCTAssertTrue(cache.memoryStorage.isCached(forKey: key))
- XCTAssertTrue(cache.diskStorage.isCached(forKey: key))
- cleanDefaultCache()
- }
-
- func testRetrieveDiskCacheSynchronously() {
- let exp = expectation(description: #function)
- let key = testKeys[0]
- cache.store(testImage, forKey: key, toDisk: true) { _ in
- var cacheType = self.cache.imageCachedType(forKey: key)
- XCTAssertEqual(cacheType, .memory)
-
- self.cache.memoryStorage.remove(forKey: key)
- cacheType = self.cache.imageCachedType(forKey: key)
- XCTAssertEqual(cacheType, .disk)
-
- let dispatched = LockIsolated(false)
- self.cache.retrieveImageInDiskCache(forKey: key, options: [.loadDiskFileSynchronously]) {
- result in
- XCTAssertFalse(dispatched.value)
- exp.fulfill()
- }
- // This should be called after the completion handler above.
- dispatched.setValue(true)
- }
- waitForExpectations(timeout: 3, handler: nil)
- }
-
- func testRetrieveDiskCacheAsynchronously() {
- let exp = expectation(description: #function)
- let key = testKeys[0]
- cache.store(testImage, forKey: key, toDisk: true) { _ in
- var cacheType = self.cache.imageCachedType(forKey: key)
- XCTAssertEqual(cacheType, .memory)
-
- self.cache.memoryStorage.remove(forKey: key)
- cacheType = self.cache.imageCachedType(forKey: key)
- XCTAssertEqual(cacheType, .disk)
-
- let dispatched = LockIsolated(false)
- self.cache.retrieveImageInDiskCache(forKey: key, options: nil) {
- result in
- XCTAssertTrue(dispatched.value)
- exp.fulfill()
- }
- // This should be called before the completion handler above.
- dispatched.setValue(true)
- }
- waitForExpectations(timeout: 3, handler: nil)
- }
- #if os(iOS) || os(tvOS) || os(watchOS) || os(visionOS)
- func testModifierShouldOnlyApplyForFinalResultWhenMemoryLoad() {
- let exp = expectation(description: #function)
- let key = testKeys[0]
-
- let modifierCalled = LockIsolated(false)
- let modifier = AnyImageModifier { image in
- modifierCalled.setValue(true)
- return image.withRenderingMode(.alwaysTemplate)
- }
-
- cache.store(testImage, original: testImageData, forKey: key) { _ in
- self.cache.retrieveImage(forKey: key, options: [.imageModifier(modifier)]) { result in
- XCTAssertEqual(result.value?.image?.renderingMode, .automatic)
- XCTAssertFalse(modifierCalled.value)
- exp.fulfill()
- }
- }
- waitForExpectations(timeout: 3, handler: nil)
- }
-
- func testModifierShouldOnlyApplyForFinalResultWhenMemoryLoadAsync() async throws {
- let key = testKeys[0]
- let modifierCalled = LockIsolated(false)
- let modifier = AnyImageModifier { image in
- modifierCalled.setValue(true)
- return image.withRenderingMode(.alwaysTemplate)
- }
- try await cache.store(testImage, original: testImageData, forKey: key)
- let result = try await cache.retrieveImage(forKey: key, options: [.imageModifier(modifier)])
- XCTAssertFalse(modifierCalled.value)
- XCTAssertEqual(result.image?.renderingMode, .automatic)
- }
- func testModifierShouldOnlyApplyForFinalResultWhenDiskLoad() {
- let exp = expectation(description: #function)
- let key = testKeys[0]
- let modifierCalled = LockIsolated(false)
- let modifier = AnyImageModifier { image in
- modifierCalled.setValue(true)
- return image.withRenderingMode(.alwaysTemplate)
- }
- cache.store(testImage, original: testImageData, forKey: key) { _ in
- self.cache.clearMemoryCache()
- self.cache.retrieveImage(forKey: key, options: [.imageModifier(modifier)]) { result in
- XCTAssertEqual(result.value?.image?.renderingMode, .automatic)
- XCTAssertFalse(modifierCalled.value)
- exp.fulfill()
- }
- }
- waitForExpectations(timeout: 3, handler: nil)
- }
-
- func testModifierShouldOnlyApplyForFinalResultWhenDiskLoadAsync() async throws {
- let key = testKeys[0]
- let modifierCalled = LockIsolated(false)
- let modifier = AnyImageModifier { image in
- modifierCalled.setValue(true)
- return image.withRenderingMode(.alwaysTemplate)
- }
-
- try await cache.store(testImage, original: testImageData, forKey: key)
- cache.clearMemoryCache()
- let result = try await cache.retrieveImage(forKey: key, options: [.imageModifier(modifier)])
- XCTAssertFalse(modifierCalled.value)
- // The renderingMode is expected to be the default value `.automatic`. The image modifier should only apply to
- // the image manager result.
- XCTAssertEqual(result.image?.renderingMode, .automatic)
- }
- #endif
-
- func testStoreToMemoryWithExpiration() {
- let exp = expectation(description: #function)
- let key = testKeys[0]
- cache.store(
- testImage,
- original: testImageData,
- forKey: key,
- options: KingfisherParsedOptionsInfo([.memoryCacheExpiration(.seconds(0.5))]),
- toDisk: true)
- {
- _ in
- XCTAssertEqual(self.cache.imageCachedType(forKey: key), .memory)
- delay(1) {
- XCTAssertEqual(self.cache.imageCachedType(forKey: key), .disk)
- exp.fulfill()
- }
- }
- waitForExpectations(timeout: 5, handler: nil)
- }
-
- func testStoreToMemoryWithExpirationAsync() async throws {
- let key = testKeys[0]
- try await cache.store(
- testImage,
- original: testImageData,
- forKey: key,
- options: KingfisherParsedOptionsInfo([.memoryCacheExpiration(.seconds(0.5))]),
- toDisk: true
- )
- XCTAssertEqual(self.cache.imageCachedType(forKey: key), .memory)
- // After 1 sec, the cache only remains on disk.
- try await Task.sleep(nanoseconds: NSEC_PER_SEC)
- XCTAssertEqual(self.cache.imageCachedType(forKey: key), .disk)
- }
-
- func testStoreToDiskWithExpiration() {
- let exp = expectation(description: #function)
- let key = testKeys[0]
- cache.store(
- testImage,
- original: testImageData,
- forKey: key,
- options: KingfisherParsedOptionsInfo([.diskCacheExpiration(.expired)]),
- toDisk: true)
- {
- _ in
- XCTAssertEqual(self.cache.imageCachedType(forKey: key), .memory)
- self.cache.clearMemoryCache()
- XCTAssertEqual(self.cache.imageCachedType(forKey: key), .none)
- exp.fulfill()
- }
- waitForExpectations(timeout: 3, handler: nil)
- }
-
- func testStoreToDiskWithExpirationAsync() async throws {
- let key = testKeys[0]
- try await cache.store(
- testImage,
- original: testImageData,
- forKey: key,
- options: KingfisherParsedOptionsInfo([.diskCacheExpiration(.expired)]),
- toDisk: true
- )
-
- XCTAssertEqual(self.cache.imageCachedType(forKey: key), .memory)
- self.cache.clearMemoryCache()
- XCTAssertEqual(self.cache.imageCachedType(forKey: key), .none)
- }
- func testCalculateDiskStorageSize() {
- let exp = expectation(description: #function)
- cache.calculateDiskStorageSize { result in
- switch result {
- case .success(let size):
- XCTAssertEqual(size, 0)
- self.storeMultipleImages {
- self.cache.calculateDiskStorageSize { result in
- switch result {
- case .success(let size):
- XCTAssertEqual(size, UInt(testImagePNGData.count * testKeys.count))
- case .failure:
- XCTAssert(false)
- }
- exp.fulfill()
- }
- }
- case .failure:
- XCTAssert(false)
- exp.fulfill()
- }
- }
- waitForExpectations(timeout: 3, handler: nil)
- }
-
- func testDiskCacheStillWorkWhenFolderDeletedExternally() {
- let exp = expectation(description: #function)
- let key = testKeys[0]
- let url = URL(string: key)!
-
- let exists = cache.imageCachedType(forKey: url.cacheKey)
- XCTAssertEqual(exists, .none)
-
- cache.store(testImage, forKey: key, toDisk: true) { _ in
- self.cache.retrieveImage(forKey: key) { result in
- XCTAssertNotNil(result.value?.image)
- XCTAssertEqual(result.value?.cacheType, .memory)
- self.cache.clearMemoryCache()
- self.cache.retrieveImage(forKey: key) { result in
- XCTAssertNotNil(result.value?.image)
- XCTAssertEqual(result.value?.cacheType, .disk)
- self.cache.clearMemoryCache()
-
- try! FileManager.default.removeItem(at: self.cache.diskStorage.directoryURL)
-
- let exists = self.cache.imageCachedType(forKey: url.cacheKey)
- XCTAssertEqual(exists, .none)
-
- self.cache.store(testImage, forKey: key, toDisk: true) { _ in
- self.cache.clearMemoryCache()
- let cacheType = self.cache.imageCachedType(forKey: url.cacheKey)
- XCTAssertEqual(cacheType, .disk)
- exp.fulfill()
- }
- }
- }
- }
-
- waitForExpectations(timeout: 3, handler: nil)
- }
-
- func testDiskCacheCalculateSizeWhenFolderDeletedExternally() {
- let exp = expectation(description: #function)
-
- let key = testKeys[0]
-
- cache.calculateDiskStorageSize { result in
- XCTAssertEqual(result.value, 0)
-
- self.cache.store(testImage, forKey: key, toDisk: true) { _ in
- self.cache.calculateDiskStorageSize { result in
- XCTAssertEqual(result.value, UInt(testImagePNGData.count))
-
- try! FileManager.default.removeItem(at: self.cache.diskStorage.directoryURL)
- self.cache.calculateDiskStorageSize { result in
- XCTAssertEqual(result.value, 0)
- exp.fulfill()
- }
-
- }
- }
- }
- waitForExpectations(timeout: 3, handler: nil)
- }
- func testCalculateDiskStorageSizeAsync() async throws {
- let size = try await cache.diskStorageSize
- XCTAssertEqual(size, 0)
- await storeMultipleImages()
- let newSize = try await cache.diskStorageSize
- XCTAssertEqual(newSize, UInt(testImagePNGData.count * testKeys.count))
- }
-
- func testStoreFileWithForcedExtension() async throws {
- let key = testKeys[0]
- try await cache.store(testImage, forKey: key, forcedExtension: "jpg", toDisk: true)
-
- let pathWithoutExtension = cache.cachePath(forKey: key)
- XCTAssertFalse(FileManager.default.fileExists(atPath: pathWithoutExtension))
-
- let pathWithExtension = cache.cachePath(forKey: key, forcedExtension: "jpg")
- XCTAssertTrue(FileManager.default.fileExists(atPath: pathWithExtension))
-
- XCTAssertEqual(cache.imageCachedType(forKey: key), .memory)
- XCTAssertEqual(cache.imageCachedType(forKey: key, forcedExtension: "jpg"), .memory)
-
- cache.clearMemoryCache()
- XCTAssertEqual(cache.imageCachedType(forKey: key), .none)
- XCTAssertEqual(cache.imageCachedType(forKey: key, forcedExtension: "jpg"), .disk)
- }
-
- func testPossibleCacheFileURLIfOnDiskNotCached() {
- let url = URL(string: "https://example.com/photo")!
- let resource = LivePhotoResource(downloadURL: url)
-
- let fileURL = cache.possibleCacheFileURLIfOnDisk(
- forKey: resource.cacheKey,
- processorIdentifier: LivePhotoImageProcessor.default.identifier,
- referenceFileType: .heic
- )
-
- // Not cached
- XCTAssertNil(fileURL)
- }
-
- func testPossibleCacheFileURLIfOnDiskCachedWithWrongFileType() async throws {
- let url = URL(string: "https://example.com/photo")!
- let resource = LivePhotoResource(downloadURL: url, fileType: .heic)
-
- // Cache without a file type extension
- try await cache.storeToDisk(
- testImageData,
- forKey: resource.cacheKey,
- processorIdentifier: LivePhotoImageProcessor.default.identifier
- )
-
- let fileURL = cache.possibleCacheFileURLIfOnDisk(
- forKey: resource.cacheKey,
- processorIdentifier: LivePhotoImageProcessor.default.identifier,
- referenceFileType: .heic
- )
-
- // Not cached
- XCTAssertNil(fileURL)
- }
-
- func testPossibleCacheFileURLIfOnDiskCachedWithExplicitFileType() async throws {
- let url = URL(string: "https://example.com/photo")!
- let resource = LivePhotoResource(downloadURL: url, fileType: .heic)
-
- // Cache without a file type extension
- try await cache.storeToDisk(
- testImageData,
- forKey: resource.cacheKey,
- processorIdentifier: LivePhotoImageProcessor.default.identifier,
- forcedExtension: "heic"
- )
-
- let fileURL = cache.possibleCacheFileURLIfOnDisk(
- forKey: resource.cacheKey,
- processorIdentifier: LivePhotoImageProcessor.default.identifier,
- referenceFileType: .heic
- )
- let result = try XCTUnwrap(fileURL)
- XCTAssertTrue(result.absoluteString.hasSuffix(".heic"))
- }
-
- func testPossibleCacheFileURLIfOnDiskCachedGuessingFileTypeNotHit() async throws {
- let url = URL(string: "https://example.com/photo")!
- let resource = LivePhotoResource(downloadURL: url, fileType: .heic)
- let fileURL = cache.possibleCacheFileURLIfOnDisk(
- forKey: resource.cacheKey,
- processorIdentifier: LivePhotoImageProcessor.default.identifier,
- referenceFileType: .other("")
- )
- XCTAssertNil(fileURL)
- }
-
- func testPossibleCacheFileURLIfOnDiskCachedGuessingFileType() async throws {
- let url = URL(string: "https://example.com/photo")!
- let resource = LivePhotoResource(downloadURL: url, fileType: .heic)
-
- // Cache without a file type extension
- try await cache.storeToDisk(
- testImageData,
- forKey: resource.cacheKey,
- processorIdentifier: LivePhotoImageProcessor.default.identifier,
- forcedExtension: "heic"
- )
-
- let fileURL = cache.possibleCacheFileURLIfOnDisk(
- forKey: resource.cacheKey,
- processorIdentifier: LivePhotoImageProcessor.default.identifier,
- referenceFileType: .other("")
- )
- let result = try XCTUnwrap(fileURL)
- XCTAssertTrue(result.absoluteString.hasSuffix(".heic"))
- }
-
- func testPossibleCacheFileURLIfOnDiskCachedArbitraryFileType() async throws {
- let url = URL(string: "https://example.com/photo")!
- let resource = LivePhotoResource(downloadURL: url, fileType: .heic)
-
- // Cache without a file type extension
- try await cache.storeToDisk(
- testImageData,
- forKey: resource.cacheKey,
- processorIdentifier: LivePhotoImageProcessor.default.identifier,
- forcedExtension: "myExt"
- )
-
- let fileURL = cache.possibleCacheFileURLIfOnDisk(
- forKey: resource.cacheKey,
- processorIdentifier: LivePhotoImageProcessor.default.identifier,
- referenceFileType: .other("myExt")
- )
- let result = try XCTUnwrap(fileURL)
- XCTAssertTrue(result.absoluteString.hasSuffix(".myExt"))
- }
- #if !os(macOS) && !os(watchOS)
- func testKingfisherWrapperUIApplicationSharedReturnsNilInUnitTest() {
- // UIApplication.shared is not available in some Unit Tests contexts.
- // This tests that accessing it via KingfisherWrapper does not cause a crash.
- XCTAssertNil(KingfisherWrapper<UIApplication>.shared)
- }
- #endif
- // MARK: - Helper
- private func storeMultipleImages(_ completionHandler: @escaping () -> Void) {
- let group = DispatchGroup()
- testKeys.forEach {
- group.enter()
- cache.store(testImage, original: testImageData, forKey: $0, toDisk: true) { _ in
- group.leave()
- }
- }
- group.notify(queue: .main, execute: completionHandler)
- }
-
- private func storeMultipleImages() async {
- await withCheckedContinuation {
- storeMultipleImages($0.resume)
- }
- }
- }
- @dynamicMemberLookup
- public final class LockIsolated<Value>: @unchecked Sendable {
- private var _value: Value
- private let lock = NSRecursiveLock()
- /// Initializes lock-isolated state around a value.
- ///
- /// - Parameter value: A value to isolate with a lock.
- public init(_ value: @autoclosure @Sendable () throws -> Value) rethrows {
- self._value = try value()
- }
- public subscript<Subject: Sendable>(dynamicMember keyPath: KeyPath<Value, Subject>) -> Subject {
- self.lock.sync {
- self._value[keyPath: keyPath]
- }
- }
- /// Perform an operation with isolated access to the underlying value.
- ///
- /// Useful for modifying a value in a single transaction.
- ///
- /// ```swift
- /// // Isolate an integer for concurrent read/write access:
- /// var count = LockIsolated(0)
- ///
- /// func increment() {
- /// // Safely increment it:
- /// self.count.withValue { $0 += 1 }
- /// }
- /// ```
- ///
- /// - Parameter operation: An operation to be performed on the the underlying value with a lock.
- /// - Returns: The result of the operation.
- public func withValue<T: Sendable>(
- _ operation: @Sendable (inout Value) throws -> T
- ) rethrows -> T {
- try self.lock.sync {
- var value = self._value
- defer { self._value = value }
- return try operation(&value)
- }
- }
- /// Overwrite the isolated value with a new value.
- ///
- /// ```swift
- /// // Isolate an integer for concurrent read/write access:
- /// var count = LockIsolated(0)
- ///
- /// func reset() {
- /// // Reset it:
- /// self.count.setValue(0)
- /// }
- /// ```
- ///
- /// > Tip: Use ``withValue(_:)`` instead of ``setValue(_:)`` if the value being set is derived
- /// > from the current value. That is, do this:
- /// >
- /// > ```swift
- /// > self.count.withValue { $0 += 1 }
- /// > ```
- /// >
- /// > ...and not this:
- /// >
- /// > ```swift
- /// > self.count.setValue(self.count + 1)
- /// > ```
- /// >
- /// > ``withValue(_:)`` isolates the entire transaction and avoids data races between reading and
- /// > writing the value.
- ///
- /// - Parameter newValue: The value to replace the current isolated value with.
- public func setValue(_ newValue: @autoclosure @Sendable () throws -> Value) rethrows {
- try self.lock.sync {
- self._value = try newValue()
- }
- }
- }
- extension LockIsolated where Value: Sendable {
- /// The lock-isolated value.
- public var value: Value {
- self.lock.sync {
- self._value
- }
- }
- }
- extension NSRecursiveLock {
- @inlinable @discardableResult
- @_spi(Internals) public func sync<R>(work: () throws -> R) rethrows -> R {
- self.lock()
- defer { self.unlock() }
- return try work()
- }
- }
|