lesavka/testing/tests/server_main_process_contract.rs

110 lines
3.4 KiB
Rust

//! Integration coverage for server process startup behavior.
//!
//! Scope: launch the real `lesavka-server` binary and assert startup stays
//! resilient in this non-gadget test environment.
//! Targets: `server/src/main.rs`.
//! Why: missing gadget endpoints should not crash the relay; the server keeps
//! running and opens HID lazily when the device nodes appear.
use serial_test::serial;
use std::fs;
use std::path::PathBuf;
use std::process::Command;
use std::time::{Duration, Instant};
fn cargo_binary(name: &str) -> Option<PathBuf> {
let key = format!("CARGO_BIN_EXE_{name}");
option_env!("CARGO_BIN_EXE_lesavka-server")
.filter(|_| name == "lesavka-server")
.map(PathBuf::from)
.or_else(|| std::env::var_os(key).map(PathBuf::from))
.filter(|path| path.exists() && path.is_file())
}
fn candidate_dirs() -> Vec<PathBuf> {
let exe = std::env::current_exe().expect("current exe path");
let mut dirs = Vec::new();
if let Some(parent) = exe.parent() {
dirs.push(parent.to_path_buf());
if let Some(grand) = parent.parent() {
dirs.push(grand.to_path_buf());
}
}
dirs.push(PathBuf::from("target/debug"));
dirs.push(PathBuf::from("target/llvm-cov-target/debug"));
dirs
}
fn find_binary(name: &str) -> Option<PathBuf> {
cargo_binary(name).or_else(|| {
candidate_dirs()
.into_iter()
.map(|dir| dir.join(name))
.find(|path| path.exists() && path.is_file())
})
}
fn server_package_version() -> Option<String> {
let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()?
.join("server/Cargo.toml");
fs::read_to_string(manifest).ok()?.lines().find_map(|line| {
let trimmed = line.trim();
trimmed
.strip_prefix("version")
.and_then(|value| value.split('=').nth(1))
.map(str::trim)
.and_then(|value| value.strip_prefix('"')?.strip_suffix('"'))
.map(str::to_string)
})
}
fn wait_for_log(path: &PathBuf, needle: &str, deadline: Instant) -> String {
loop {
let log = fs::read_to_string(path).unwrap_or_default();
if log.contains(needle) {
return log;
}
assert!(
Instant::now() < deadline,
"timed out waiting for log line {needle:?}; log was:\n{log}"
);
std::thread::sleep(Duration::from_millis(50));
}
}
#[test]
#[serial]
fn server_binary_stays_up_with_missing_hid_nodes_and_current_version() {
let Some(bin) = find_binary("lesavka-server") else {
return;
};
let log_path = PathBuf::from("/tmp/lesavka-server.log");
let _ = fs::remove_file(&log_path);
let mut child = Command::new(bin)
.env("LESAVKA_DISABLE_UVC", "1")
.spawn()
.expect("spawn lesavka-server");
let deadline = Instant::now() + Duration::from_secs(3);
if let Some(version) = server_package_version() {
let _ = wait_for_log(
&log_path,
&format!("lesavka_server v{version} starting up"),
deadline,
);
}
let log = wait_for_log(
&log_path,
"HID endpoints are not ready; relay will keep running and open them lazily",
deadline,
);
assert!(
child.try_wait().expect("poll child").is_none(),
"server should stay alive with lazy HID recovery; log was:\n{log}"
);
let _ = child.kill();
let _ = child.wait();
}