261 lines
9.3 KiB
Rust
261 lines
9.3 KiB
Rust
//! 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"));
|
|
});
|
|
}
|
|
}
|