lesavka/testing/tests/server_main_process_contract.rs

190 lines
6.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::io::Read;
use std::net::TcpListener;
use std::path::PathBuf;
use std::process::{Command, Stdio};
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 workspace_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("workspace root")
.to_path_buf()
}
fn build_current_binary(name: &str) -> Option<PathBuf> {
if name != "lesavka-server" {
return None;
}
let cargo = option_env!("CARGO").unwrap_or("cargo");
let target_dir = workspace_root().join("target/process-contract-debug");
let mut command = Command::new(cargo);
let status = command
.current_dir(workspace_root())
.env_remove("CARGO_ENCODED_RUSTFLAGS")
.env_remove("RUSTFLAGS")
.env_remove("LLVM_PROFILE_FILE")
.arg("build")
.arg("--target-dir")
.arg(&target_dir)
.args(["-p", "lesavka_server", "--bin", name])
.status()
.ok()?;
status
.success()
.then(|| target_dir.join("debug").join(name))
.filter(|path| path.exists() && path.is_file())
}
fn find_binary(name: &str) -> Option<PathBuf> {
build_current_binary(name)
.or_else(|| 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 listener = TcpListener::bind("127.0.0.1:0").expect("reserve local test port");
let bind_addr = listener.local_addr().expect("local test addr");
drop(listener);
if cfg!(coverage) {
let mut child = Command::new(bin)
.env("LESAVKA_DISABLE_UVC", "1")
.env("LESAVKA_SERVER_BIND_ADDR", bind_addr.to_string())
.stderr(Stdio::piped())
.spawn()
.expect("spawn coverage-mode lesavka-server");
let deadline = Instant::now() + Duration::from_secs(3);
loop {
if let Some(status) = child.try_wait().expect("poll coverage-mode server") {
let mut stderr = String::new();
if let Some(mut pipe) = child.stderr.take() {
let _ = pipe.read_to_string(&mut stderr);
}
assert!(
!status.success(),
"coverage-mode server should not report success before the live-loop proof"
);
assert!(
stderr.contains("coverage mode skips live gRPC serve loop"),
"coverage-mode server should report the intentional live-loop skip; stderr was:\n{stderr}"
);
return;
}
let log = fs::read_to_string(&log_path).unwrap_or_default();
if log.contains(
"HID endpoints are not ready; relay will keep running and open them lazily",
) {
let _ = child.kill();
let _ = child.wait();
return;
}
assert!(
Instant::now() < deadline,
"timed out waiting for coverage stub exit or live startup log; log was:\n{log}"
);
std::thread::sleep(Duration::from_millis(50));
}
}
let mut child = Command::new(bin)
.env("LESAVKA_DISABLE_UVC", "1")
.env("LESAVKA_SERVER_BIND_ADDR", bind_addr.to_string())
.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();
}