rainbow-dragon/Tests/ForgeCameraARRITests/ArriDriverTests.swift

202 lines
6.9 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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