//! 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 { 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 { 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 { 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 { 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 { 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(); }