From aaf723017883ae8f561b234284ea6dac6ba1920e Mon Sep 17 00:00:00 2001 From: Brad Stein Date: Sun, 12 Apr 2026 21:12:22 -0300 Subject: [PATCH] testing: add gadget include coverage contracts --- testing/build.rs | 8 ++ .../tests/server_gadget_include_contract.rs | 102 ++++++++++++++++++ 2 files changed, 110 insertions(+) create mode 100644 testing/tests/server_gadget_include_contract.rs diff --git a/testing/build.rs b/testing/build.rs index 7896235..9075826 100644 --- a/testing/build.rs +++ b/testing/build.rs @@ -16,6 +16,10 @@ fn main() { .join("server/src/video.rs") .canonicalize() .expect("canonical server video path"); + let server_gadget = workspace_dir + .join("server/src/gadget.rs") + .canonicalize() + .expect("canonical server gadget path"); let client_main = workspace_dir .join("client/src/main.rs") .canonicalize() @@ -50,6 +54,10 @@ fn main() { "cargo:rustc-env=LESAVKA_SERVER_VIDEO_SRC={}", server_video.display() ); + println!( + "cargo:rustc-env=LESAVKA_SERVER_GADGET_SRC={}", + server_gadget.display() + ); println!( "cargo:rustc-env=LESAVKA_CLIENT_MAIN_SRC={}", client_main.display() diff --git a/testing/tests/server_gadget_include_contract.rs b/testing/tests/server_gadget_include_contract.rs new file mode 100644 index 0000000..99356be --- /dev/null +++ b/testing/tests/server_gadget_include_contract.rs @@ -0,0 +1,102 @@ +//! 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}; + + #[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 wait_state_any_times_out_for_missing_controller() { + let result = UsbGadget::wait_state_any("definitely-missing-udc", 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 result = UsbGadget::probe_platform_udc(); + assert!(result.is_ok()); + } + + #[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)); + + 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)); + } + + #[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()); + }); + } +}