145 lines
6.3 KiB
Swift
145 lines
6.3 KiB
Swift
import XCTest
|
|
import Foundation
|
|
import ForgeColor
|
|
import ForgeGrade
|
|
import ForgeLibrary
|
|
@testable import ForgeExport
|
|
|
|
final class IngestMatchTests: XCTestCase {
|
|
|
|
var mediaDir: String!
|
|
var dbPath: String!
|
|
var store: LibraryStore!
|
|
var day: ForgeLibrary.Day!
|
|
|
|
override func setUpWithError() throws {
|
|
mediaDir = NSTemporaryDirectory() + "forge-ingest-\(UUID().uuidString)"
|
|
try FileManager.default.createDirectory(atPath: mediaDir + "/CLIPS", withIntermediateDirectories: true)
|
|
dbPath = NSTemporaryDirectory() + "forge-ingest-\(UUID().uuidString).db"
|
|
store = try LibraryStore(path: dbPath)
|
|
let p = try store.createProject(name: "P")
|
|
day = try store.createDay(projectID: p.id, label: "Day 03", date: "2026-07-11")
|
|
}
|
|
|
|
override func tearDown() {
|
|
try? FileManager.default.removeItem(atPath: mediaDir)
|
|
try? FileManager.default.removeItem(atPath: dbPath)
|
|
}
|
|
|
|
func makeShot(clip: String, withGrade: Bool = true) throws {
|
|
var snapshot: Data?
|
|
if withGrade {
|
|
let stack = GradeStack(nodes: [GradeNode(kind: .cdl(CDL(
|
|
slope: SIMD3(1.2, 1.0, 0.9), offset: SIMD3(0.01, 0, 0),
|
|
power: .one, saturation: 1.1)))])
|
|
snapshot = try GradeSnapshot.encode(stack)
|
|
}
|
|
_ = try store.createShot(Shot(
|
|
dayID: day.id, clipName: clip, cameraSlot: "A-Cam",
|
|
timecodeIn: "10:00:00:00", timecodeOut: "10:01:00:00",
|
|
gradeSnapshot: snapshot))
|
|
}
|
|
|
|
func makeMedia(_ name: String) throws {
|
|
try Data("fake clip".utf8).write(to: URL(fileURLWithPath: mediaDir + "/CLIPS/\(name)"))
|
|
}
|
|
|
|
// Clip file matches shot by basename -> sidecar .cdl + .cube written next to media.
|
|
func testMatchWritesSidecars() throws {
|
|
try makeShot(clip: "R001C001")
|
|
try makeMedia("R001C001.mov")
|
|
|
|
let result = try IngestMatcher.match(
|
|
mediaRoot: mediaDir, store: store, dayID: day.id,
|
|
options: IngestMatcher.Options(writeCDL: true, writeCube: true, latticeSize: 17))
|
|
|
|
XCTAssertEqual(result.matched.count, 1)
|
|
XCTAssertEqual(result.matched[0].clipName, "R001C001")
|
|
XCTAssertTrue(FileManager.default.fileExists(atPath: mediaDir + "/CLIPS/R001C001.cdl"))
|
|
XCTAssertTrue(FileManager.default.fileExists(atPath: mediaDir + "/CLIPS/R001C001.cube"))
|
|
|
|
// CDL sidecar parses back to shot's grade.
|
|
let cdlText = try String(contentsOfFile: mediaDir + "/CLIPS/R001C001.cdl", encoding: .utf8)
|
|
let parsed = try CDLExport.parseCDL(cdlText)
|
|
XCTAssertEqual(parsed.slope.x, 1.2, accuracy: 1e-5)
|
|
XCTAssertEqual(parsed.saturation, 1.1, accuracy: 1e-5)
|
|
}
|
|
|
|
// Case-insensitive + extension-agnostic matching.
|
|
func testMatchIsCaseAndExtensionAgnostic() throws {
|
|
try makeShot(clip: "R001C002")
|
|
try makeMedia("r001c002.MXF")
|
|
let result = try IngestMatcher.match(
|
|
mediaRoot: mediaDir, store: store, dayID: day.id,
|
|
options: .init(writeCDL: true, writeCube: false, latticeSize: 17))
|
|
XCTAssertEqual(result.matched.count, 1)
|
|
XCTAssertTrue(FileManager.default.fileExists(atPath: mediaDir + "/CLIPS/r001c002.cdl"))
|
|
}
|
|
|
|
// Media with no shot record -> unmatched list.
|
|
func testUnmatchedMediaListed() throws {
|
|
try makeShot(clip: "R001C003")
|
|
try makeMedia("R001C003.mov")
|
|
try makeMedia("R001C099.mov") // no shot
|
|
let result = try IngestMatcher.match(
|
|
mediaRoot: mediaDir, store: store, dayID: day.id,
|
|
options: .init(writeCDL: false, writeCube: false, latticeSize: 17))
|
|
XCTAssertEqual(result.matched.count, 1)
|
|
XCTAssertEqual(result.unmatchedMedia, ["CLIPS/R001C099.mov"])
|
|
}
|
|
|
|
// Shot with no media -> missing list.
|
|
func testShotsWithoutMediaListed() throws {
|
|
try makeShot(clip: "R001C004")
|
|
try makeShot(clip: "R001C005")
|
|
try makeMedia("R001C004.mov")
|
|
let result = try IngestMatcher.match(
|
|
mediaRoot: mediaDir, store: store, dayID: day.id,
|
|
options: .init(writeCDL: false, writeCube: false, latticeSize: 17))
|
|
XCTAssertEqual(result.shotsWithoutMedia, ["R001C005"])
|
|
}
|
|
|
|
// Shot without grade snapshot matches but writes no sidecars.
|
|
func testNoGradeNoSidecar() throws {
|
|
try makeShot(clip: "R001C006", withGrade: false)
|
|
try makeMedia("R001C006.mov")
|
|
let result = try IngestMatcher.match(
|
|
mediaRoot: mediaDir, store: store, dayID: day.id,
|
|
options: .init(writeCDL: true, writeCube: true, latticeSize: 17))
|
|
XCTAssertEqual(result.matched.count, 1)
|
|
XCTAssertFalse(result.matched[0].sidecarsWritten)
|
|
XCTAssertFalse(FileManager.default.fileExists(atPath: mediaDir + "/CLIPS/R001C006.cdl"))
|
|
}
|
|
|
|
// Non-media files ignored.
|
|
func testNonMediaIgnored() throws {
|
|
try makeShot(clip: "R001C007")
|
|
try makeMedia("R001C007.mov")
|
|
try Data("x".utf8).write(to: URL(fileURLWithPath: mediaDir + "/CLIPS/notes.txt"))
|
|
try Data("x".utf8).write(to: URL(fileURLWithPath: mediaDir + "/manifest.mhl"))
|
|
let result = try IngestMatcher.match(
|
|
mediaRoot: mediaDir, store: store, dayID: day.id,
|
|
options: .init(writeCDL: false, writeCube: false, latticeSize: 17))
|
|
XCTAssertEqual(result.matched.count, 1)
|
|
XCTAssertTrue(result.unmatchedMedia.isEmpty)
|
|
}
|
|
|
|
// Existing sidecar not overwritten unless force.
|
|
func testExistingSidecarPreserved() throws {
|
|
try makeShot(clip: "R001C008")
|
|
try makeMedia("R001C008.mov")
|
|
try Data("existing".utf8).write(to: URL(fileURLWithPath: mediaDir + "/CLIPS/R001C008.cdl"))
|
|
|
|
_ = try IngestMatcher.match(
|
|
mediaRoot: mediaDir, store: store, dayID: day.id,
|
|
options: .init(writeCDL: true, writeCube: false, latticeSize: 17))
|
|
let content = try String(contentsOfFile: mediaDir + "/CLIPS/R001C008.cdl", encoding: .utf8)
|
|
XCTAssertEqual(content, "existing")
|
|
|
|
_ = try IngestMatcher.match(
|
|
mediaRoot: mediaDir, store: store, dayID: day.id,
|
|
options: .init(writeCDL: true, writeCube: false, latticeSize: 17, force: true))
|
|
let after = try String(contentsOfFile: mediaDir + "/CLIPS/R001C008.cdl", encoding: .utf8)
|
|
XCTAssertTrue(after.contains("ColorDecisionList"))
|
|
}
|
|
}
|