diff --git a/testing/tests/client_main_process_contract.rs b/testing/tests/client_main_process_contract.rs new file mode 100644 index 0000000..371f65e --- /dev/null +++ b/testing/tests/client_main_process_contract.rs @@ -0,0 +1,48 @@ +//! Integration coverage for client process startup behavior. +//! +//! Scope: launch the real `lesavka-client` binary and validate guarded startup +//! behavior when desktop runtime prerequisites are missing. +//! Targets: `client/src/main.rs`. +//! Why: process-level startup failures should stay deterministic and +//! user-readable. + +use serial_test::serial; +use std::path::{Path, PathBuf}; +use std::process::Command; + +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 find_binary(name: &str) -> Option { + candidate_dirs() + .into_iter() + .map(|dir| dir.join(name)) + .find(|path| path.exists() && path.is_file()) +} + +#[test] +#[serial] +fn client_binary_fails_fast_without_runtime_dir() { + let Some(bin) = find_binary("lesavka-client") else { + return; + }; + + let status = Command::new(Path::new(&bin)) + .env_remove("XDG_RUNTIME_DIR") + .env_remove("LESAVKA_HEADLESS") + .status() + .expect("spawn lesavka-client"); + + assert!(!status.success(), "startup should fail without runtime dir"); +} diff --git a/testing/tests/server_main_process_contract.rs b/testing/tests/server_main_process_contract.rs new file mode 100644 index 0000000..95ebf94 --- /dev/null +++ b/testing/tests/server_main_process_contract.rs @@ -0,0 +1,62 @@ +//! Integration coverage for server process startup behavior. +//! +//! Scope: launch the real `lesavka-server` binary and assert startup reaches a +//! terminal state quickly in this non-gadget test environment. +//! Targets: `server/src/main.rs`. +//! Why: process-level boot behavior should remain deterministic when required +//! gadget endpoints are unavailable. + +use serial_test::serial; +use std::path::PathBuf; +use std::process::Command; +use std::time::{Duration, Instant}; + +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 find_binary(name: &str) -> Option { + candidate_dirs() + .into_iter() + .map(|dir| dir.join(name)) + .find(|path| path.exists() && path.is_file()) +} + +#[test] +#[serial] +fn server_binary_exits_quickly_without_hid_nodes() { + let Some(bin) = find_binary("lesavka-server") else { + return; + }; + + let mut child = Command::new(bin) + .env("LESAVKA_DISABLE_UVC", "1") + .spawn() + .expect("spawn lesavka-server"); + + let deadline = Instant::now() + Duration::from_secs(3); + loop { + if let Some(status) = child.try_wait().expect("poll child") { + assert!( + !status.success(), + "server unexpectedly succeeded in test environment" + ); + break; + } + if Instant::now() >= deadline { + let _ = child.kill(); + panic!("server did not terminate within startup timeout"); + } + std::thread::sleep(Duration::from_millis(50)); + } +}