rainbow-dragon/Tests/ForgeCameraTests/CameraSeamTests.swift

147 lines
5.4 KiB
Swift

import XCTest
import ForgeGrade
import ForgeColor
@testable import ForgeCamera
/// Scriptable in-test driver: fails N connects, then succeeds. Emits states on demand.
actor FlakyDriver: CameraDriver {
nonisolated let capabilities = CameraCapabilities(
vendor: .arri,
supportsNativeCDL: true,
lut3dSizes: [33],
metadataFields: [.clipName, .exposureIndex])
private var failuresRemaining: Int
private(set) var connectAttempts = 0
private(set) var pushedLooks = [FlattenedLook]()
private var continuation: AsyncStream<CameraState>.Continuation?
private let stream: AsyncStream<CameraState>
init(failFirst: Int) {
failuresRemaining = failFirst
var cont: AsyncStream<CameraState>.Continuation!
stream = AsyncStream { cont = $0 }
continuation = cont
}
nonisolated var state: AsyncStream<CameraState> { stream }
func connect() async throws {
connectAttempts += 1
if failuresRemaining > 0 {
failuresRemaining -= 1
throw CameraError.connectionFailed("scripted failure")
}
continuation?.yield(CameraState(connection: .connected))
}
func disconnect() async {
continuation?.yield(CameraState(connection: .disconnected))
}
func push(look: FlattenedLook) async throws {
pushedLooks.append(look)
}
// Test hooks.
func emit(_ s: CameraState) {
continuation?.yield(s)
}
}
final class CameraSeamTests: XCTestCase {
// Supervisor retries with backoff until driver connects.
func testSupervisorRetriesUntilConnected() async throws {
let driver = FlakyDriver(failFirst: 3)
let supervisor = ConnectionSupervisor(
driver: driver,
backoff: BackoffPolicy(initial: .milliseconds(1), multiplier: 1, maxDelay: .milliseconds(2)))
try await supervisor.start()
// Wait for connected.
let ok = await supervisor.waitForConnection(timeout: .seconds(2))
XCTAssertTrue(ok)
let attempts = await driver.connectAttempts
XCTAssertEqual(attempts, 4) // 3 failures + 1 success
await supervisor.stop()
}
// Health reflects driver state stream.
func testHealthTracksState() async throws {
let driver = FlakyDriver(failFirst: 0)
let supervisor = ConnectionSupervisor(
driver: driver,
backoff: BackoffPolicy(initial: .milliseconds(1), multiplier: 1, maxDelay: .milliseconds(2)))
try await supervisor.start()
_ = await supervisor.waitForConnection(timeout: .seconds(2))
var health = await supervisor.health
XCTAssertEqual(health, .connected)
await driver.emit(CameraState(connection: .disconnected))
try await Task.sleep(for: .milliseconds(50))
health = await supervisor.health
XCTAssertEqual(health, .disconnected)
await supervisor.stop()
}
// Recording state + metadata pass through supervisor's published stream.
func testStatePassthrough() async throws {
let driver = FlakyDriver(failFirst: 0)
let supervisor = ConnectionSupervisor(
driver: driver,
backoff: BackoffPolicy(initial: .milliseconds(1), multiplier: 1, maxDelay: .milliseconds(2)))
try await supervisor.start()
_ = await supervisor.waitForConnection(timeout: .seconds(2))
var iterator = supervisor.states.makeAsyncIterator()
await driver.emit(CameraState(
connection: .connected,
isRecording: true,
clipName: "A001C002",
metadata: CameraMetadata(exposureIndex: 800, whiteBalance: 5600, tint: 0, fps: 24, timecode: "12:00:00:00")))
// Drain until we see the recording state (connected event may arrive first).
var found = false
for _ in 0..<5 {
if let s = await iterator.next(), s.isRecording, s.clipName == "A001C002" {
XCTAssertEqual(s.metadata?.exposureIndex, 800)
found = true
break
}
}
XCTAssertTrue(found)
await supervisor.stop()
}
// Slot registry add/remove/lookup.
func testSlotRegistry() async throws {
let registry = SlotRegistry()
let driver = FlakyDriver(failFirst: 0)
let slot = await registry.addSlot(name: "A-Cam", driver: driver)
let all = await registry.slots
XCTAssertEqual(all.count, 1)
XCTAssertEqual(all[0].name, "A-Cam")
await registry.removeSlot(id: slot.id)
let after = await registry.slots
XCTAssertTrue(after.isEmpty)
}
// Capabilities surfaced through slot.
func testCapabilities() async {
let driver = FlakyDriver(failFirst: 0)
XCTAssertTrue(driver.capabilities.supportsNativeCDL)
XCTAssertEqual(driver.capabilities.vendor, .arri)
XCTAssertTrue(driver.capabilities.lut3dSizes.contains(33))
}
// Backoff policy caps at maxDelay and multiplies.
func testBackoffPolicy() {
var policy = BackoffPolicy(initial: .milliseconds(100), multiplier: 2, maxDelay: .milliseconds(350))
XCTAssertEqual(policy.nextDelay(), .milliseconds(100))
XCTAssertEqual(policy.nextDelay(), .milliseconds(200))
XCTAssertEqual(policy.nextDelay(), .milliseconds(350)) // capped from 400
XCTAssertEqual(policy.nextDelay(), .milliseconds(350))
policy.reset()
XCTAssertEqual(policy.nextDelay(), .milliseconds(100))
}
}