| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012 |
- //
- // 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
- 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 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.01)
- 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 === self.cache)
-
- 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!, self.cache.hash(forKey: key))
- guard let o = self.observer else { return }
- NotificationCenter.default.removeObserver(o)
- exp.fulfill()
- }
- delay(1) {
- 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 = ActorBox(false)
- let modifier = AnyImageModifier { image in
- Task {
- await 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)
- Task {
- let called = await modifierCalled.value
- XCTAssertFalse(called)
- exp.fulfill()
-
- }
- }
- }
- waitForExpectations(timeout: 3, handler: nil)
- }
-
- func testModifierShouldOnlyApplyForFinalResultWhenMemoryLoadAsync() async throws {
- let key = testKeys[0]
- let modifierCalled = ActorBox(false)
- let modifier = AnyImageModifier { image in
- Task {
- await 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)])
- let called = await modifierCalled.value
- XCTAssertFalse(called)
- XCTAssertEqual(result.image?.renderingMode, .automatic)
- }
- func testModifierShouldOnlyApplyForFinalResultWhenDiskLoad() {
- let exp = expectation(description: #function)
- let key = testKeys[0]
- let modifierCalled = ActorBox(false)
- let modifier = AnyImageModifier { image in
- Task {
- await 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)
- Task {
- let called = await modifierCalled.value
- XCTAssertFalse(called)
- exp.fulfill()
- }
- }
- }
- waitForExpectations(timeout: 3, handler: nil)
- }
-
- func testModifierShouldOnlyApplyForFinalResultWhenDiskLoadAsync() async throws {
- let key = testKeys[0]
- let modifierCalled = ActorBox(false)
- let modifier = AnyImageModifier { image in
- Task {
- await 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)])
- let called = await modifierCalled.value
- XCTAssertFalse(called)
- // 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.2))]),
- 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.2))]),
- 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"))
- }
-
- // 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()
- }
- }
|