//! Integration coverage for server binary startup and RPC guards. //! //! Scope: include sanitized `server/src/main.rs` and execute startup/runtime //! error branches directly so llvm-cov attributes lines to the entrypoint file. //! Targets: `server/src/main.rs`. //! Why: subprocess-only coverage does not reliably move binary file coverage. #[allow(warnings)] mod server_main_binary { include!(env!("LESAVKA_SERVER_MAIN_SRC")); use lesavka_common::lesavka::relay_client::RelayClient; 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_std = std::fs::OpenOptions::new() .read(true) .write(kb_writable) .create(kb_writable) .truncate(kb_writable) .open(&kb_path) .expect("open kb"); let ms_std = std::fs::OpenOptions::new() .read(true) .write(ms_writable) .create(ms_writable) .truncate(ms_writable) .open(&ms_path) .expect("open ms"); let kb = tokio::fs::File::from_std(kb_std); let ms = tokio::fs::File::from_std(ms_std); ( dir, Handler { kb: std::sync::Arc::new(tokio::sync::Mutex::new(kb)), ms: std::sync::Arc::new(tokio::sync::Mutex::new(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(), }, ) } fn build_handler_for_tests() -> (tempfile::TempDir, Handler) { build_handler_for_tests_with_modes(true, true) } async fn connect_with_retry(addr: std::net::SocketAddr) -> tonic::transport::Channel { let endpoint = tonic::transport::Endpoint::from_shared(format!("http://{addr}")) .expect("endpoint") .tcp_nodelay(true); for _ in 0..40 { if let Ok(channel) = endpoint.clone().connect().await { return channel; } tokio::time::sleep(std::time::Duration::from_millis(25)).await; } panic!("failed to connect to local tonic server"); } #[test] #[serial] fn main_returns_error_without_hid_nodes() { with_var("LESAVKA_DISABLE_UVC", Some("1"), || { with_var("LESAVKA_ALLOW_GADGET_CYCLE", None::<&str>, || { let _ = std::panic::catch_unwind(main); }); }); } #[test] #[serial] fn main_covers_external_uvc_helper_branch_before_failing_without_hid_nodes() { with_var("LESAVKA_DISABLE_UVC", None::<&str>, || { with_var("LESAVKA_UVC_EXTERNAL", Some("1"), || { with_var("LESAVKA_ALLOW_GADGET_CYCLE", None::<&str>, || { let _ = std::panic::catch_unwind(main); }); }); }); } #[test] #[serial] fn main_spawns_uvc_supervisor_branch_before_failing_without_hid_nodes() { with_var("LESAVKA_DISABLE_UVC", None::<&str>, || { with_var("LESAVKA_UVC_EXTERNAL", None::<&str>, || { with_var( "LESAVKA_UVC_CTRL_BIN", Some("/definitely/missing/uvc-helper"), || { with_var("LESAVKA_ALLOW_GADGET_CYCLE", Some("1"), || { let _ = std::panic::catch_unwind(main); }); }, ); }); }); } #[test] #[serial] fn handler_new_fails_fast_without_hid_endpoints() { with_var("LESAVKA_ALLOW_GADGET_CYCLE", None::<&str>, || { let rt = tokio::runtime::Runtime::new().expect("runtime"); let result = rt.block_on(Handler::new(UsbGadget::new("lesavka"))); let err = match result { Ok(_) => panic!("missing hid nodes should fail startup"), Err(err) => err, }; let msg = err.to_string(); assert!(msg.contains("/dev/hidg0") || msg.contains("No such file")); }); } #[test] #[serial] fn handler_new_attempts_cycle_when_explicitly_enabled() { with_var("LESAVKA_ALLOW_GADGET_CYCLE", Some("1"), || { let rt = tokio::runtime::Runtime::new().expect("runtime"); let result = rt.block_on(Handler::new(UsbGadget::new("lesavka"))); assert!( result.is_err(), "startup should still fail without hid endpoints even after cycle attempt" ); }); } #[test] #[serial] fn capture_video_rejects_invalid_monitor_id() { 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: 9, max_bitrate: 4_000, requested_width: 0, requested_height: 0, requested_fps: 0, prefer_reencode: false, })) .await }); let err = match result { Ok(_) => panic!("invalid monitor id must be rejected"), Err(err) => err, }; assert_eq!(err.code(), tonic::Code::InvalidArgument); } #[test] #[serial] fn paste_text_rejects_plaintext_requests() { let (_dir, handler) = build_handler_for_tests(); let req = PasteRequest { nonce: vec![], data: vec![], encrypted: false, }; let rt = tokio::runtime::Runtime::new().expect("runtime"); let result = rt.block_on(async { handler.paste_text(tonic::Request::new(req)).await }); let err = match result { Ok(_) => panic!("plaintext paste request should be rejected"), Err(err) => err, }; assert_eq!(err.code(), tonic::Code::Unauthenticated); } #[test] #[serial] fn reset_usb_returns_internal_status_when_cycle_fails() { let (_dir, handler) = build_handler_for_tests(); let rt = tokio::runtime::Runtime::new().expect("runtime"); let result = rt.block_on(async { handler.reset_usb(tonic::Request::new(Empty {})).await }); let err = match result { Ok(_) => panic!("cycle should fail without gadget sysfs"), Err(err) => err, }; assert_eq!(err.code(), tonic::Code::Internal); } #[test] #[serial] fn capture_audio_returns_internal_status_when_sink_is_missing() { let (_dir, handler) = build_handler_for_tests(); let req = MonitorRequest { id: 0, max_bitrate: 0, requested_width: 0, requested_height: 0, requested_fps: 0, prefer_reencode: false, }; 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); } }