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> {
147 if let Some(binary_path) = &config.binary_path {
149 if binary_path.exists() {
150 return Ok(binary_path.clone());
151 }
152 return Err(Error::BinaryNotFound);
153 }
154
155 which::which("mockforge")
157 .map_err(|_| Error::BinaryNotFound)
158 .map(|p| p.to_path_buf())
159}
160
161pub fn is_port_available(port: u16) -> bool {
163 use std::net::TcpListener;
164 TcpListener::bind(("127.0.0.1", port)).is_ok()
165}
166
167pub fn find_available_port(start_port: u16) -> Result<u16> {
169 for port in start_port..start_port + 100 {
170 if is_port_available(port) {
171 return Ok(port);
172 }
173 }
174 Err(Error::ConfigError(format!(
175 "No available ports found in range {}-{}",
176 start_port,
177 start_port + 100
178 )))
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184
185 #[test]
186 fn test_is_port_available() {
187 assert!(is_port_available(0));
189 }
190
191 #[test]
192 fn test_find_available_port() {
193 let port = find_available_port(30000).expect("Failed to find available port");
195 assert!(port >= 30000);
196 assert!(port < 30100);
197 }
198}