Files
photochk/Tests/PhotochkTests/GroupingTests.swift
T

167 lines
6.9 KiB
Swift

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)
}
}