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}