Skip to main content

term_session_mock/
lib.rs

1//! Shared test helpers for the `term-session-mock` binary.
2//!
3//! Every test suite that needs the mock binary (the session server integration
4//! tests, the daemon tests, the PTY engine unit tests) resolves it through the
5//! single canonical [`get_mock_bin`] helper instead of re-implementing
6//! `current_exe()` path-walking in each crate. Keeping the resolution logic in
7//! the mock's own library means there is exactly one place that knows where the
8//! binary lives and how to find it on every platform.
9
10use std::path::PathBuf;
11
12/// Locate (building on demand if necessary) the compiled `term-session-mock`
13/// binary.
14///
15/// Resolution order:
16/// 1. `CARGO_BIN_EXE_term-session-mock`, which Cargo sets when this crate's
17///    *binary* is a dependency (e.g. for this crate's own integration tests).
18/// 2. The plain workspace build location: `target/debug/term-session-mock`
19///    (walking up from the test executable, skipping the `deps` directory).
20/// 3. The hashed dependency build location: `target/debug/deps/term-session-mock-*`.
21/// 4. If none exist, run `cargo build` to produce the binary, then resolve
22///    again.
23///
24/// Never returns a missing path: the binary is built on demand so tests can
25/// never silently skip. Panics if the build fails.
26pub fn get_mock_bin() -> PathBuf {
27    get_bin("term-session-mock", "CARGO_BIN_EXE_term-session-mock")
28}
29
30/// The two conventional locations for the compiled `bin_name` binary.
31fn bin_candidates(bin_name: &str) -> (PathBuf, PathBuf) {
32    let mut path = std::env::current_exe().expect("test exe path");
33    path.pop();
34    if path.ends_with("deps") {
35        path.pop();
36    }
37    let plain = path.join(format!("{bin_name}{}", std::env::consts::EXE_SUFFIX));
38    let deps_dir = path.join("deps");
39    (plain, deps_dir)
40}
41
42fn resolve_bin(bin_name: &str) -> Option<PathBuf> {
43    let (plain, deps_dir) = bin_candidates(bin_name);
44    if plain.exists() {
45        return Some(plain);
46    }
47
48    let suffix = std::env::consts::EXE_SUFFIX;
49    let prefix = format!("{bin_name}-");
50    if let Ok(entries) = std::fs::read_dir(&deps_dir) {
51        for entry in entries.flatten() {
52            let name = entry.file_name();
53            let name = name.to_string_lossy();
54            if name.starts_with(&prefix) && name.ends_with(suffix) {
55                return Some(entry.path());
56            }
57        }
58    }
59
60    None
61}
62
63/// Invoke `cargo build` for this crate so the binaries exist in the target
64/// directory. Uses the crate's own `Cargo.toml` so it works regardless of
65/// which workspace directory the test process happens to run from.
66fn build_bin() {
67    let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("Cargo.toml");
68    let status = std::process::Command::new(env!("CARGO"))
69        .arg("build")
70        .arg("--manifest-path")
71        .arg(&manifest)
72        .status()
73        .expect("failed to spawn `cargo build` for term-session-mock");
74    if !status.success() {
75        panic!(
76            "`cargo build --manifest-path {}` failed with {status}",
77            manifest.display()
78        );
79    }
80}
81
82fn get_bin(bin_name: &str, env_var: &str) -> PathBuf {
83    if let Ok(path) = std::env::var(env_var) {
84        return PathBuf::from(path);
85    }
86
87    let resolved = resolve_bin(bin_name);
88    if let Some(path) = resolved {
89        return path;
90    }
91
92    build_bin();
93
94    match resolve_bin(bin_name) {
95        Some(path) => path,
96        None => panic!(
97            "{bin_name} binary still missing after `cargo build`; \
98             searched {:?} and {:?}",
99            bin_candidates(bin_name).0,
100            bin_candidates(bin_name).1,
101        ),
102    }
103}
104
105// TODO: The following `process_is_alive` utils could be migrated somewhere else.
106// Here's an issue/comment with ideas: https://github.com/jzombie/term-wm/issues/204#issuecomment-5170285844
107
108/// Exit code for `check_pid` when the process is alive.
109pub const CHECK_PID_ALIVE: i32 = 0;
110/// Exit code for `check_pid` when the process is not running.
111pub const CHECK_PID_DEAD: i32 = 1;
112
113/// Whether a process with the given OS PID is currently running.
114#[cfg(windows)]
115pub fn process_is_alive(pid: u32) -> bool {
116    use windows_sys::Win32::Foundation::{CloseHandle, STILL_ACTIVE};
117    use windows_sys::Win32::System::Threading::{
118        GetExitCodeProcess, OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION,
119    };
120
121    unsafe {
122        let handle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid);
123        if handle.is_null() {
124            return false;
125        }
126        let mut code: u32 = 0;
127        let ok = GetExitCodeProcess(handle, &mut code);
128        let _ = CloseHandle(handle);
129        ok != 0 && code == STILL_ACTIVE as u32
130    }
131}
132
133/// Whether a process with the given OS PID is currently running.
134#[cfg(not(windows))]
135pub fn process_is_alive(pid: u32) -> bool {
136    // kill(pid, 0) probes existence without signalling.
137    unsafe { libc::kill(pid as i32, 0) == 0 }
138}