lesavka/testing/tests/server_main_rpc_contract.rs

523 lines
20 KiB
Rust
Raw Normal View History

//! Integration coverage for server main RPC handler branches.
//!
//! Scope: include `server/src/main.rs` and exercise additional RPC paths that
//! are awkward to hit from process-level tests.
//! Targets: `server/src/main.rs`.
//! Why: keep handler-side error/reply behavior stable without HID hardware.
#[allow(warnings)]
mod server_main_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"),
);
2026-04-21 20:19:47 -03:00
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()),
2026-04-30 08:16:57 -03:00
calibration: std::sync::Arc::new(CalibrationStore::load(std::sync::Arc::new(
UpstreamMediaRuntime::new(),
))),
2026-04-21 20:19:47 -03:00
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]
#[serial]
2026-04-21 13:31:49 -03:00
fn reopen_hid_tolerates_missing_hid_endpoints() {
let (_dir, handler) = build_handler_for_tests();
2026-04-21 13:31:49 -03:00
let missing_dir = tempdir().expect("missing hid dir");
let hid_dir = missing_dir.path().join("missing");
with_var(
"LESAVKA_HID_DIR",
Some(hid_dir.to_string_lossy().to_string()),
|| {
let rt = tokio::runtime::Runtime::new().expect("runtime");
let result = rt.block_on(handler.reopen_hid());
assert!(
result.is_ok(),
"reopen_hid should keep the server alive while HID endpoints are absent"
);
let endpoints = rt.block_on(async {
(
handler.kb.lock().await.is_none(),
handler.ms.lock().await.is_none(),
)
});
assert_eq!(endpoints, (true, true));
},
);
}
#[test]
#[serial]
fn capture_video_valid_monitor_surfaces_internal_error_without_device() {
let (_dir, handler) = build_handler_for_tests();
let rt = tokio::runtime::Runtime::new().expect("runtime");
let result = rt.block_on(async {
handler
.capture_video(tonic::Request::new(MonitorRequest {
id: 0,
max_bitrate: 3_000,
requested_width: 0,
requested_height: 0,
requested_fps: 0,
source_id: None,
}))
.await
});
let err = match result {
Ok(_) => panic!("missing camera device should fail"),
Err(err) => err,
};
assert_eq!(err.code(), tonic::Code::Internal);
}
#[test]
#[serial]
fn capture_video_right_eye_surfaces_internal_error_without_device() {
let (_dir, handler) = build_handler_for_tests();
let rt = tokio::runtime::Runtime::new().expect("runtime");
let result = rt.block_on(async {
handler
.capture_video(tonic::Request::new(MonitorRequest {
id: 1,
max_bitrate: 3_000,
requested_width: 0,
requested_height: 0,
requested_fps: 0,
source_id: None,
}))
.await
});
let err = match result {
Ok(_) => panic!("missing right-eye camera device should fail"),
Err(err) => err,
};
assert_eq!(err.code(), tonic::Code::Internal);
}
#[test]
#[cfg(coverage)]
#[serial]
fn capture_video_returns_stream_when_coverage_source_is_overridden() {
let (_dir, handler) = build_handler_for_tests();
let rt = tokio::runtime::Runtime::new().expect("runtime");
with_var(
"LESAVKA_TEST_VIDEO_SOURCE",
Some("/dev/lesavka_l_eye"),
|| {
let mut stream = rt
.block_on(async {
handler
.capture_video(tonic::Request::new(MonitorRequest {
id: 0,
max_bitrate: 3_000,
requested_width: 0,
requested_height: 0,
requested_fps: 0,
source_id: None,
}))
.await
})
.expect("coverage video stream should succeed")
.into_inner();
let packet = rt
.block_on(async { stream.next().await })
.expect("stream item")
.expect("packet");
assert_eq!(packet.id, 0);
assert!(!packet.data.is_empty());
2026-04-22 00:56:03 -03:00
drop(stream);
rt.block_on(async {
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
});
},
);
}
#[test]
#[cfg(coverage)]
#[serial]
fn capture_video_stream_drop_releases_shared_hub() {
let (_dir, handler) = build_handler_for_tests();
let rt = tokio::runtime::Runtime::new().expect("runtime");
with_var(
"LESAVKA_TEST_VIDEO_SOURCE",
Some("/dev/lesavka_l_eye"),
|| {
rt.block_on(async {
let mut stream = handler
.capture_video(tonic::Request::new(MonitorRequest {
id: 0,
max_bitrate: 3_000,
requested_width: 0,
requested_height: 0,
requested_fps: 0,
source_id: None,
}))
.await
.expect("coverage video stream")
.into_inner();
let packet = stream.next().await.expect("first item").expect("packet");
assert_eq!(packet.id, 0);
drop(stream);
let hub = handler
.eye_hubs
.lock()
.await
.values()
.next()
.cloned()
.expect("active preview hub");
let _ = hub.tx.send(VideoPacket {
id: 0,
pts: 2,
data: vec![0, 0, 0, 1, 0x65],
..Default::default()
});
for _ in 0..40 {
if handler.active_eye_source_count().await == 0 {
return;
}
tokio::time::sleep(std::time::Duration::from_millis(25)).await;
}
panic!("dropping a preview stream should release its shared hub");
});
},
);
}
#[test]
#[cfg(coverage)]
#[serial]
fn mirrored_capture_requests_share_one_source_hub_but_keep_logical_ids() {
let (_dir, handler) = build_handler_for_tests();
let rt = tokio::runtime::Runtime::new().expect("runtime");
with_var(
"LESAVKA_TEST_VIDEO_SOURCE",
Some("/dev/lesavka_r_eye"),
|| {
let (left_packet, right_packet, hub_count) = rt.block_on(async {
let mut left = handler
.capture_video(tonic::Request::new(MonitorRequest {
id: 0,
max_bitrate: 3_000,
requested_width: 1920,
requested_height: 1080,
requested_fps: 60,
source_id: Some(1),
}))
.await
.expect("left stream")
.into_inner();
let mut right = handler
.capture_video(tonic::Request::new(MonitorRequest {
id: 1,
max_bitrate: 3_000,
requested_width: 1920,
requested_height: 1080,
requested_fps: 60,
source_id: Some(1),
}))
.await
.expect("right stream")
.into_inner();
let left_packet = left.next().await.expect("left item").expect("left packet");
let right_packet = right
.next()
.await
.expect("right item")
.expect("right packet");
2026-04-22 00:56:03 -03:00
drop(left);
drop(right);
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
let hub_count = handler.eye_hub_count().await;
(left_packet, right_packet, hub_count)
});
assert_eq!(left_packet.id, 0);
assert_eq!(right_packet.id, 1);
assert!(!left_packet.data.is_empty());
assert!(!right_packet.data.is_empty());
assert_eq!(hub_count, 1);
},
);
}
#[test]
#[serial]
fn paste_text_accepts_encrypted_payload_and_returns_reply() {
let (_dir, handler) = build_handler_for_tests();
with_var(
"LESAVKA_PASTE_KEY",
Some("hex:00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"),
|| {
with_var("LESAVKA_PASTE_DELAY_MS", Some("0"), || {
let req =
lesavka_client::paste::build_paste_request("hello").expect("build request");
let rt = tokio::runtime::Runtime::new().expect("runtime");
let reply = rt
.block_on(async { handler.paste_text(tonic::Request::new(req)).await })
.expect("paste rpc should return reply")
.into_inner();
assert!(
reply.ok || !reply.error.is_empty(),
"paste path should execute and return a structured reply"
);
});
},
);
}
#[test]
#[serial]
fn paste_text_returns_structured_error_when_hid_write_fails() {
let (_dir, handler) = build_handler_for_tests_with_modes(false, true);
with_var(
"LESAVKA_PASTE_KEY",
Some("hex:00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"),
|| {
with_var("LESAVKA_PASTE_DELAY_MS", Some("0"), || {
let req =
lesavka_client::paste::build_paste_request("hello").expect("build request");
let rt = tokio::runtime::Runtime::new().expect("runtime");
let reply = rt
.block_on(async { handler.paste_text(tonic::Request::new(req)).await })
.expect("paste rpc should return structured reply")
.into_inner();
assert!(!reply.ok);
assert!(!reply.error.is_empty());
});
},
);
}
#[test]
#[serial]
fn capture_audio_accepts_secondary_monitor_id_and_fails_internally_without_sink() {
let (_dir, handler) = build_handler_for_tests();
let req = MonitorRequest {
id: 1,
max_bitrate: 0,
requested_width: 0,
requested_height: 0,
requested_fps: 0,
source_id: None,
};
let rt = tokio::runtime::Runtime::new().expect("runtime");
let result = rt.block_on(async { handler.capture_audio(tonic::Request::new(req)).await });
let err = match result {
Ok(_) => panic!("missing ALSA source should fail"),
Err(err) => err,
};
assert_eq!(err.code(), tonic::Code::Internal);
}
#[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");
}
2026-04-30 08:16:57 -03:00
#[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");
assert_eq!(initial.active_audio_offset_us, 0);
2026-04-30 08:16:57 -03:00
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);
2026-05-02 10:51:49 -03:00
assert_eq!(adjusted.active_video_offset_us, 132_000);
2026-04-30 08:16:57 -03:00
assert!(
std::fs::read_to_string(calibration_path)
.expect("persisted")
.contains("active_audio_offset_us=-35000")
);
},
);
}
#[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);
}
}