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