//! Include-based coverage for USB gadget orchestration helpers. //! //! Scope: include `server/src/gadget.rs` and exercise deterministic helper //! logic and error branches without touching live gadget sysfs state. //! Targets: `server/src/gadget.rs`. //! Why: most gadget logic is file-system and errno handling that should remain //! stable regardless of host environment. #[allow(warnings)] mod gadget_include_contract { include!(env!("LESAVKA_SERVER_GADGET_SRC")); use serial_test::serial; use temp_env::with_var; use tempfile::{NamedTempFile, tempdir}; 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_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_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!("sys/class/udc/{ctrl}/soft_connect")), "1\n", ); write_file( &base.join("sys/bus/platform/drivers/dwc2/unbind"), "placeholder\n", ); write_file( &base.join("sys/bus/platform/drivers/dwc2/bind"), "placeholder\n", ); write_file( &base.join(format!("cfg/{gadget_name}/UDC")), &format!("{ctrl}\n"), ); } #[test] fn new_builds_expected_udc_path() { let gadget = UsbGadget::new("lesavka-test"); assert!(gadget.udc_file.ends_with("/lesavka-test/UDC")); } #[test] fn state_errors_for_missing_controller() { let result = UsbGadget::state("definitely-missing-udc"); assert!(result.is_err()); } #[test] fn host_attached_state_matches_udc_states_that_can_accept_hid() { for state in ["configured", "addressed", "default", "suspended", "unknown"] { assert!(UsbGadget::host_attached_state(state), "{state}"); } assert!(!UsbGadget::host_attached_state("not attached")); assert!(!UsbGadget::host_attached_state("broken")); } #[test] #[serial] fn current_controller_state_reads_fake_udc_tree() { let dir = tempdir().expect("tempdir"); let ctrl = "fake-ctrl.usb"; build_fake_tree(dir.path(), ctrl, "lesavka-test", "configured"); with_fake_roots(&dir.path().join("sys"), &dir.path().join("cfg"), || { let (found_ctrl, state) = UsbGadget::current_controller_state().expect("current state"); assert_eq!(found_ctrl, ctrl); assert_eq!(state, "configured"); }); } #[test] fn wait_state_any_times_out_for_missing_controller() { let result = UsbGadget::wait_state_any("definitely-missing-udc", 0); assert!(result.is_err()); } #[test] fn wait_state_times_out_for_missing_controller() { let result = UsbGadget::wait_state("definitely-missing-udc", "configured", 0); assert!(result.is_err()); } #[test] fn write_attr_writes_value_with_trailing_newline() { let file = NamedTempFile::new().expect("temp file"); UsbGadget::write_attr(file.path(), "configured").expect("write attr"); let content = std::fs::read_to_string(file.path()).expect("read back"); assert_eq!(content, "configured\n"); } #[test] fn wait_udc_present_times_out_for_missing_path() { let result = UsbGadget::wait_udc_present("definitely-missing-udc", 0); assert!(result.is_err()); } #[test] fn probe_platform_udc_is_non_panicking() { let _ = UsbGadget::probe_platform_udc(); } #[test] fn find_controller_returns_name_or_error_without_panicking() { let result = UsbGadget::find_controller(); if let Ok(name) = result { assert!(!name.is_empty()); } } #[test] fn is_still_detaching_matches_expected_errno_set() { let busy = anyhow::Error::from(std::io::Error::from_raw_os_error(libc::EBUSY)); let missing = anyhow::Error::from(std::io::Error::from_raw_os_error(libc::ENOENT)); let no_dev = anyhow::Error::from(std::io::Error::from_raw_os_error(libc::ENODEV)); let other = anyhow::Error::from(std::io::Error::from_raw_os_error(libc::EACCES)); let non_io = anyhow::anyhow!("plain error"); assert!(UsbGadget::is_still_detaching(&busy)); assert!(UsbGadget::is_still_detaching(&missing)); assert!(UsbGadget::is_still_detaching(&no_dev)); assert!(!UsbGadget::is_still_detaching(&other)); assert!(!UsbGadget::is_still_detaching(&non_io)); } #[test] fn rebind_driver_errors_when_driver_nodes_are_absent() { let dir = tempdir().expect("tempdir"); with_fake_roots(&dir.path().join("sys"), &dir.path().join("cfg"), || { let result = UsbGadget::rebind_driver("definitely-missing-udc"); assert!(result.is_err()); }); } #[test] #[serial] fn cycle_handles_missing_gadget_sysfs_gracefully() { let gadget = UsbGadget::new("lesavka-test"); with_var("LESAVKA_GADGET_FORCE_CYCLE", None::<&str>, || { let result = gadget.cycle(); assert!(result.is_err() || result.is_ok()); }); } #[test] #[serial] fn cycle_force_mode_still_returns_without_panicking() { let dir = tempdir().expect("tempdir"); let fake_udc = dir.path().join("UDC"); std::fs::write(&fake_udc, "").expect("create fake udc file"); let gadget = UsbGadget { udc_file: Box::leak(fake_udc.to_string_lossy().to_string().into_boxed_str()), }; with_var("LESAVKA_GADGET_FORCE_CYCLE", Some("1"), || { let result = gadget.cycle(); assert!(result.is_err() || result.is_ok()); }); } #[test] #[serial] fn cycle_short_circuits_when_host_is_attached_without_force() { let dir = tempdir().expect("tempdir"); let ctrl = "fake-ctrl.usb"; build_fake_tree(dir.path(), ctrl, "lesavka-test", "configured"); with_fake_roots(&dir.path().join("sys"), &dir.path().join("cfg"), || { let gadget = UsbGadget::new("lesavka-test"); let result = gadget.cycle(); assert!( result.is_ok(), "configured host state should short-circuit safely" ); }); } #[test] #[serial] fn cycle_force_mode_completes_on_fake_tree_when_state_stays_not_attached() { let dir = tempdir().expect("tempdir"); let ctrl = "fake-ctrl.usb"; build_fake_tree(dir.path(), ctrl, "lesavka-test", "not attached"); with_fake_roots(&dir.path().join("sys"), &dir.path().join("cfg"), || { with_var("LESAVKA_GADGET_FORCE_CYCLE", Some("1"), || { let gadget = UsbGadget::new("lesavka-test"); let result = gadget.cycle(); assert!( result.is_ok(), "force cycle should complete on fake sysfs tree" ); let udc_path = dir.path().join("cfg/lesavka-test/UDC"); let value = std::fs::read_to_string(udc_path).expect("read udc file"); assert_eq!(value.trim(), ctrl); }); }); } #[test] #[serial] fn cycle_force_mode_accepts_late_configured_transition() { let dir = tempdir().expect("tempdir"); let ctrl = "fake-ctrl.usb"; build_fake_tree(dir.path(), ctrl, "lesavka-test", "not attached"); let state_path = dir.path().join(format!("sys/class/udc/{ctrl}/state")); let state_path_bg = state_path.clone(); let writer = std::thread::spawn(move || { std::thread::sleep(std::time::Duration::from_millis(150)); std::fs::write(state_path_bg, "configured\n").expect("flip state"); }); with_fake_roots(&dir.path().join("sys"), &dir.path().join("cfg"), || { with_var("LESAVKA_GADGET_FORCE_CYCLE", Some("1"), || { let gadget = UsbGadget::new("lesavka-test"); let result = gadget.cycle(); assert!( result.is_ok(), "configured transition should satisfy final wait_state" ); }); }); writer.join().expect("join state writer"); } #[test] #[serial] fn probe_platform_udc_reads_fake_platform_tree() { let dir = tempdir().expect("tempdir"); let dev_root = dir.path().join("sys/bus/platform/devices"); std::fs::create_dir_all(&dev_root).expect("create platform devices"); std::fs::create_dir_all(dev_root.join("foo.usb")).expect("create usb entry"); with_fake_roots(&dir.path().join("sys"), &dir.path().join("cfg"), || { let found = UsbGadget::probe_platform_udc().expect("probe"); assert_eq!(found.as_deref(), Some("foo.usb")); }); } }