import Foundation import Testing import Vision @testable import Photochk @Suite("キャッシュ") struct FingerprintCacheTests { private func makeFile(at directory: URL) throws -> ScannedFile { let url = directory.appendingPathComponent("cached.jpg") try TestImages.writeJPEG(TestImages.pattern(seed: 3), to: url, quality: 0.9) let values = try url.resourceValues(forKeys: [.fileSizeKey, .contentModificationDateKey]) return ScannedFile( url: url, fileSize: Int64(values.fileSize ?? 0), modificationDate: values.contentModificationDate ?? Date(), creationDate: nil ) } @Test("dHash と feature print がディスクを経由して復元できる") func roundTrip() async throws { let directory = try TestImages.makeTemporaryDirectory() defer { try? FileManager.default.removeItem(at: directory) } let file = try makeFile(at: directory) let cacheURL = directory.appendingPathComponent("cache.plist") let basics = try #require(DHash.compute(for: file.url)) let print = try await FeaturePrinter.compute(for: file.url) let writer = FingerprintCache(fileURL: cacheURL) await writer.store(basics: [file.url: basics], for: [file]) await writer.store(featurePrints: [file.url: print], for: [file]) await writer.save() let reader = FingerprintCache(fileURL: cacheURL) let restoredBasics = await reader.basics(for: [file]) let restoredPrints = await reader.featurePrints(for: [file]) #expect(restoredBasics[file.url]?.dHash == basics.dHash) let restoredPrint = try #require(restoredPrints[file.url]) // 同一の特徴量なので距離は 0 #expect(try restoredPrint.distance(to: print) == 0) } @Test("ファイルが更新されていたらキャッシュは使われない") func invalidatesOnChange() async throws { let directory = try TestImages.makeTemporaryDirectory() defer { try? FileManager.default.removeItem(at: directory) } let file = try makeFile(at: directory) let cache = FingerprintCache(fileURL: nil) let basics = try #require(DHash.compute(for: file.url)) await cache.store(basics: [file.url: basics], for: [file]) let changed = ScannedFile( url: file.url, fileSize: file.fileSize + 100, modificationDate: file.modificationDate, creationDate: nil ) #expect(await cache.basics(for: [changed]).isEmpty) #expect(await cache.basics(for: [file]).count == 1) } @Test("消えたファイルのエントリは prune で捨てられる") func prunesMissingFiles() async throws { let directory = try TestImages.makeTemporaryDirectory() defer { try? FileManager.default.removeItem(at: directory) } let file = try makeFile(at: directory) let cache = FingerprintCache(fileURL: nil) let basics = try #require(DHash.compute(for: file.url)) await cache.store(basics: [file.url: basics], for: [file]) await cache.prune(keeping: []) #expect(await cache.basics(for: [file]).isEmpty) } }