//! RPC reset coverage for server main USB recovery replies. //! //! Scope: include `server/src/main.rs` and exercise reset RPC edge replies. //! Targets: `server/src/main.rs`. //! Why: USB reset is an operator recovery path, so failed HID reopen behavior //! needs deterministic coverage without requiring real gadget hardware. #[allow(warnings)] mod server_main_rpc_reset { include!(env!("LESAVKA_SERVER_MAIN_SRC")); use serial_test::serial; use temp_env::with_var; use tempfile::tempdir; fn build_handler_for_tests() -> (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(true) .create(true) .truncate(true) .open(&kb_path) .expect("open kb"), ); let ms = tokio::fs::File::from_std( std::fs::OpenOptions::new() .read(true) .write(true) .create(true) .truncate(true) .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()), capture_power: CapturePowerManager::new(), eye_hubs: std::sync::Arc::new( tokio::sync::Mutex::new(std::collections::HashMap::new()), ), }); (dir, handler) } #[test] #[cfg(coverage)] #[serial] fn reset_usb_tolerates_missing_hid_after_successful_cycle() { let dir = tempdir().expect("tempdir"); std::fs::write(dir.path().join("hidg0.bin"), "").expect("create kb file"); std::fs::write(dir.path().join("hidg1.bin"), "").expect("create ms file"); std::fs::create_dir_all(dir.path().join("sys/class/udc/fake-ctrl.usb")) .expect("create udc dir"); std::fs::create_dir_all(dir.path().join("cfg/lesavka")).expect("create cfg dir"); std::fs::write( dir.path().join("sys/class/udc/fake-ctrl.usb/state"), "configured\n", ) .expect("write state"); std::fs::write(dir.path().join("cfg/lesavka/UDC"), "fake-ctrl.usb\n").expect("write udc"); let kb = tokio::fs::File::from_std( std::fs::OpenOptions::new() .read(true) .write(true) .open(dir.path().join("hidg0.bin")) .expect("open kb"), ); let ms = tokio::fs::File::from_std( std::fs::OpenOptions::new() .read(true) .write(true) .open(dir.path().join("hidg1.bin")) .expect("open ms"), ); with_var( "LESAVKA_GADGET_SYSFS_ROOT", Some(dir.path().join("sys").to_string_lossy().to_string()), || { with_var( "LESAVKA_GADGET_CONFIGFS_ROOT", Some(dir.path().join("cfg").to_string_lossy().to_string()), || { let handler = 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()), capture_power: CapturePowerManager::new(), eye_hubs: std::sync::Arc::new(tokio::sync::Mutex::new( std::collections::HashMap::new(), )), }; with_var( "LESAVKA_HID_DIR", Some(dir.path().join("missing").to_string_lossy().to_string()), || { let rt = tokio::runtime::Runtime::new().expect("runtime"); let reply = rt .block_on(async { handler.reset_usb(tonic::Request::new(Empty {})).await }) .expect("missing HID should not fail USB reset") .into_inner(); assert!(reply.ok); }, ); }, ); }, ); } #[test] #[cfg(coverage)] #[serial] fn reset_usb_reports_reopen_hid_failure_after_successful_cycle() { let dir = tempdir().expect("tempdir"); std::fs::create_dir_all(dir.path().join("bad-hid/hidg0")).expect("create bad hidg0 dir"); std::fs::write(dir.path().join("bad-hid/hidg1"), "").expect("create hidg1 file"); std::fs::create_dir_all(dir.path().join("sys/class/udc/fake-ctrl.usb")) .expect("create udc dir"); std::fs::create_dir_all(dir.path().join("cfg/lesavka")).expect("create cfg dir"); std::fs::write( dir.path().join("sys/class/udc/fake-ctrl.usb/state"), "configured\n", ) .expect("write state"); std::fs::write(dir.path().join("cfg/lesavka/UDC"), "fake-ctrl.usb\n").expect("write udc"); let (_hid_dir, handler) = build_handler_for_tests(); with_var( "LESAVKA_GADGET_SYSFS_ROOT", Some(dir.path().join("sys").to_string_lossy().to_string()), || { with_var( "LESAVKA_GADGET_CONFIGFS_ROOT", Some(dir.path().join("cfg").to_string_lossy().to_string()), || { with_var( "LESAVKA_HID_DIR", Some(dir.path().join("bad-hid").to_string_lossy().to_string()), || { let rt = tokio::runtime::Runtime::new().expect("runtime"); let err = rt .block_on(async { handler.reset_usb(tonic::Request::new(Empty {})).await }) .expect_err("bad HID path should fail reopen"); assert_eq!(err.code(), tonic::Code::Internal); assert!( err.message().contains("opening") && err.message().contains("bad-hid/hidg0"), "unexpected reopen error: {}", err.message() ); }, ); }, ); }, ); } }