2026-07-10 15:01:40 -04:00
|
|
|
import XCTest
|
|
|
|
|
import Foundation
|
2026-07-11 10:02:56 -04:00
|
|
|
import ForgeCameraARRI
|
|
|
|
|
import ForgeOffload
|
2026-07-10 15:01:40 -04:00
|
|
|
@testable import ForgeSim
|
|
|
|
|
|
2026-07-11 10:02:56 -04:00
|
|
|
/// CAP-level sim tests: raw TCP client speaking binary CAP frames.
|
2026-07-10 15:01:40 -04:00
|
|
|
final class ArriSimTests: XCTestCase {
|
|
|
|
|
|
|
|
|
|
var sim: ArriSimulator!
|
|
|
|
|
|
|
|
|
|
override func setUp() async throws {
|
2026-07-11 10:02:56 -04:00
|
|
|
sim = ArriSimulator(password: "arri")
|
|
|
|
|
try await sim.start(port: 0)
|
2026-07-10 15:01:40 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override func tearDown() async throws {
|
2026-07-11 10:02:56 -04:00
|
|
|
try? await sim.stop()
|
2026-07-10 15:01:40 -04:00
|
|
|
sim = nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-11 10:02:56 -04:00
|
|
|
/// 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<timeval>.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<sockaddr_in>.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..<n])
|
|
|
|
|
} else if n == 0 {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return try? CAP.decodeFirst(&buffer) ?? nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Read frames until predicate matches (skips events/other replies).
|
|
|
|
|
func readUntil(_ predicate: (CAP.Frame) -> 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
|
|
|
|
|
}
|
2026-07-10 15:01:40 -04:00
|
|
|
}
|
|
|
|
|
|
2026-07-11 10:02:56 -04:00
|
|
|
func testWelcomeOnConnect() async throws {
|
2026-07-10 15:01:40 -04:00
|
|
|
let port = await sim.boundPort!
|
2026-07-11 10:02:56 -04:00
|
|
|
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"))
|
2026-07-10 15:01:40 -04:00
|
|
|
}
|
|
|
|
|
|
2026-07-11 10:02:56 -04:00
|
|
|
func testAuthFlow() async throws {
|
|
|
|
|
let port = await sim.boundPort!
|
|
|
|
|
let client = try XCTUnwrap(TestClient(port: port))
|
|
|
|
|
XCTAssertTrue(client.authenticate(password: "arri"))
|
2026-07-10 15:01:40 -04:00
|
|
|
}
|
|
|
|
|
|
2026-07-11 10:02:56 -04:00
|
|
|
func testWrongPassword() async throws {
|
|
|
|
|
let port = await sim.boundPort!
|
|
|
|
|
let client = try XCTUnwrap(TestClient(port: port))
|
|
|
|
|
XCTAssertFalse(client.authenticate(password: "nope"))
|
2026-07-10 15:01:40 -04:00
|
|
|
}
|
|
|
|
|
|
2026-07-11 10:02:56 -04:00
|
|
|
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)
|
2026-07-10 15:01:40 -04:00
|
|
|
}
|
|
|
|
|
|
2026-07-11 10:02:56 -04:00
|
|
|
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)
|
2026-07-10 15:01:40 -04:00
|
|
|
}
|
|
|
|
|
|
2026-07-11 10:02:56 -04:00
|
|
|
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)
|
2026-07-10 15:01:40 -04:00
|
|
|
}
|
|
|
|
|
|
2026-07-11 10:02:56 -04:00
|
|
|
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)
|
2026-07-10 15:01:40 -04:00
|
|
|
}
|
|
|
|
|
}
|