//! Extra integration coverage for server main HID startup branches. //! //! Scope: include `server/src/main.rs` and exercise successful handler startup //! with synthetic HID endpoints. //! Targets: `server/src/main.rs`. //! Why: the main contract file is near the 500 LOC cap, so additional branch //! coverage lives here. #[allow(warnings)] mod server_main_binary_extra { include!(env!("LESAVKA_SERVER_MAIN_SRC")); use futures_util::stream; use lesavka_common::lesavka::relay_client::RelayClient; use serial_test::serial; use std::path::Path; use temp_env::with_var; use tempfile::tempdir; 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"); } fn write_file(path: &Path, content: &str) { if let Some(parent) = path.parent() { std::fs::create_dir_all(parent).expect("create parent"); } std::fs::write(path, content).expect("write file"); } fn with_fake_gadget_roots(sys_root: &Path, cfg_root: &Path, f: impl FnOnce()) { let sys_root = sys_root.to_string_lossy().to_string(); let cfg_root = cfg_root.to_string_lossy().to_string(); with_var("LESAVKA_GADGET_SYSFS_ROOT", Some(sys_root), || { with_var("LESAVKA_GADGET_CONFIGFS_ROOT", Some(cfg_root), f); }); } fn build_fake_gadget_tree(base: &Path, ctrl: &str, gadget_name: &str, state: &str) { write_file( &base.join(format!("sys/class/udc/{ctrl}/state")), &format!("{state}\n"), ); write_file( &base.join(format!("cfg/{gadget_name}/UDC")), &format!("{ctrl}\n"), ); } 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) } #[test] #[serial] fn handler_new_and_reopen_hid_succeed_with_override_paths() { let dir = tempdir().expect("tempdir"); std::fs::write(dir.path().join("hidg0"), "").expect("create hidg0"); std::fs::write(dir.path().join("hidg1"), "").expect("create hidg1"); let hid_dir = dir.path().to_string_lossy().to_string(); with_var("LESAVKA_HID_DIR", Some(hid_dir), || { with_var("LESAVKA_ALLOW_GADGET_CYCLE", None::<&str>, || { let rt = tokio::runtime::Runtime::new().expect("runtime"); rt.block_on(async { let handler = Handler::new(UsbGadget::new("lesavka")) .await .expect("handler startup"); handler.reopen_hid().await.expect("reopen hid"); }); }); }); } #[test] #[serial] fn handler_new_with_cycle_enabled_can_still_open_override_paths() { let dir = tempdir().expect("tempdir"); std::fs::write(dir.path().join("hidg0"), "").expect("create hidg0"); std::fs::write(dir.path().join("hidg1"), "").expect("create hidg1"); let hid_dir = dir.path().to_string_lossy().to_string(); with_var("LESAVKA_HID_DIR", Some(hid_dir), || { with_var("LESAVKA_ALLOW_GADGET_CYCLE", Some("1"), || { let rt = tokio::runtime::Runtime::new().expect("runtime"); rt.block_on(async { let _handler = Handler::new(UsbGadget::new("lesavka")) .await .expect("handler startup with cycle enabled"); }); }); }); } #[test] #[serial] fn stream_keyboard_writes_reports_to_hid_file() { let rt = tokio::runtime::Runtime::new().expect("runtime"); rt.block_on(async { let (dir, handler) = build_handler_for_tests(); let kb_path = dir.path().join("hidg0.bin"); let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind"); let addr = listener.local_addr().expect("addr"); drop(listener); let server = tokio::spawn(async move { let _ = tonic::transport::Server::builder() .add_service(RelayServer::new(handler)) .serve(addr) .await; }); let channel = connect_with_retry(addr).await; let mut cli = RelayClient::new(channel); let (tx, rx) = tokio::sync::mpsc::channel(4); tx.send(KeyboardReport { data: vec![1, 2, 3, 4, 5, 6, 7, 8], }) .await .expect("send keyboard packet"); drop(tx); let outbound = tokio_stream::wrappers::ReceiverStream::new(rx); let mut resp = cli .stream_keyboard(tonic::Request::new(outbound)) .await .expect("stream keyboard"); let echoed = resp .get_mut() .message() .await .expect("grpc result") .expect("echo packet"); assert_eq!(echoed.data, vec![1, 2, 3, 4, 5, 6, 7, 8]); tokio::time::sleep(std::time::Duration::from_millis(30)).await; let written = std::fs::read(&kb_path).expect("read hidg0 file"); assert!( !written.is_empty(), "keyboard stream should write HID bytes to target file" ); server.abort(); }); } #[test] #[serial] fn stream_mouse_writes_reports_to_hid_file() { let rt = tokio::runtime::Runtime::new().expect("runtime"); rt.block_on(async { let (dir, handler) = build_handler_for_tests(); let ms_path = dir.path().join("hidg1.bin"); let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind"); let addr = listener.local_addr().expect("addr"); drop(listener); let server = tokio::spawn(async move { let _ = tonic::transport::Server::builder() .add_service(RelayServer::new(handler)) .serve(addr) .await; }); let channel = connect_with_retry(addr).await; let mut cli = RelayClient::new(channel); let (tx, rx) = tokio::sync::mpsc::channel(4); tx.send(MouseReport { data: vec![8, 7, 6, 5, 4, 3, 2, 1], }) .await .expect("send mouse packet"); drop(tx); let outbound = tokio_stream::wrappers::ReceiverStream::new(rx); let mut resp = cli .stream_mouse(tonic::Request::new(outbound)) .await .expect("stream mouse"); let echoed = resp .get_mut() .message() .await .expect("grpc result") .expect("echo packet"); assert_eq!(echoed.data, vec![8, 7, 6, 5, 4, 3, 2, 1]); tokio::time::sleep(std::time::Duration::from_millis(30)).await; let written = std::fs::read(&ms_path).expect("read hidg1 file"); assert!( !written.is_empty(), "mouse stream should write HID bytes to target file" ); server.abort(); }); } #[test] #[serial] fn stream_keyboard_recovers_when_hid_write_fails() { let rt = tokio::runtime::Runtime::new().expect("runtime"); rt.block_on(async { let (_dir, handler) = build_handler_for_tests_with_modes(false, true); let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind"); let addr = listener.local_addr().expect("addr"); drop(listener); let server = tokio::spawn(async move { let _ = tonic::transport::Server::builder() .add_service(RelayServer::new(handler)) .serve(addr) .await; }); let channel = connect_with_retry(addr).await; let mut cli = RelayClient::new(channel); let (tx, rx) = tokio::sync::mpsc::channel(4); tx.send(KeyboardReport { data: vec![11, 12, 13, 14, 15, 16, 17, 18], }) .await .expect("send keyboard packet"); drop(tx); let outbound = tokio_stream::wrappers::ReceiverStream::new(rx); let mut resp = cli .stream_keyboard(tonic::Request::new(outbound)) .await .expect("stream keyboard"); let echoed = resp .get_mut() .message() .await .expect("grpc result") .expect("echo packet"); assert_eq!(echoed.data, vec![11, 12, 13, 14, 15, 16, 17, 18]); server.abort(); }); } #[test] #[serial] fn stream_mouse_recovers_when_hid_write_fails() { let rt = tokio::runtime::Runtime::new().expect("runtime"); rt.block_on(async { let (_dir, handler) = build_handler_for_tests_with_modes(true, false); let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind"); let addr = listener.local_addr().expect("addr"); drop(listener); let server = tokio::spawn(async move { let _ = tonic::transport::Server::builder() .add_service(RelayServer::new(handler)) .serve(addr) .await; }); let channel = connect_with_retry(addr).await; let mut cli = RelayClient::new(channel); let (tx, rx) = tokio::sync::mpsc::channel(4); tx.send(MouseReport { data: vec![21, 22, 23, 24, 25, 26, 27, 28], }) .await .expect("send mouse packet"); drop(tx); let outbound = tokio_stream::wrappers::ReceiverStream::new(rx); let mut resp = cli .stream_mouse(tonic::Request::new(outbound)) .await .expect("stream mouse"); let echoed = resp .get_mut() .message() .await .expect("grpc result") .expect("echo packet"); assert_eq!(echoed.data, vec![21, 22, 23, 24, 25, 26, 27, 28]); server.abort(); }); } #[test] #[serial] fn stream_microphone_returns_internal_error_without_uac_device() { let rt = tokio::runtime::Runtime::new().expect("runtime"); rt.block_on(async { let (_dir, handler) = build_handler_for_tests(); let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind"); let addr = listener.local_addr().expect("addr"); drop(listener); let server = tokio::spawn(async move { let _ = tonic::transport::Server::builder() .add_service(RelayServer::new(handler)) .serve(addr) .await; }); let channel = connect_with_retry(addr).await; let mut cli = RelayClient::new(channel); let (_tx, rx) = tokio::sync::mpsc::channel::(4); let outbound = tokio_stream::wrappers::ReceiverStream::new(rx); let err = cli .stream_microphone(tonic::Request::new(outbound)) .await .expect_err("missing UAC sink should fail stream setup"); assert_eq!(err.code(), tonic::Code::Internal); server.abort(); }); } #[test] #[serial] fn stream_camera_reports_error_or_terminates_cleanly_without_camera_hardware() { let rt = tokio::runtime::Runtime::new().expect("runtime"); rt.block_on(async { let (_dir, handler) = build_handler_for_tests(); let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind"); let addr = listener.local_addr().expect("addr"); drop(listener); let server = tokio::spawn(async move { let _ = tonic::transport::Server::builder() .add_service(RelayServer::new(handler)) .serve(addr) .await; }); let channel = connect_with_retry(addr).await; let mut cli = RelayClient::new(channel); let (tx, rx) = tokio::sync::mpsc::channel(4); tx.send(VideoPacket { id: 2, pts: 1, data: vec![0, 1, 2, 3], }) .await .expect("send camera packet"); drop(tx); let outbound = tokio_stream::wrappers::ReceiverStream::new(rx); let result = cli.stream_camera(tonic::Request::new(outbound)).await; match result { Ok(mut stream) => { let _ = stream.get_mut().message().await; } Err(err) => { assert!( matches!( err.code(), tonic::Code::Internal | tonic::Code::Unavailable | tonic::Code::Unknown ), "unexpected camera stream error code: {}", err.code() ); } } server.abort(); }); } #[test] #[serial] fn reset_usb_succeeds_with_fake_cycle_and_override_hid_paths() { let dir = tempdir().expect("tempdir"); let hid_dir = dir.path().join("hid"); std::fs::create_dir_all(&hid_dir).expect("create hid dir"); std::fs::write(hid_dir.join("hidg0"), "").expect("create hidg0"); std::fs::write(hid_dir.join("hidg1"), "").expect("create hidg1"); build_fake_gadget_tree(dir.path(), "fake-ctrl.usb", "lesavka", "configured"); with_fake_gadget_roots(&dir.path().join("sys"), &dir.path().join("cfg"), || { with_var( "LESAVKA_HID_DIR", Some(hid_dir.to_string_lossy().to_string()), || { let kb = tokio::fs::File::from_std( std::fs::OpenOptions::new() .read(true) .write(true) .open(hid_dir.join("hidg0")) .expect("open hidg0"), ); let ms = tokio::fs::File::from_std( std::fs::OpenOptions::new() .read(true) .write(true) .open(hid_dir.join("hidg1")) .expect("open hidg1"), ); let handler = 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(), }; let rt = tokio::runtime::Runtime::new().expect("runtime"); let reply = rt .block_on(async { handler.reset_usb(tonic::Request::new(Empty {})).await }) .expect("reset usb should succeed on fake gadget tree") .into_inner(); assert!(reply.ok); }, ); }); } #[test] fn guarded_video_stream_forwards_inner_packets() { let rt = tokio::runtime::Runtime::new().expect("runtime"); rt.block_on(async { let lease = CapturePowerManager::new().acquire().await; let packet = VideoPacket { id: 2, pts: 42, data: vec![9, 8, 7], }; let mut guarded = GuardedVideoStream { inner: stream::iter(vec![Ok(packet.clone())]), _lease: lease, }; let observed = guarded .next() .await .expect("guarded stream item") .expect("packet"); assert_eq!(observed.id, packet.id); assert_eq!(observed.pts, packet.pts); assert_eq!(observed.data, packet.data); assert!(guarded.next().await.is_none()); }); } }