mockforge_test/
process.rs1use crate::config::ServerConfig;
4use crate::error::{Error, Result};
5use std::path::PathBuf;
6use std::process::{Child, Command, Stdio};
7use tracing::{debug, info, warn};
8
9pub struct ManagedProcess {
11 child: Child,
12 http_port: u16,
13 pid: u32,
14}
15
16impl ManagedProcess {
17 pub fn spawn(config: &ServerConfig) -> Result<Self> {
19 let binary_path = find_mockforge_binary(config)?;
20 debug!("Using MockForge binary at: {:?}", binary_path);
21
22 let mut cmd = Command::new(&binary_path);
23 cmd.arg("serve");
24
25 cmd.arg("--http-port").arg(config.http_port.to_string());
27
28 if let Some(ws_port) = config.ws_port {
29 cmd.arg("--ws-port").arg(ws_port.to_string());
30 }
31
32 if let Some(grpc_port) = config.grpc_port {
33 cmd.arg("--grpc-port").arg(grpc_port.to_string());
34 }
35
36 if let Some(admin_port) = config.admin_port {
37 cmd.arg("--admin-port").arg(admin_port.to_string());
38 }
39
40 if let Some(metrics_port) = config.metrics_port {
41 cmd.arg("--metrics-port").arg(metrics_port.to_string());
42 }
43
44 if config.enable_admin {
46 cmd.arg("--admin");
47 }
48
49 if config.enable_metrics {
51 cmd.arg("--metrics");
52 }
53
54 if let Some(spec_file) = &config.spec_file {
56 cmd.arg("--spec").arg(spec_file);
57 }
58
59 if let Some(workspace_dir) = &config.workspace_dir {
61 cmd.arg("--workspace-dir").arg(workspace_dir);
62 }
63
64 if let Some(profile) = &config.profile {
66 cmd.arg("--profile").arg(profile);
67 }
68
69 for arg in &config.extra_args {
71 cmd.arg(arg);
72 }
73
74 if let Some(working_dir) = &config.working_dir {
76 cmd.current_dir(working_dir);
77 }
78
79 for (key, value) in &config.env_vars {
81 cmd.env(key, value);
82 }
83
84 cmd.stdout(Stdio::inherit());
86 cmd.stderr(Stdio::inherit());
87
88 debug!("Spawning MockForge process: {:?}", cmd);
89
90 let child = cmd
91 .spawn()
92 .map_err(|e| Error::ServerStartFailed(format!("Failed to spawn process: {}", e)))?;
93
94 let pid = child.id();
95 info!("Spawned MockForge process with PID: {}", pid);
96
97 Ok(Self {
98 child,
99 http_port: config.http_port,
100 pid,
101 })
102 }
103
104 pub fn http_port(&self) -> u16 {
106 self.http_port
107 }
108
109 pub fn pid(&self) -> u32 {
111 self.pid
112 }
113
114 pub fn is_running(&mut self) -> bool {
116 matches!(self.child.try_wait(), Ok(None))
117 }
118
119 pub fn kill(&mut self) -> Result<()> {
121 if self.is_running() {
122 debug!("Killing MockForge process (PID: {})", self.pid);
123 self.child
124 .kill()
125 .map_err(|e| Error::ProcessError(format!("Failed to kill process: {}", e)))?;
126
127 let _ = self.child.wait();
129 info!("MockForge process (PID: {}) terminated", self.pid);
130 } else {
131 debug!("Process (PID: {}) already exited", self.pid);
132 }
133 Ok(())
134 }
135}
136
137impl Drop for ManagedProcess {
138 fn drop(&mut self) {
139 if let Err(e) = self.kill() {
140 warn!("Failed to kill process on drop: {}", e);
141 }
142 }
143}
144
145fn find_mockforge_binary(config: &ServerConfig) -> Result<PathBuf> {
158 if let Some(binary_path) = &config.binary_path {
159 if binary_path.exists() {
160 return Ok(binary_path.clone());
161 }
162 return Err(Error::BinaryNotFound);
163 }
164
165 if let Ok(env_path) = std::env::var("MOCKFORGE_TEST_BINARY") {
166 let p = PathBuf::from(env_path);
167 if p.exists() {
168 return Ok(p);
169 }
170 }
171
172 if let Some(p) = workspace_target_binary() {
173 return Ok(p);
174 }
175
176 which::which("mockforge")
177 .map_err(|_| Error::BinaryNotFound)
178 .map(|p| p.to_path_buf())
179}
180
181fn workspace_target_binary() -> Option<PathBuf> {
195 let target_dir = std::env::var_os("CARGO_TARGET_DIR")
196 .map(PathBuf::from)
197 .or_else(target_dir_from_manifest)
198 .or_else(target_dir_from_current_exe)?;
199
200 let debug = target_dir.join("debug").join("mockforge");
201 if debug.exists() {
202 return Some(debug);
203 }
204 let release = target_dir.join("release").join("mockforge");
205 if release.exists() {
206 return Some(release);
207 }
208 None
209}
210
211fn target_dir_from_manifest() -> Option<PathBuf> {
212 let manifest_dir = std::env::var_os("CARGO_MANIFEST_DIR").map(PathBuf::from)?;
213 let mut dir: &std::path::Path = &manifest_dir;
214 loop {
215 let candidate = dir.join("target");
216 if candidate.is_dir() {
217 return Some(candidate);
218 }
219 dir = dir.parent()?;
220 }
221}
222
223fn target_dir_from_current_exe() -> Option<PathBuf> {
224 let exe = std::env::current_exe().ok()?;
225 let mut dir = exe.parent()?;
226 loop {
227 if dir.file_name() == Some(std::ffi::OsStr::new("target")) {
228 return Some(dir.to_path_buf());
229 }
230 dir = dir.parent()?;
231 }
232}
233
234pub fn is_port_available(port: u16) -> bool {
236 use std::net::TcpListener;
237 TcpListener::bind(("127.0.0.1", port)).is_ok()
238}
239
240pub fn find_available_port(_start_port: u16) -> Result<u16> {
261 use std::net::TcpListener;
262 let listener = TcpListener::bind("127.0.0.1:0")
263 .map_err(|e| Error::ConfigError(format!("OS-assigned port bind failed: {}", e)))?;
264 let port = listener
265 .local_addr()
266 .map_err(|e| Error::ConfigError(format!("Failed to read OS-assigned port: {}", e)))?
267 .port();
268 drop(listener);
269 Ok(port)
270}
271
272#[cfg(test)]
273mod tests {
274 use super::*;
275
276 #[test]
277 fn test_is_port_available() {
278 assert!(is_port_available(0));
280 }
281
282 #[test]
283 fn test_find_available_port_returns_nonzero() {
284 let port = find_available_port(30000).expect("Failed to find available port");
289 assert!(port > 0);
290 }
291
292 #[test]
293 fn test_find_available_port_ignores_hint() {
294 let port1 = find_available_port(30000).expect("Failed to find port 1");
297 let port2 = find_available_port(30000).expect("Failed to find port 2");
298 assert_ne!(port1, port2, "ephemeral allocator handed back the same port twice");
299 }
300
301 #[test]
302 fn test_is_port_available_high_port() {
303 let available = is_port_available(59999);
305 let _ = available;
308 }
309
310 #[test]
311 fn test_multiple_port_allocations() {
312 let port1 = find_available_port(31000).expect("Failed to find port 1");
314 let port2 = find_available_port(32000).expect("Failed to find port 2");
315 let port3 = find_available_port(33000).expect("Failed to find port 3");
316
317 assert!(port1 > 0);
318 assert!(port2 > 0);
319 assert!(port3 > 0);
320 assert_ne!(port1, port2);
321 assert_ne!(port1, port3);
322 assert_ne!(port2, port3);
323 }
324}