276 lines
10 KiB
Rust
276 lines
10 KiB
Rust
|
|
//! Include-based coverage for aggressive USB gadget recovery helpers.
|
||
|
|
//!
|
||
|
|
//! Scope: exercise forced Lesavka core rebuild and fake UDC recovery branches.
|
||
|
|
//! Targets: `server/src/gadget.rs`.
|
||
|
|
//! Why: recovery is the fragile path that protects UVC enumeration after host
|
||
|
|
//! or gadget bumps, so it needs focused regression coverage.
|
||
|
|
|
||
|
|
#[allow(warnings)]
|
||
|
|
mod gadget_recovery_contract {
|
||
|
|
include!(env!("LESAVKA_SERVER_GADGET_SRC"));
|
||
|
|
|
||
|
|
use serial_test::serial;
|
||
|
|
use std::os::unix::fs::{PermissionsExt, symlink};
|
||
|
|
use temp_env::with_var;
|
||
|
|
use tempfile::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",
|
||
|
|
);
|
||
|
|
let driver_target = base.join("sys/bus/platform/drivers/dwc2");
|
||
|
|
let driver_link = base.join(format!("sys/bus/platform/devices/{ctrl}/driver"));
|
||
|
|
if let Some(parent) = driver_link.parent() {
|
||
|
|
std::fs::create_dir_all(parent).expect("create driver link parent");
|
||
|
|
}
|
||
|
|
symlink(&driver_target, &driver_link).expect("link controller driver");
|
||
|
|
write_file(
|
||
|
|
&base.join(format!("cfg/{gadget_name}/UDC")),
|
||
|
|
&format!("{ctrl}\n"),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
fn write_helper(path: &Path, body: &str) {
|
||
|
|
write_file(path, body);
|
||
|
|
let mut perms = std::fs::metadata(path)
|
||
|
|
.expect("helper metadata")
|
||
|
|
.permissions();
|
||
|
|
perms.set_mode(0o755);
|
||
|
|
std::fs::set_permissions(path, perms).expect("chmod helper");
|
||
|
|
}
|
||
|
|
|
||
|
|
fn with_fast_recovery_env(helper: &Path, f: impl FnOnce()) {
|
||
|
|
let helper = helper.to_string_lossy().to_string();
|
||
|
|
with_var("LESAVKA_CORE_HELPER", Some(helper), || {
|
||
|
|
with_var("LESAVKA_USB_RECOVERY_CYCLE_WAIT_MS", Some("0"), || {
|
||
|
|
with_var("LESAVKA_USB_RECOVERY_REBUILD_WAIT_MS", Some("0"), || {
|
||
|
|
with_var("LESAVKA_USB_RECOVERY_FINAL_WAIT_MS", Some("0"), f);
|
||
|
|
})
|
||
|
|
})
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
#[serial]
|
||
|
|
fn recover_enumeration_runs_forced_core_rebuild_after_stuck_soft_cycle() {
|
||
|
|
let dir = tempdir().expect("tempdir");
|
||
|
|
let ctrl = "fake-ctrl.usb";
|
||
|
|
build_fake_tree(dir.path(), ctrl, "lesavka-test", "not attached");
|
||
|
|
let helper = dir.path().join("fake-core.sh");
|
||
|
|
write_helper(
|
||
|
|
&helper,
|
||
|
|
r#"#!/usr/bin/env bash
|
||
|
|
set -euo pipefail
|
||
|
|
echo forced core helper >&2
|
||
|
|
printf 'configured\n' > "$LESAVKA_GADGET_SYSFS_ROOT/class/udc/fake-ctrl.usb/state"
|
||
|
|
"#,
|
||
|
|
);
|
||
|
|
|
||
|
|
with_fake_roots(&dir.path().join("sys"), &dir.path().join("cfg"), || {
|
||
|
|
with_fast_recovery_env(&helper, || {
|
||
|
|
let gadget = UsbGadget::new("lesavka-test");
|
||
|
|
gadget
|
||
|
|
.recover_enumeration()
|
||
|
|
.expect("forced rebuild should recover fake UDC");
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
let state = std::fs::read_to_string(dir.path().join(format!("sys/class/udc/{ctrl}/state")))
|
||
|
|
.expect("read state");
|
||
|
|
assert_eq!(state.trim(), "configured");
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
#[serial]
|
||
|
|
fn recover_enumeration_passes_aggressive_rebuild_environment_to_core_helper() {
|
||
|
|
let dir = tempdir().expect("tempdir");
|
||
|
|
let ctrl = "fake-ctrl.usb";
|
||
|
|
build_fake_tree(dir.path(), ctrl, "lesavka-test", "not attached");
|
||
|
|
let helper = dir.path().join("fake-core-env.sh");
|
||
|
|
let env_dump = dir.path().join("helper-env.txt");
|
||
|
|
write_helper(
|
||
|
|
&helper,
|
||
|
|
r#"#!/usr/bin/env bash
|
||
|
|
set -euo pipefail
|
||
|
|
cat > "$LESAVKA_HELPER_ENV_DUMP" <<EOF
|
||
|
|
LESAVKA_ALLOW_GADGET_RESET=${LESAVKA_ALLOW_GADGET_RESET:-}
|
||
|
|
LESAVKA_ATTACH_WRITE_UDC=${LESAVKA_ATTACH_WRITE_UDC:-}
|
||
|
|
LESAVKA_DETACH_CLEAR_UDC=${LESAVKA_DETACH_CLEAR_UDC:-}
|
||
|
|
LESAVKA_RELOAD_UVCVIDEO=${LESAVKA_RELOAD_UVCVIDEO:-}
|
||
|
|
LESAVKA_UVC_FALLBACK=${LESAVKA_UVC_FALLBACK:-}
|
||
|
|
LESAVKA_UVC_CODEC=${LESAVKA_UVC_CODEC:-}
|
||
|
|
EOF
|
||
|
|
printf 'configured\n' > "$LESAVKA_GADGET_SYSFS_ROOT/class/udc/fake-ctrl.usb/state"
|
||
|
|
"#,
|
||
|
|
);
|
||
|
|
|
||
|
|
with_fake_roots(&dir.path().join("sys"), &dir.path().join("cfg"), || {
|
||
|
|
with_fast_recovery_env(&helper, || {
|
||
|
|
with_var(
|
||
|
|
"LESAVKA_HELPER_ENV_DUMP",
|
||
|
|
Some(env_dump.to_string_lossy().to_string()),
|
||
|
|
|| {
|
||
|
|
let gadget = UsbGadget::new("lesavka-test");
|
||
|
|
gadget
|
||
|
|
.recover_enumeration()
|
||
|
|
.expect("forced rebuild should recover fake UDC");
|
||
|
|
},
|
||
|
|
);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
let dumped = std::fs::read_to_string(env_dump).expect("read helper env dump");
|
||
|
|
for line in [
|
||
|
|
"LESAVKA_ALLOW_GADGET_RESET=1",
|
||
|
|
"LESAVKA_ATTACH_WRITE_UDC=1",
|
||
|
|
"LESAVKA_DETACH_CLEAR_UDC=1",
|
||
|
|
"LESAVKA_RELOAD_UVCVIDEO=1",
|
||
|
|
"LESAVKA_UVC_FALLBACK=0",
|
||
|
|
"LESAVKA_UVC_CODEC=mjpeg",
|
||
|
|
] {
|
||
|
|
assert!(dumped.contains(line), "{line} missing from {dumped}");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
#[serial]
|
||
|
|
fn recover_enumeration_honors_explicit_uvc_fallback_override() {
|
||
|
|
let dir = tempdir().expect("tempdir");
|
||
|
|
let ctrl = "fake-ctrl.usb";
|
||
|
|
build_fake_tree(dir.path(), ctrl, "lesavka-test", "not attached");
|
||
|
|
let helper = dir.path().join("fake-core-env-override.sh");
|
||
|
|
let env_dump = dir.path().join("helper-env-override.txt");
|
||
|
|
write_helper(
|
||
|
|
&helper,
|
||
|
|
r#"#!/usr/bin/env bash
|
||
|
|
set -euo pipefail
|
||
|
|
cat > "$LESAVKA_HELPER_ENV_DUMP" <<EOF
|
||
|
|
LESAVKA_UVC_FALLBACK=${LESAVKA_UVC_FALLBACK:-}
|
||
|
|
EOF
|
||
|
|
printf 'configured\n' > "$LESAVKA_GADGET_SYSFS_ROOT/class/udc/fake-ctrl.usb/state"
|
||
|
|
"#,
|
||
|
|
);
|
||
|
|
|
||
|
|
with_fake_roots(&dir.path().join("sys"), &dir.path().join("cfg"), || {
|
||
|
|
with_fast_recovery_env(&helper, || {
|
||
|
|
with_var("LESAVKA_UVC_FALLBACK", Some("1"), || {
|
||
|
|
with_var(
|
||
|
|
"LESAVKA_HELPER_ENV_DUMP",
|
||
|
|
Some(env_dump.to_string_lossy().to_string()),
|
||
|
|
|| {
|
||
|
|
let gadget = UsbGadget::new("lesavka-test");
|
||
|
|
gadget
|
||
|
|
.recover_enumeration()
|
||
|
|
.expect("forced rebuild should recover fake UDC");
|
||
|
|
},
|
||
|
|
);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
let dumped = std::fs::read_to_string(env_dump).expect("read helper env dump");
|
||
|
|
assert!(
|
||
|
|
dumped.contains("LESAVKA_UVC_FALLBACK=1"),
|
||
|
|
"explicit fallback override missing from {dumped}"
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
#[serial]
|
||
|
|
fn recover_enumeration_reports_clear_failure_when_helper_leaves_udc_unattached() {
|
||
|
|
let dir = tempdir().expect("tempdir");
|
||
|
|
let ctrl = "fake-ctrl.usb";
|
||
|
|
build_fake_tree(dir.path(), ctrl, "lesavka-test", "not attached");
|
||
|
|
let helper = dir.path().join("fake-core-noop.sh");
|
||
|
|
write_helper(
|
||
|
|
&helper,
|
||
|
|
r#"#!/usr/bin/env bash
|
||
|
|
set -euo pipefail
|
||
|
|
echo noop core helper >&2
|
||
|
|
"#,
|
||
|
|
);
|
||
|
|
|
||
|
|
with_fake_roots(&dir.path().join("sys"), &dir.path().join("cfg"), || {
|
||
|
|
with_fast_recovery_env(&helper, || {
|
||
|
|
let gadget = UsbGadget::new("lesavka-test");
|
||
|
|
let err = gadget
|
||
|
|
.recover_enumeration()
|
||
|
|
.expect_err("still-unattached UDC should fail recovery");
|
||
|
|
let message = format!("{err:#}");
|
||
|
|
assert!(message.contains("still not attached"), "{message}");
|
||
|
|
assert!(
|
||
|
|
message.contains("forced gadget rebuild helper"),
|
||
|
|
"{message}"
|
||
|
|
);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
#[serial]
|
||
|
|
fn run_forced_core_rebuild_reports_helper_failure_and_truncates_tail() {
|
||
|
|
let dir = tempdir().expect("tempdir");
|
||
|
|
let helper = dir.path().join("fake-core-fail.sh");
|
||
|
|
write_helper(
|
||
|
|
&helper,
|
||
|
|
r#"#!/usr/bin/env bash
|
||
|
|
set -euo pipefail
|
||
|
|
printf '%*s\n' 1400 '' | tr ' ' x
|
||
|
|
exit 42
|
||
|
|
"#,
|
||
|
|
);
|
||
|
|
|
||
|
|
with_fast_recovery_env(&helper, || {
|
||
|
|
let gadget = UsbGadget::new("lesavka-test");
|
||
|
|
let err = gadget
|
||
|
|
.run_forced_core_rebuild()
|
||
|
|
.expect_err("failing helper should report stdout/stderr");
|
||
|
|
let message = format!("{err:#}");
|
||
|
|
assert!(message.contains("exited with"), "{message}");
|
||
|
|
assert!(message.contains("..."), "{message}");
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
#[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"));
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|