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