lesavka/testing/tests/server_main_rpc_reset_contract.rs

190 lines
8.1 KiB
Rust
Raw Permalink Normal View History

//! 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()),
2026-04-30 08:16:57 -03:00
calibration: std::sync::Arc::new(CalibrationStore::load(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()),
2026-04-30 08:16:57 -03:00
calibration: std::sync::Arc::new(CalibrationStore::load(
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()
);
},
);
},
);
},
);
}
}