rainbow-dragon/Tests/ForgeCameraARRITests/ArriDriverTests.swift

203 lines
6.9 KiB
Swift
Raw Permalink Normal View History

import XCTest
import Foundation
import ForgeCamera
import ForgeGrade
import ForgeColor
import ForgeSim
@testable import ForgeCameraARRI
/// Tests against CAP-faithful simulator (binary TCP, MD5 challenge auth,
/// variable subscription docs/research/arri-cap-protocol.md).
final class ArriDriverTests: XCTestCase {
var sim: ArriSimulator!
override func setUp() async throws {
sim = ArriSimulator(password: "arri")
try await sim.start(port: 0)
}
override func tearDown() async throws {
try? await sim.stop()
sim = nil
}
func makeDriver(password: String = "arri") async -> ArriDriver {
let port = await sim.boundPort!
return ArriDriver(host: "127.0.0.1", port: port, password: password)
}
func testCapabilities() async {
let driver = await makeDriver()
XCTAssertEqual(driver.capabilities.vendor, .arri)
XCTAssertTrue(driver.capabilities.supportsNativeCDL)
XCTAssertTrue(driver.capabilities.lut3dSizes.contains(33))
}
// Full handshake: challenge -> MD5 password -> client name -> subscribed + connected.
func testConnectHandshake() async throws {
let driver = await makeDriver()
var iterator = driver.state.makeAsyncIterator()
try await driver.connect()
var connected = false
for _ in 0..<10 {
if let s = await iterator.next(), s.connection == .connected {
connected = true
break
}
}
XCTAssertTrue(connected)
let names = await sim.clientNames
XCTAssertEqual(names, ["Forge"])
await driver.disconnect()
}
// Wrong password -> connectionFailed, never authenticated.
func testWrongPasswordRejected() async throws {
let driver = await makeDriver(password: "wrong")
do {
try await driver.connect()
XCTFail("expected throw")
} catch let e as CameraError {
guard case .connectionFailed(let msg) = e else { return XCTFail("wrong error") }
XCTAssertTrue(msg.contains("password"))
}
let names = await sim.clientNames
XCTAssertTrue(names.isEmpty)
}
func testConnectFailsOnDeadPort() async {
let driver = ArriDriver(host: "127.0.0.1", port: 1)
do {
try await driver.connect()
XCTFail("expected throw")
} catch {}
}
// Subscription events: rec start/stop with composed clip name RxxxCxxx.
func testRecStateEvents() async throws {
let driver = await makeDriver()
var iterator = driver.state.makeAsyncIterator()
try await driver.connect()
await sim.setRecording(true, clipNumber: 7)
// Events arrive per-variable; wait for a state with BOTH rec + clip.
var sawRec = false
for _ in 0..<40 {
if let s = await iterator.next(), s.isRecording, s.clipName == "R001C007" {
sawRec = true
break
}
}
XCTAssertTrue(sawRec)
await sim.setRecording(false)
var sawStop = false
for _ in 0..<30 {
if let s = await iterator.next(), !s.isRecording {
sawStop = true
break
}
}
XCTAssertTrue(sawStop)
await driver.disconnect()
}
// Metadata via subscribed variables incl. BCD timecode decode.
func testMetadataEvents() async throws {
let driver = await makeDriver()
var iterator = driver.state.makeAsyncIterator()
try await driver.connect()
await sim.setMetadata(ei: 1280, colorTemp: 3200, tint: -2, fps: 25, timecode: "10:20:30:12")
var got = false
for _ in 0..<40 {
if let s = await iterator.next(), let md = s.metadata,
md.exposureIndex == 1280, md.whiteBalance == 3200,
md.tint == -2, md.timecode == "10:20:30:12",
let fps = md.fps, abs(fps - 25) < 0.01 {
got = true
break
}
}
XCTAssertTrue(got)
await driver.disconnect()
}
// CDL push -> SetVariable 0x0050 blob lands in sim with correct 10 floats.
func testPushCDL() async throws {
let driver = await makeDriver()
try await driver.connect()
let cdl = CDL(
slope: SIMD3(1.1, 1.0, 0.9),
offset: SIMD3(0.02, 0.0, -0.01),
power: SIMD3(0.95, 1.0, 1.05),
saturation: 1.3)
try await driver.push(look: FlattenedLook(cdl: cdl, lut: nil, latticeSize: 33))
let blob = await sim.lastCDL
XCTAssertNotNil(blob)
XCTAssertEqual(blob![0], 1.1, accuracy: 1e-5) // slope R
XCTAssertEqual(blob![2], 0.9, accuracy: 1e-5) // slope B
XCTAssertEqual(blob![3], 0.02, accuracy: 1e-5) // offset R
XCTAssertEqual(blob![5], -0.01, accuracy: 1e-5)
XCTAssertEqual(blob![6], 0.95, accuracy: 1e-5) // power R
XCTAssertEqual(blob![9], 1.3, accuracy: 1e-5) // sat
await driver.disconnect()
}
// CDL + LUT push: both land (LUT path experimental but sim accepts).
func testPushCDLAndLut() async throws {
let driver = await makeDriver()
try await driver.connect()
let look = FlattenedLook(
cdl: CDL(slope: SIMD3(1.2, 1, 1), offset: .zero, power: .one, saturation: 1),
lut: Lut3D.build(size: 17) { $0 * 0.9 },
latticeSize: 17)
try await driver.push(look: look)
let cdl = await sim.lastCDL
XCTAssertEqual(cdl?[0] ?? 0, 1.2, accuracy: 1e-5)
let luts = await sim.lutUploads
XCTAssertEqual(luts.count, 1)
// U16 size prefix + 17³×3 floats.
XCTAssertEqual(luts[0].count, 2 + 17 * 17 * 17 * 3 * 4)
await driver.disconnect()
}
// Unsupported LUT size rejected before wire.
func testUnsupportedLutSizeRejected() async throws {
let driver = await makeDriver()
try await driver.connect()
let look = FlattenedLook(cdl: nil, lut: Lut3D.build(size: 5) { $0 * 0.5 }, latticeSize: 5)
do {
try await driver.push(look: look)
XCTFail("expected unsupportedLook")
} catch let e as CameraError {
guard case .unsupportedLook = e else { return XCTFail("wrong error") }
}
await driver.disconnect()
}
// Reconnect after sim restart on same port.
func testReconnectAfterSimRestart() async throws {
let port = await sim.boundPort!
let driver = ArriDriver(host: "127.0.0.1", port: port)
try await driver.connect()
await driver.disconnect()
try await sim.stop()
sim = ArriSimulator(password: "arri")
try await sim.start(port: port)
try await driver.connect()
try await driver.push(look: FlattenedLook(
cdl: .identity, lut: nil, latticeSize: 33))
let blob = await sim.lastCDL
XCTAssertNotNil(blob)
await driver.disconnect()
}
}