rainbow-dragon/Tests/ForgeCameraTests/GangTests.swift

137 lines
4.9 KiB
Swift
Raw Normal View History

import XCTest
import Foundation
import ForgeGrade
import ForgeColor
@testable import ForgeCamera
/// Recording driver: captures pushes, declares capabilities.
actor RecordingDriver: CameraDriver {
nonisolated let capabilities: CameraCapabilities
private(set) var pushedLooks = [FlattenedLook]()
init(nativeCDL: Bool, lutSizes: [Int] = [17, 33]) {
capabilities = CameraCapabilities(
vendor: .simulated, supportsNativeCDL: nativeCDL,
lut3dSizes: lutSizes, metadataFields: [])
}
nonisolated var state: AsyncStream<CameraState> {
AsyncStream { _ in }
}
func connect() async throws {}
func disconnect() async {}
func push(look: FlattenedLook) async throws {
pushedLooks.append(look)
}
}
final class GangTests: XCTestCase {
func makeGrade() -> GradeStack {
GradeStack(nodes: [
GradeNode(kind: .cdl(CDL(slope: SIMD3(1.2, 1, 0.9), offset: .zero, power: .one, saturation: 1.1))),
GradeNode(kind: .saturation(0.95)),
])
}
// Push to ganged slots: each camera gets a look flattened per its own capabilities.
func testGangPushRespectsCapabilities() async throws {
let arriLike = RecordingDriver(nativeCDL: true)
let sonyLike = RecordingDriver(nativeCDL: false)
let gang = GradeGang(latticeSize: 17)
await gang.setGrade(makeGrade(), for: "A")
await gang.link(slots: ["A", "B"])
await gang.register(slot: "A", driver: arriLike)
await gang.register(slot: "B", driver: sonyLike)
try await gang.pushGrade(from: "A")
let arriPushes = await arriLike.pushedLooks
let sonyPushes = await sonyLike.pushedLooks
XCTAssertEqual(arriPushes.count, 1)
XCTAssertEqual(sonyPushes.count, 1)
// ARRI-like gets split CDL; Sony-like full bake, no CDL.
XCTAssertNotNil(arriPushes[0].cdl)
XCTAssertNil(sonyPushes[0].cdl)
XCTAssertNotNil(sonyPushes[0].lut)
}
// Ganged edit propagates grade to linked slots.
func testGangPropagatesGrade() async throws {
let gang = GradeGang(latticeSize: 17)
await gang.link(slots: ["A", "B", "C"])
await gang.setGrade(makeGrade(), for: "A")
let gradeB = await gang.grade(for: "B")
let gradeC = await gang.grade(for: "C")
XCTAssertEqual(gradeB?.nodes.count, 2)
XCTAssertEqual(gradeC?.nodes.count, 2)
}
// Unlink stops propagation.
func testUnlinkStopsPropagation() async throws {
let gang = GradeGang(latticeSize: 17)
await gang.link(slots: ["A", "B"])
await gang.setGrade(makeGrade(), for: "A")
await gang.unlink(slot: "B")
// New grade on A only.
let solo = GradeStack(nodes: [GradeNode(kind: .saturation(0.5))])
await gang.setGrade(solo, for: "A")
let gradeA = await gang.grade(for: "A")
let gradeB = await gang.grade(for: "B")
XCTAssertEqual(gradeA?.nodes.count, 1)
XCTAssertEqual(gradeB?.nodes.count, 2) // stays on pre-unlink grade
}
// Per-slot override: editing an unlinked slot never touches others.
func testSoloEditIsolated() async throws {
let gang = GradeGang(latticeSize: 17)
await gang.setGrade(makeGrade(), for: "A")
await gang.setGrade(GradeStack(nodes: []), for: "B")
let gradeA = await gang.grade(for: "A")
let gradeB = await gang.grade(for: "B")
XCTAssertEqual(gradeA?.nodes.count, 2)
XCTAssertEqual(gradeB?.nodes.count, 0)
}
// Push from an unregistered slot throws.
func testPushUnregisteredThrows() async {
let gang = GradeGang(latticeSize: 17)
await gang.setGrade(makeGrade(), for: "X")
do {
try await gang.pushGrade(from: "X")
XCTFail("expected throw")
} catch {}
}
// Push failure on one camera doesn't block others (best-effort multi-push).
func testPushPartialFailure() async throws {
let good = RecordingDriver(nativeCDL: true)
let bad = RecordingDriver(nativeCDL: false, lutSizes: [99]) // rejects 17
let gang = GradeGang(latticeSize: 17)
await gang.link(slots: ["A", "B"])
await gang.setGrade(makeGrade(), for: "A")
await gang.register(slot: "A", driver: good)
await gang.register(slot: "B", driver: bad)
// bad driver: flatten will target unsupported size -> per-slot error collected.
do {
try await gang.pushGrade(from: "A")
XCTFail("expected partial failure to throw summary")
} catch let e as GangError {
guard case .partialFailure(let failures) = e else { return XCTFail("wrong error") }
XCTAssertEqual(failures.count, 1)
XCTAssertEqual(failures[0].slot, "B")
}
// Good camera still got its push.
let pushes = await good.pushedLooks
XCTAssertEqual(pushes.count, 1)
}
}