Add end-to-end grouping tests
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
import Foundation
|
||||
import Testing
|
||||
|
||||
@testable import Photochk
|
||||
|
||||
/// 走査からグループ生成まで、実際に画像ファイルを作って一気通貫で確かめる。
|
||||
@Suite("グループ生成")
|
||||
struct GroupingTests {
|
||||
/// 同じ絵の派生(完全コピー・縮小・低画質)と、無関係な絵を1つ置いたフォルダを作る。
|
||||
private func makeFixture() throws -> URL {
|
||||
let directory = try TestImages.makeTemporaryDirectory()
|
||||
let subdirectory = directory.appendingPathComponent("sub", isDirectory: true)
|
||||
try FileManager.default.createDirectory(at: subdirectory, withIntermediateDirectories: true)
|
||||
|
||||
let pattern = TestImages.pattern(seed: 42)
|
||||
let base = directory.appendingPathComponent("base.jpg")
|
||||
try TestImages.writeJPEG(pattern, to: base, quality: 0.95)
|
||||
|
||||
// バイト単位で同じコピー
|
||||
try FileManager.default.copyItem(
|
||||
at: base, to: directory.appendingPathComponent("base_copy.jpg")
|
||||
)
|
||||
// 縮小版と、そのバイト単位のコピー(代表以外にできる完全同一の組)
|
||||
let small = directory.appendingPathComponent("base_small.jpg")
|
||||
try TestImages.writeJPEG(
|
||||
TestImages.scaled(pattern, factor: 0.5), to: small, quality: 0.9
|
||||
)
|
||||
try FileManager.default.copyItem(
|
||||
at: small, to: directory.appendingPathComponent("base_small_copy.jpg")
|
||||
)
|
||||
// 低画質版
|
||||
try TestImages.writeJPEG(
|
||||
pattern, to: directory.appendingPathComponent("base_low.jpg"), quality: 0.3
|
||||
)
|
||||
// サブフォルダにもう1つ派生を置く
|
||||
try TestImages.writeJPEG(
|
||||
TestImages.scaled(pattern, factor: 0.75),
|
||||
to: subdirectory.appendingPathComponent("base_sub.jpg"),
|
||||
quality: 0.85
|
||||
)
|
||||
// 無関係な絵
|
||||
try TestImages.writeJPEG(
|
||||
TestImages.pattern(seed: 9_999),
|
||||
to: directory.appendingPathComponent("other.jpg"),
|
||||
quality: 0.95
|
||||
)
|
||||
|
||||
return directory
|
||||
}
|
||||
|
||||
@Test("派生画像が1グループにまとまり、無関係な画像は入らない")
|
||||
func groupsDerivatives() async throws {
|
||||
let directory = try makeFixture()
|
||||
defer { try? FileManager.default.removeItem(at: directory) }
|
||||
|
||||
let groups = try await SimilarityGrouper.run(
|
||||
root: directory,
|
||||
settings: ScanSettings(includesSubfolders: true, sensitivity: .standard),
|
||||
cache: FingerprintCache(fileURL: nil),
|
||||
progress: { _ in }
|
||||
)
|
||||
|
||||
#expect(groups.count == 1)
|
||||
let group = try #require(groups.first)
|
||||
|
||||
let names = Set(group.members.map(\.asset.fileName))
|
||||
#expect(
|
||||
names == [
|
||||
"base.jpg", "base_copy.jpg", "base_small.jpg", "base_small_copy.jpg",
|
||||
"base_low.jpg", "base_sub.jpg",
|
||||
]
|
||||
)
|
||||
#expect(!names.contains("other.jpg"))
|
||||
}
|
||||
|
||||
@Test("代表以外にできた完全同一の組には同じ印が付く")
|
||||
func tagsExactSetsAwayFromRepresentative() async throws {
|
||||
let directory = try makeFixture()
|
||||
defer { try? FileManager.default.removeItem(at: directory) }
|
||||
|
||||
let groups = try await SimilarityGrouper.run(
|
||||
root: directory,
|
||||
settings: ScanSettings(includesSubfolders: true, sensitivity: .standard),
|
||||
cache: FingerprintCache(fileURL: nil),
|
||||
progress: { _ in }
|
||||
)
|
||||
let group = try #require(groups.first)
|
||||
let tags = Dictionary(
|
||||
uniqueKeysWithValues: group.members.map { ($0.asset.fileName, $0.exactSetTag) }
|
||||
)
|
||||
|
||||
// 縮小版の2枚は互いにバイト単位で同じなので、同じ印が付く
|
||||
let smallTag = try #require(tags["base_small.jpg"] ?? nil)
|
||||
#expect(tags["base_small_copy.jpg"] == smallTag)
|
||||
|
||||
// 代表と完全同一のものは relation が exact になるので印は要らない
|
||||
#expect(tags["base_copy.jpg"] == .some(nil))
|
||||
#expect(tags["base_low.jpg"] == .some(nil))
|
||||
}
|
||||
|
||||
@Test("完全同一のコピーは exact、加工された派生は similar になる")
|
||||
func relationKinds() async throws {
|
||||
let directory = try makeFixture()
|
||||
defer { try? FileManager.default.removeItem(at: directory) }
|
||||
|
||||
let groups = try await SimilarityGrouper.run(
|
||||
root: directory,
|
||||
settings: ScanSettings(includesSubfolders: true, sensitivity: .standard),
|
||||
cache: FingerprintCache(fileURL: nil),
|
||||
progress: { _ in }
|
||||
)
|
||||
let group = try #require(groups.first)
|
||||
|
||||
// 代表は元画像か、それとバイト単位で同じコピー
|
||||
#expect(["base.jpg", "base_copy.jpg"].contains(group.representative.fileName))
|
||||
#expect(group.representative.pixelWidth == TestImages.width)
|
||||
|
||||
let relations = Dictionary(
|
||||
uniqueKeysWithValues: group.members.map { ($0.asset.fileName, $0.relation) }
|
||||
)
|
||||
#expect(relations["base.jpg"]?.isExact == true)
|
||||
#expect(relations["base_copy.jpg"]?.isExact == true)
|
||||
#expect(relations["base_small.jpg"]?.isExact == false)
|
||||
#expect(relations["base_low.jpg"]?.isExact == false)
|
||||
|
||||
// 加工された派生には feature print 由来の距離が入っている
|
||||
if case .similar(let distance) = relations["base_small.jpg"] {
|
||||
let value = try #require(distance)
|
||||
#expect(value < Sensitivity.standard.featurePrintThreshold)
|
||||
} else {
|
||||
Issue.record("base_small.jpg は similar であるべき")
|
||||
}
|
||||
}
|
||||
|
||||
@Test("サブフォルダを含めないとサブフォルダの画像は対象外になる")
|
||||
func respectsRecursionSetting() async throws {
|
||||
let directory = try makeFixture()
|
||||
defer { try? FileManager.default.removeItem(at: directory) }
|
||||
|
||||
let groups = try await SimilarityGrouper.run(
|
||||
root: directory,
|
||||
settings: ScanSettings(includesSubfolders: false, sensitivity: .standard),
|
||||
cache: FingerprintCache(fileURL: nil),
|
||||
progress: { _ in }
|
||||
)
|
||||
let group = try #require(groups.first)
|
||||
#expect(!group.members.contains { $0.asset.fileName == "base_sub.jpg" })
|
||||
#expect(group.members.count == 5)
|
||||
}
|
||||
|
||||
@Test("削減できるバイト数は代表1枚を残した残り")
|
||||
func reclaimableBytes() async throws {
|
||||
let directory = try makeFixture()
|
||||
defer { try? FileManager.default.removeItem(at: directory) }
|
||||
|
||||
let groups = try await SimilarityGrouper.run(
|
||||
root: directory,
|
||||
settings: ScanSettings(includesSubfolders: true, sensitivity: .standard),
|
||||
cache: FingerprintCache(fileURL: nil),
|
||||
progress: { _ in }
|
||||
)
|
||||
let group = try #require(groups.first)
|
||||
#expect(group.reclaimableBytes == group.totalBytes - group.representative.fileSize)
|
||||
#expect(group.reclaimableBytes > 0)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user