rapace_testkit/
helper_binary.rs

1//! Helper binary management for cross-process tests.
2//!
3//! This module provides utilities for locating and using pre-built helper binaries
4//! in cross-process tests. When `RAPACE_PREBUILT_HELPERS` is set, tests will only
5//! use pre-built binaries and fail immediately if they're missing.
6//!
7//! # Environment Variables
8//!
9//! - `RAPACE_PREBUILT_HELPERS`: When set to `1` or `true`, enforce that helper
10//!   binaries must be pre-built (skip inline building). Tests will panic if binaries
11//!   are not found. This ensures tests don't rebuild binaries during execution.
12//!
13//! # Usage
14//!
15//! In your cross-process test:
16//!
17//! ```ignore
18//! use rapace_testkit::helper_binary::find_helper_binary;
19//!
20//! #[tokio::test]
21//! async fn test_my_service() {
22//!     // Find the helper binary (will fail fast if not pre-built and RAPACE_PREBUILT_HELPERS is set)
23//!     let helper_path = find_helper_binary("my-helper").unwrap();
24//!
25//!     // Spawn the helper
26//!     let mut helper = Command::new(&helper_path)
27//!         .args(&["--transport=stream", "--addr=127.0.0.1:9000"])
28//!         .spawn()
29//!         .expect("failed to spawn helper");
30//!
31//!     // ... test logic ...
32//! }
33//! ```
34
35use std::path::PathBuf;
36
37/// Check if pre-built helpers are enforced via environment variable.
38///
39/// When `RAPACE_PREBUILT_HELPERS=1` or `RAPACE_PREBUILT_HELPERS=true`,
40/// tests must use pre-built binaries and will fail if they're missing.
41pub fn enforce_prebuilt_helpers() -> bool {
42    matches!(
43        std::env::var("RAPACE_PREBUILT_HELPERS"),
44        Ok(v) if v.to_lowercase() == "1" || v.to_lowercase() == "true"
45    )
46}
47
48/// Find a pre-built helper binary in the target directory.
49///
50/// This function:
51/// 1. Uses the current executable's path to locate the target directory
52/// 2. Looks for the binary in the debug or release subdirectory
53/// 3. If `RAPACE_PREBUILT_HELPERS` is set, fails immediately if not found
54/// 4. Otherwise, returns an error that tests can use to decide whether to build inline
55///
56/// # Arguments
57///
58/// * `binary_name` - The name of the helper binary (e.g., "diagnostics-plugin-helper")
59///
60/// # Returns
61///
62/// `Ok(PathBuf)` if the binary is found, `Err(String)` with an error message otherwise.
63///
64/// # Panics
65///
66/// If `RAPACE_PREBUILT_HELPERS` is set and the binary is not found.
67pub fn find_helper_binary(binary_name: &str) -> Result<PathBuf, String> {
68    let enforce = enforce_prebuilt_helpers();
69
70    // Get the current executable's directory
71    let current_exe =
72        std::env::current_exe().map_err(|e| format!("failed to get current executable: {}", e))?;
73
74    // The test executable is in target/{debug|release}/deps/ (via nextest) or target/{debug|release}/ (via cargo test)
75    // We need to find the profile directory (target/debug or target/release) containing the binary
76    let mut search_dir = current_exe
77        .parent()
78        .ok_or_else(|| "could not find parent directory".to_string())?;
79
80    // Try up to 3 levels up to find the profile directory containing helper binaries
81    for _ in 0..3 {
82        let candidate_path = search_dir.join(binary_name);
83        if candidate_path.exists() {
84            return Ok(candidate_path);
85        }
86
87        if let Some(parent) = search_dir.parent() {
88            search_dir = parent;
89        } else {
90            break;
91        }
92    }
93
94    // Fallback: Go up 2 levels from deps to get to profile directory
95    let profile_dir = match current_exe.parent().and_then(|p| p.parent()) {
96        Some(dir) => dir.to_path_buf(),
97        None => {
98            return Err(format!(
99                "Could not determine target directory from executable path: {:?}",
100                current_exe
101            ));
102        }
103    };
104
105    let binary_path = profile_dir.join(binary_name);
106
107    let error_msg = format!(
108        "helper binary '{}' not found. Searched in: {:?}. \
109         Run 'cargo xtask test' or build helpers with 'cargo build --bin {} -p <package>'",
110        binary_name, binary_path, binary_name
111    );
112
113    if enforce {
114        panic!(
115            "RAPACE_PREBUILT_HELPERS is set: {}\n\
116             To build helpers manually: cargo xtask test --no-run\n\
117             Then use: RAPACE_PREBUILT_HELPERS=1 cargo test",
118            error_msg
119        );
120    }
121
122    Err(error_msg)
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128
129    fn env_lock() -> &'static std::sync::Mutex<()> {
130        static LOCK: std::sync::OnceLock<std::sync::Mutex<()>> = std::sync::OnceLock::new();
131        LOCK.get_or_init(|| std::sync::Mutex::new(()))
132    }
133
134    fn get_prebuilt_helpers_var() -> Option<std::ffi::OsString> {
135        std::env::var_os("RAPACE_PREBUILT_HELPERS")
136    }
137
138    fn set_prebuilt_helpers_var(value: &str) {
139        // SAFETY: Mutating the process environment is unsafe on newer Rust versions because it is
140        // global state. In these unit tests we serialize access via `env_lock()` and restore the
141        // previous value to avoid cross-test interference.
142        unsafe { std::env::set_var("RAPACE_PREBUILT_HELPERS", value) };
143    }
144
145    fn remove_prebuilt_helpers_var() {
146        // SAFETY: See `set_prebuilt_helpers_var`.
147        unsafe { std::env::remove_var("RAPACE_PREBUILT_HELPERS") };
148    }
149
150    #[test]
151    fn test_enforce_prebuilt_helpers_off_by_default() {
152        let _guard = env_lock().lock().unwrap();
153        let prev = get_prebuilt_helpers_var();
154        remove_prebuilt_helpers_var();
155
156        // Should be false when env var is not set
157        assert!(!enforce_prebuilt_helpers());
158
159        match prev {
160            Some(v) => unsafe { std::env::set_var("RAPACE_PREBUILT_HELPERS", v) },
161            None => remove_prebuilt_helpers_var(),
162        }
163    }
164
165    #[test]
166    fn test_enforce_prebuilt_helpers_true() {
167        let _guard = env_lock().lock().unwrap();
168        let prev = get_prebuilt_helpers_var();
169        set_prebuilt_helpers_var("true");
170
171        assert!(enforce_prebuilt_helpers());
172
173        match prev {
174            Some(v) => unsafe { std::env::set_var("RAPACE_PREBUILT_HELPERS", v) },
175            None => remove_prebuilt_helpers_var(),
176        }
177    }
178
179    #[test]
180    fn test_enforce_prebuilt_helpers_1() {
181        let _guard = env_lock().lock().unwrap();
182        let prev = get_prebuilt_helpers_var();
183        set_prebuilt_helpers_var("1");
184
185        assert!(enforce_prebuilt_helpers());
186
187        match prev {
188            Some(v) => unsafe { std::env::set_var("RAPACE_PREBUILT_HELPERS", v) },
189            None => remove_prebuilt_helpers_var(),
190        }
191    }
192
193    #[test]
194    fn test_enforce_prebuilt_helpers_false() {
195        let _guard = env_lock().lock().unwrap();
196        let prev = get_prebuilt_helpers_var();
197        set_prebuilt_helpers_var("false");
198
199        assert!(!enforce_prebuilt_helpers());
200
201        match prev {
202            Some(v) => unsafe { std::env::set_var("RAPACE_PREBUILT_HELPERS", v) },
203            None => remove_prebuilt_helpers_var(),
204        }
205    }
206
207    #[test]
208    fn test_find_helper_binary_not_found_not_enforced() {
209        let _guard = env_lock().lock().unwrap();
210        let prev = get_prebuilt_helpers_var();
211        remove_prebuilt_helpers_var();
212
213        // Should return an error without panicking
214        let result = find_helper_binary("nonexistent-binary");
215        assert!(result.is_err());
216
217        match prev {
218            Some(v) => unsafe { std::env::set_var("RAPACE_PREBUILT_HELPERS", v) },
219            None => remove_prebuilt_helpers_var(),
220        }
221    }
222}