import XCTest import Foundation import ForgeCameraARRI import ForgeOffload @testable import ForgeSim /// CAP-level sim tests: raw TCP client speaking binary CAP frames. final class ArriSimTests: 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 } /// Minimal blocking CAP test client. final class TestClient { let fd: Int32 var buffer = Data() var msgId: UInt16 = 100 init?(port: Int) { fd = socket(AF_INET, Int32(SOCK_STREAM.rawValue), 0) guard fd >= 0 else { return nil } var tv = timeval(tv_sec: 2, tv_usec: 0) setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, socklen_t(MemoryLayout.size)) var addr = sockaddr_in() addr.sin_family = sa_family_t(AF_INET) addr.sin_port = in_port_t(UInt16(port).bigEndian) _ = inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr) let rc = withUnsafePointer(to: &addr) { ptr in ptr.withMemoryRebound(to: sockaddr.self, capacity: 1) { sa in connect(fd, sa, socklen_t(MemoryLayout.size)) } } guard rc == 0 else { close(fd) return nil } } deinit { close(fd) } func send(_ cmd: CAP.Command, payload: Data = Data()) -> UInt16 { msgId &+= 1 let frame = CAP.Frame( msgType: CAP.MsgType.command.rawValue, msgId: msgId, cmdCode: cmd.rawValue, payload: payload) let data = CAP.encode(frame) _ = data.withUnsafeBytes { write(fd, $0.baseAddress, $0.count) } return msgId } func readFrame() -> CAP.Frame? { var chunk = [UInt8](repeating: 0, count: 8192) for _ in 0..<40 { if let f = try? CAP.decodeFirst(&buffer), f != nil { return f } let n = read(fd, &chunk, chunk.count) if n > 0 { buffer.append(contentsOf: chunk[0.. Bool) -> CAP.Frame? { for _ in 0..<50 { guard let f = readFrame() else { return nil } if predicate(f) { return f } } return nil } func authenticate(password: String) -> Bool { // Welcome arrives unsolicited. guard readUntil({ $0.cmdCode == CAP.Command.welcome.rawValue }) != nil else { return false } let chalId = send(.requestPwdChallenge) guard let chalReply = readUntil({ $0.msgId == chalId }), let (challenge, _) = CAP.readString(chalReply.payload, at: 0) else { return false } var pw = Data() CAP.putString(MD5.hexString(Data((challenge + password).utf8)), into: &pw) let pwId = send(.password, payload: pw) guard let pwReply = readUntil({ $0.msgId == pwId }), pwReply.cmdCode == CAP.Result.ok.rawValue else { return false } return true } } func testWelcomeOnConnect() async throws { let port = await sim.boundPort! let client = try XCTUnwrap(TestClient(port: port)) let welcome = client.readUntil { $0.cmdCode == CAP.Command.welcome.rawValue } XCTAssertNotNil(welcome) let (version, _) = CAP.readString(welcome!.payload, at: 0) ?? ("", 0) XCTAssertTrue(version.contains("CAP")) } func testAuthFlow() async throws { let port = await sim.boundPort! let client = try XCTUnwrap(TestClient(port: port)) XCTAssertTrue(client.authenticate(password: "arri")) } func testWrongPassword() async throws { let port = await sim.boundPort! let client = try XCTUnwrap(TestClient(port: port)) XCTAssertFalse(client.authenticate(password: "nope")) } func testUnauthenticatedGetRejected() async throws { let port = await sim.boundPort! let client = try XCTUnwrap(TestClient(port: port)) _ = client.readUntil { $0.cmdCode == CAP.Command.welcome.rawValue } var payload = Data() CAP.putU16(CAP.Variable.exposureIndex.rawValue, into: &payload) let id = client.send(.getVariable, payload: payload) let reply = client.readUntil { $0.msgId == id } XCTAssertEqual(reply?.cmdCode, CAP.Result.notAuthorized.rawValue) } func testGetVariable() async throws { await sim.setMetadata(ei: 1280, colorTemp: 3200, tint: -2, fps: 25, timecode: "01:02:03:04") let port = await sim.boundPort! let client = try XCTUnwrap(TestClient(port: port)) XCTAssertTrue(client.authenticate(password: "arri")) var payload = Data() CAP.putU16(CAP.Variable.exposureIndex.rawValue, into: &payload) let id = client.send(.getVariable, payload: payload) let reply = client.readUntil { $0.msgId == id } XCTAssertEqual(reply?.cmdCode, CAP.Result.ok.rawValue) XCTAssertEqual(CAP.readU16(reply!.payload, at: 0), CAP.Variable.exposureIndex.rawValue) XCTAssertEqual(CAP.readU32(reply!.payload, at: 2), 1280) } func testSetCDLStored() async throws { let port = await sim.boundPort! let client = try XCTUnwrap(TestClient(port: port)) XCTAssertTrue(client.authenticate(password: "arri")) var payload = Data() CAP.putU16(CAP.Variable.cdlValues.rawValue, into: &payload) payload.append(CAP.encodeCDLBlob( slope: (1.5, 1, 1), offset: (0, 0, 0), power: (1, 1, 1), saturation: 0.8)) let id = client.send(.setVariable, payload: payload) let reply = client.readUntil { $0.msgId == id } XCTAssertEqual(reply?.cmdCode, CAP.Result.ok.rawValue) let stored = await sim.lastCDL XCTAssertEqual(stored?[0] ?? 0, 1.5, accuracy: 1e-5) XCTAssertEqual(stored?[9] ?? 0, 0.8, accuracy: 1e-5) } func testSubscriptionPushesEvents() async throws { let port = await sim.boundPort! let client = try XCTUnwrap(TestClient(port: port)) XCTAssertTrue(client.authenticate(password: "arri")) var payload = Data() CAP.putU16(CAP.Variable.cameraState.rawValue, into: &payload) let id = client.send(.requestVariables, payload: payload) _ = client.readUntil { $0.msgId == id } await sim.setRecording(true) let event = client.readUntil { frame in frame.msgType == CAP.MsgType.event.rawValue && CAP.readU16(frame.payload, at: 0) == CAP.Variable.cameraState.rawValue && CAP.CameraStateBits(rawValue: CAP.readU16(frame.payload, at: 2) ?? 0).contains(.recording) } XCTAssertNotNil(event) } }