import XCTest import Foundation import ForgeCamera import ForgeGrade import ForgeColor import ForgeSim @testable import ForgeCameraRED final class RedDriverTests: XCTestCase { var sim: RedSimulator! override func setUp() async throws { sim = RedSimulator() try await sim.start(port: 0) } override func tearDown() async throws { try? await sim.stop() sim = nil } func makeDriver(pollInterval: Duration = .milliseconds(20)) async -> RedDriver { let port = await sim.boundPort! return RedDriver(host: "127.0.0.1", port: port, pollInterval: pollInterval) } // Capabilities: RED = native CDL (IPP2), 17/33 LUTs. func testCapabilities() async { let driver = await makeDriver() XCTAssertEqual(driver.capabilities.vendor, .red) XCTAssertTrue(driver.capabilities.supportsNativeCDL) XCTAssertTrue(driver.capabilities.lut3dSizes.contains(33)) } // Connect performs RCP-style handshake, emits connected. func testConnectHandshake() async throws { let driver = await makeDriver() var iterator = driver.state.makeAsyncIterator() try await driver.connect() var connected = false for _ in 0..<5 { if let s = await iterator.next(), s.connection == .connected { connected = true break } } XCTAssertTrue(connected) let handshakes = await sim.handshakeCount XCTAssertEqual(handshakes, 1) await driver.disconnect() } func testConnectFailsOnDeadPort() async { let driver = RedDriver(host: "127.0.0.1", port: 1, pollInterval: .milliseconds(20)) do { try await driver.connect() XCTFail("expected throw") } catch {} } // Rec state + clip name via param polling. func testRecStateEvents() async throws { let driver = await makeDriver() var iterator = driver.state.makeAsyncIterator() try await driver.connect() await sim.setParam("RECORD_STATE", value: "1") await sim.setParam("CLIP_NAME", value: "B001_C001_0710AB") var sawRec = false for _ in 0..<30 { if let s = await iterator.next(), s.isRecording { XCTAssertEqual(s.clipName, "B001_C001_0710AB") sawRec = true break } } XCTAssertTrue(sawRec) await sim.setParam("RECORD_STATE", value: "0") var sawStop = false for _ in 0..<30 { if let s = await iterator.next(), !s.isRecording { sawStop = true break } } XCTAssertTrue(sawStop) await driver.disconnect() } // Metadata params surface. func testMetadata() async throws { await sim.setParam("ISO", value: "1600") await sim.setParam("COLOR_TEMP", value: "4500") await sim.setParam("TINT", value: "3") await sim.setParam("SENSOR_FPS", value: "23.98") await sim.setParam("TIMECODE", value: "15:30:00:00") let driver = await makeDriver() var iterator = driver.state.makeAsyncIterator() try await driver.connect() var got = false for _ in 0..<30 { if let s = await iterator.next(), let md = s.metadata, md.exposureIndex == 1600 { XCTAssertEqual(md.whiteBalance, 4500) XCTAssertEqual(md.tint, 3) XCTAssertEqual(md.timecode, "15:30:00:00") got = true break } } XCTAssertTrue(got) await driver.disconnect() } // Push: CDL to IPP2 slots + LUT upload. func testPushLookWithNativeCDL() 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), power: .one, saturation: 1.3) let look = FlattenedLook(cdl: cdl, lut: Lut3D.identity(size: 33), latticeSize: 33) try await driver.push(look: look) let cdlValue = await sim.params["IPP2_CDL"] XCTAssertNotNil(cdlValue) let obj = try JSONSerialization.jsonObject(with: Data(cdlValue!.utf8)) as? [String: Any] let slope = obj?["slope"] as? [Double] XCTAssertEqual(slope?[0] ?? 0, 1.1, accuracy: 1e-5) let luts = await sim.uploadedLuts XCTAssertEqual(luts.count, 1) XCTAssertEqual(luts[0].size, 33) await driver.disconnect() } // Unsupported LUT size rejected. func testUnsupportedLutRejected() async throws { let driver = await makeDriver() try await driver.connect() do { try await driver.push(look: FlattenedLook(cdl: nil, lut: Lut3D.identity(size: 65), latticeSize: 65)) XCTFail("expected unsupportedLook") } catch let e as CameraError { guard case .unsupportedLook = e else { return XCTFail("wrong error") } } await driver.disconnect() } }