148 lines
5.4 KiB
Swift
148 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))
|
||
|
|
}
|
||
|
|
}
|