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    #[test]
130    fn test_enforce_prebuilt_helpers_off_by_default() {
131        // Should be false when env var is not set
132        std::env::remove_var("RAPACE_PREBUILT_HELPERS");
133        assert!(!enforce_prebuilt_helpers());
134    }
135
136    #[test]
137    fn test_enforce_prebuilt_helpers_true() {
138        std::env::set_var("RAPACE_PREBUILT_HELPERS", "true");
139        assert!(enforce_prebuilt_helpers());
140        std::env::remove_var("RAPACE_PREBUILT_HELPERS");
141    }
142
143    #[test]
144    fn test_enforce_prebuilt_helpers_1() {
145        std::env::set_var("RAPACE_PREBUILT_HELPERS", "1");
146        assert!(enforce_prebuilt_helpers());
147        std::env::remove_var("RAPACE_PREBUILT_HELPERS");
148    }
149
150    #[test]
151    fn test_enforce_prebuilt_helpers_false() {
152        std::env::set_var("RAPACE_PREBUILT_HELPERS", "false");
153        assert!(!enforce_prebuilt_helpers());
154        std::env::remove_var("RAPACE_PREBUILT_HELPERS");
155    }
156
157    #[test]
158    fn test_find_helper_binary_not_found_not_enforced() {
159        std::env::remove_var("RAPACE_PREBUILT_HELPERS");
160        // Should return an error without panicking
161        let result = find_helper_binary("nonexistent-binary");
162        assert!(result.is_err());
163    }
164}