362 lines
14 KiB
Rust
362 lines
14 KiB
Rust
// Integration coverage for server state-oriented RPC handler branches.
|
|
//
|
|
// Scope: include `server/src/main.rs` and exercise calibration, capture-power,
|
|
// and upstream-sync RPC surfaces.
|
|
// Targets: `server/src/main.rs`.
|
|
// Why: these RPCs expose live operational state, so tests should guard reply
|
|
// shapes without requiring gadget, HID, or capture hardware.
|
|
|
|
#[allow(warnings)]
|
|
mod server_main_state_rpc {
|
|
include!(env!("LESAVKA_SERVER_MAIN_SRC"));
|
|
|
|
use serial_test::serial;
|
|
use temp_env::with_var;
|
|
use tempfile::tempdir;
|
|
|
|
fn build_handler_for_tests_with_modes(
|
|
kb_writable: bool,
|
|
ms_writable: bool,
|
|
) -> (tempfile::TempDir, Handler) {
|
|
let dir = tempdir().expect("tempdir");
|
|
let kb_path = dir.path().join("hidg0.bin");
|
|
let ms_path = dir.path().join("hidg1.bin");
|
|
std::fs::write(&kb_path, []).expect("create kb file");
|
|
std::fs::write(&ms_path, []).expect("create ms file");
|
|
let kb = tokio::fs::File::from_std(
|
|
std::fs::OpenOptions::new()
|
|
.read(true)
|
|
.write(kb_writable)
|
|
.create(kb_writable)
|
|
.truncate(kb_writable)
|
|
.open(&kb_path)
|
|
.expect("open kb"),
|
|
);
|
|
let ms = tokio::fs::File::from_std(
|
|
std::fs::OpenOptions::new()
|
|
.read(true)
|
|
.write(ms_writable)
|
|
.create(ms_writable)
|
|
.truncate(ms_writable)
|
|
.open(&ms_path)
|
|
.expect("open ms"),
|
|
);
|
|
let handler = with_var("LESAVKA_CAPTURE_POWER_UNIT", Some("none"), || Handler {
|
|
kb: std::sync::Arc::new(tokio::sync::Mutex::new(Some(kb))),
|
|
ms: std::sync::Arc::new(tokio::sync::Mutex::new(Some(ms))),
|
|
gadget: UsbGadget::new("lesavka"),
|
|
did_cycle: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
|
camera_rt: std::sync::Arc::new(CameraRuntime::new()),
|
|
upstream_media_rt: std::sync::Arc::new(UpstreamMediaRuntime::new()),
|
|
calibration: std::sync::Arc::new(CalibrationStore::load(std::sync::Arc::new(
|
|
UpstreamMediaRuntime::new(),
|
|
))),
|
|
capture_power: CapturePowerManager::new(),
|
|
eye_hubs: std::sync::Arc::new(
|
|
tokio::sync::Mutex::new(std::collections::HashMap::new()),
|
|
),
|
|
});
|
|
|
|
(dir, handler)
|
|
}
|
|
|
|
fn build_handler_for_tests() -> (tempfile::TempDir, Handler) {
|
|
build_handler_for_tests_with_modes(true, true)
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(coverage)]
|
|
#[serial]
|
|
fn capture_power_rpcs_surface_stub_snapshot_and_manual_modes() {
|
|
let (_dir, handler) = build_handler_for_tests();
|
|
let rt = tokio::runtime::Runtime::new().expect("runtime");
|
|
|
|
with_var(
|
|
"LESAVKA_TEST_UDEV_CAPTURE_DEVICES",
|
|
Some("not-a-number"),
|
|
|| {
|
|
assert_eq!(Handler::detected_capture_devices_from_udev(), 0);
|
|
},
|
|
);
|
|
with_var("LESAVKA_TEST_UDEV_CAPTURE_DEVICES", Some("9"), || {
|
|
assert_eq!(Handler::detected_capture_devices_from_udev(), 2);
|
|
});
|
|
|
|
let snapshot = rt
|
|
.block_on(async {
|
|
handler
|
|
.get_capture_power(tonic::Request::new(Empty {}))
|
|
.await
|
|
})
|
|
.expect("capture power snapshot")
|
|
.into_inner();
|
|
assert!(snapshot.available);
|
|
assert!(!snapshot.enabled);
|
|
assert_eq!(snapshot.mode, "auto");
|
|
|
|
let forced_on = rt
|
|
.block_on(async {
|
|
handler
|
|
.set_capture_power(tonic::Request::new(SetCapturePowerRequest {
|
|
enabled: true,
|
|
command: CapturePowerCommand::ForceOn as i32,
|
|
}))
|
|
.await
|
|
})
|
|
.expect("force capture power on")
|
|
.into_inner();
|
|
assert!(forced_on.available);
|
|
assert!(forced_on.enabled);
|
|
assert_eq!(forced_on.mode, "forced-on");
|
|
|
|
let forced_off = rt
|
|
.block_on(async {
|
|
handler
|
|
.set_capture_power(tonic::Request::new(SetCapturePowerRequest {
|
|
enabled: false,
|
|
command: CapturePowerCommand::ForceOff as i32,
|
|
}))
|
|
.await
|
|
})
|
|
.expect("force capture power off")
|
|
.into_inner();
|
|
assert!(forced_off.available);
|
|
assert!(!forced_off.enabled);
|
|
assert_eq!(forced_off.mode, "forced-off");
|
|
|
|
let auto = rt
|
|
.block_on(async {
|
|
handler
|
|
.set_capture_power(tonic::Request::new(SetCapturePowerRequest {
|
|
enabled: false,
|
|
command: CapturePowerCommand::Auto as i32,
|
|
}))
|
|
.await
|
|
})
|
|
.expect("return capture power to auto")
|
|
.into_inner();
|
|
assert!(auto.available);
|
|
assert!(!auto.enabled);
|
|
assert_eq!(auto.mode, "auto");
|
|
|
|
let legacy_fallback = rt
|
|
.block_on(async {
|
|
handler
|
|
.set_capture_power(tonic::Request::new(SetCapturePowerRequest {
|
|
enabled: true,
|
|
command: CapturePowerCommand::Unspecified as i32,
|
|
}))
|
|
.await
|
|
})
|
|
.expect("legacy bool fallback")
|
|
.into_inner();
|
|
assert!(legacy_fallback.available);
|
|
assert!(legacy_fallback.enabled);
|
|
assert_eq!(legacy_fallback.mode, "forced-on");
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(coverage)]
|
|
#[serial]
|
|
fn calibration_rpcs_surface_current_state_and_apply_updates() {
|
|
let dir = tempdir().expect("calibration dir");
|
|
let calibration_path = dir.path().join("calibration.toml");
|
|
with_var(
|
|
"LESAVKA_CALIBRATION_PATH",
|
|
Some(calibration_path.to_string_lossy().to_string()),
|
|
|| {
|
|
let (_dir, handler) = build_handler_for_tests();
|
|
let rt = tokio::runtime::Runtime::new().expect("runtime");
|
|
|
|
let initial = rt
|
|
.block_on(async {
|
|
handler.get_calibration(tonic::Request::new(Empty {})).await
|
|
})
|
|
.expect("initial calibration")
|
|
.into_inner();
|
|
assert_eq!(initial.profile, "mjpeg+pcm");
|
|
assert_eq!(initial.active_audio_offset_us, 0);
|
|
let initial_video_offset_us = initial.active_video_offset_us;
|
|
|
|
let adjusted = rt
|
|
.block_on(async {
|
|
handler
|
|
.calibrate(tonic::Request::new(CalibrationRequest {
|
|
action: lesavka_common::lesavka::CalibrationAction::BlindEstimate
|
|
as i32,
|
|
audio_delta_us: 10_000,
|
|
video_delta_us: 2_000,
|
|
observed_delivery_skew_ms: 42.0,
|
|
observed_enqueue_skew_ms: 2.5,
|
|
note: "coverage estimate".to_string(),
|
|
}))
|
|
.await
|
|
})
|
|
.expect("calibrate")
|
|
.into_inner();
|
|
assert_eq!(adjusted.source, "blind");
|
|
assert_eq!(adjusted.active_audio_offset_us, 10_000);
|
|
assert_eq!(
|
|
adjusted.active_video_offset_us,
|
|
initial_video_offset_us + 2_000
|
|
);
|
|
assert!(
|
|
std::fs::read_to_string(calibration_path)
|
|
.expect("persisted")
|
|
.contains("active_audio_offset_us=10000")
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(coverage)]
|
|
#[serial]
|
|
fn upstream_sync_rpc_surfaces_planner_snapshot() {
|
|
let (_dir, handler) = build_handler_for_tests();
|
|
let rt = tokio::runtime::Runtime::new().expect("runtime");
|
|
|
|
let lease_camera = handler.upstream_media_rt.activate_camera();
|
|
let lease_microphone = handler.upstream_media_rt.activate_microphone();
|
|
assert_eq!(lease_camera.session_id, lease_microphone.session_id);
|
|
|
|
let initial = rt
|
|
.block_on(async {
|
|
handler
|
|
.get_upstream_sync(tonic::Request::new(Empty {}))
|
|
.await
|
|
})
|
|
.expect("planner sync state")
|
|
.into_inner();
|
|
assert_eq!(initial.phase, "acquiring");
|
|
assert_eq!(initial.session_id, lease_camera.session_id);
|
|
|
|
handler.upstream_media_rt.record_client_timing(
|
|
UpstreamMediaKind::Camera,
|
|
UpstreamClientTiming {
|
|
capture_pts_us: 1_000_000,
|
|
send_pts_us: 1_010_000,
|
|
queue_depth: 2,
|
|
queue_age_ms: 20,
|
|
},
|
|
);
|
|
std::thread::sleep(Duration::from_millis(1));
|
|
handler.upstream_media_rt.record_client_timing(
|
|
UpstreamMediaKind::Microphone,
|
|
UpstreamClientTiming {
|
|
capture_pts_us: 1_001_500,
|
|
send_pts_us: 1_012_000,
|
|
queue_depth: 3,
|
|
queue_age_ms: 25,
|
|
},
|
|
);
|
|
let due = tokio::time::Instant::now() - Duration::from_millis(3);
|
|
handler.upstream_media_rt.mark_video_presented(10_000, due);
|
|
handler.upstream_media_rt.mark_audio_presented(11_500, due);
|
|
|
|
let live = rt
|
|
.block_on(async {
|
|
handler
|
|
.get_upstream_sync(tonic::Request::new(Empty {}))
|
|
.await
|
|
})
|
|
.expect("live planner sync state")
|
|
.into_inner();
|
|
assert_eq!(live.phase, "live");
|
|
assert_eq!(live.latest_camera_remote_pts_us, Some(1_000_000));
|
|
assert_eq!(live.latest_microphone_remote_pts_us, Some(1_001_500));
|
|
assert_eq!(live.last_video_presented_pts_us, Some(10_000));
|
|
assert_eq!(live.last_audio_presented_pts_us, Some(11_500));
|
|
assert!(live.live_lag_ms.is_some());
|
|
assert_eq!(live.planner_skew_ms, Some(1.5));
|
|
assert_eq!(live.client_capture_skew_ms, Some(1.5));
|
|
assert_eq!(live.client_send_skew_ms, Some(2.0));
|
|
assert!(live.server_receive_skew_ms.is_some());
|
|
assert_eq!(live.camera_client_queue_age_ms, Some(20.0));
|
|
assert_eq!(live.microphone_client_queue_age_ms, Some(25.0));
|
|
assert!(live.camera_server_receive_age_ms.is_some());
|
|
assert!(live.microphone_server_receive_age_ms.is_some());
|
|
assert!(live.client_capture_abs_skew_p95_ms.is_some());
|
|
assert!(live.client_send_abs_skew_p95_ms.is_some());
|
|
assert!(live.server_receive_abs_skew_p95_ms.is_some());
|
|
assert!(live.camera_client_queue_age_p95_ms.is_some());
|
|
assert!(live.microphone_client_queue_age_p95_ms.is_some());
|
|
assert!(live.sink_handoff_skew_ms.is_some());
|
|
assert!(live.sink_handoff_abs_skew_p95_ms.is_some());
|
|
assert!(live.camera_sink_late_ms.is_some());
|
|
assert!(live.microphone_sink_late_ms.is_some());
|
|
assert!(live.camera_sink_late_p95_ms.is_some());
|
|
assert!(live.microphone_sink_late_p95_ms.is_some());
|
|
assert_eq!(live.client_timing_window_samples, 1);
|
|
assert_eq!(live.sink_handoff_window_samples, 1);
|
|
|
|
handler
|
|
.upstream_media_rt
|
|
.record_video_freeze("coverage freeze");
|
|
let healing = rt
|
|
.block_on(async {
|
|
handler
|
|
.get_upstream_sync(tonic::Request::new(Empty {}))
|
|
.await
|
|
})
|
|
.expect("healing planner sync state")
|
|
.into_inner();
|
|
assert_eq!(healing.phase, "healing");
|
|
assert_eq!(healing.video_freezes, 1);
|
|
assert_eq!(healing.last_reason, "coverage freeze");
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(coverage)]
|
|
#[serial]
|
|
fn recover_soft_rpcs_surface_uac_success_and_non_uvc_guard() {
|
|
with_var("LESAVKA_CAM_OUTPUT", Some("hdmi"), || {
|
|
let (_dir, handler) = build_handler_for_tests();
|
|
let rt = tokio::runtime::Runtime::new().expect("runtime");
|
|
|
|
let uac = rt
|
|
.block_on(async { handler.recover_uac(tonic::Request::new(Empty {})).await })
|
|
.expect("uac recovery")
|
|
.into_inner();
|
|
assert!(uac.ok);
|
|
|
|
let uvc = rt
|
|
.block_on(async { handler.recover_uvc(tonic::Request::new(Empty {})).await })
|
|
.expect_err("HDMI output should reject soft UVC recovery");
|
|
assert_eq!(uvc.code(), tonic::Code::FailedPrecondition);
|
|
assert!(
|
|
uvc.to_string().contains("hdmi"),
|
|
"unexpected UVC recovery error: {uvc}"
|
|
);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(coverage)]
|
|
#[serial]
|
|
fn recover_soft_helpers_cover_usb_state_failure_and_uvc_success() {
|
|
let (_dir, handler) = build_handler_for_tests();
|
|
let rt = tokio::runtime::Runtime::new().expect("runtime");
|
|
|
|
let usb = rt
|
|
.block_on(async { handler.recover_usb_reply().await })
|
|
.expect_err("missing fake UDC state should be reported clearly");
|
|
assert_eq!(usb.code(), tonic::Code::FailedPrecondition);
|
|
assert!(usb.to_string().contains("could not read UDC state"));
|
|
|
|
let usb_rpc = rt
|
|
.block_on(async { handler.recover_usb(tonic::Request::new(Empty {})).await })
|
|
.expect_err("RPC wrapper should surface the same missing fake UDC state");
|
|
assert_eq!(usb_rpc.code(), tonic::Code::FailedPrecondition);
|
|
assert!(usb_rpc.to_string().contains("could not read UDC state"));
|
|
|
|
with_var("LESAVKA_CAM_OUTPUT", Some("uvc"), || {
|
|
let uvc = rt
|
|
.block_on(async { handler.recover_uvc_reply().await })
|
|
.expect("UVC soft recovery should retire the active relay")
|
|
.into_inner();
|
|
assert!(uvc.ok);
|
|
});
|
|
}
|
|
}
|