//! 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"), ); 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()), 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] fn reopen_hid_tolerates_missing_hid_endpoints() { let (_dir, handler) = build_handler_for_tests(); 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()); 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"); 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"); } }