mockforge_test/
process.rs

1//! Process management for MockForge servers
2
3use crate::config::ServerConfig;
4use crate::error::{Error, Result};
5use std::path::PathBuf;
6use std::process::{Child, Command, Stdio};
7use tracing::{debug, info, warn};
8
9/// Managed MockForge process
10pub struct ManagedProcess {
11    child: Child,
12    http_port: u16,
13    pid: u32,
14}
15
16impl ManagedProcess {
17    /// Spawn a new MockForge server process
18    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        // Configure ports
26        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        // Configure admin UI
45        if config.enable_admin {
46            cmd.arg("--admin");
47        }
48
49        // Configure metrics
50        if config.enable_metrics {
51            cmd.arg("--metrics");
52        }
53
54        // Configure spec file
55        if let Some(spec_file) = &config.spec_file {
56            cmd.arg("--spec").arg(spec_file);
57        }
58
59        // Configure workspace
60        if let Some(workspace_dir) = &config.workspace_dir {
61            cmd.arg("--workspace-dir").arg(workspace_dir);
62        }
63
64        // Configure profile
65        if let Some(profile) = &config.profile {
66            cmd.arg("--profile").arg(profile);
67        }
68
69        // Add extra arguments
70        for arg in &config.extra_args {
71            cmd.arg(arg);
72        }
73
74        // Set working directory
75        if let Some(working_dir) = &config.working_dir {
76            cmd.current_dir(working_dir);
77        }
78
79        // Set environment variables
80        for (key, value) in &config.env_vars {
81            cmd.env(key, value);
82        }
83
84        // Configure stdio - use inherit() for testing to see actual output
85        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    /// Get the HTTP port the server is running on
105    pub fn http_port(&self) -> u16 {
106        self.http_port
107    }
108
109    /// Get the process ID
110    pub fn pid(&self) -> u32 {
111        self.pid
112    }
113
114    /// Check if the process is still running
115    pub fn is_running(&mut self) -> bool {
116        matches!(self.child.try_wait(), Ok(None))
117    }
118
119    /// Kill the process
120    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            // Wait for the process to exit
128            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
145/// Find the MockForge binary
146fn find_mockforge_binary(config: &ServerConfig) -> Result<PathBuf> {
147    // If binary path is explicitly provided, use it
148    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    // Try to find mockforge in PATH
156    which::which("mockforge")
157        .map_err(|_| Error::BinaryNotFound)
158        .map(|p| p.to_path_buf())
159}
160
161/// Check if a port is available
162pub fn is_port_available(port: u16) -> bool {
163    use std::net::TcpListener;
164    TcpListener::bind(("127.0.0.1", port)).is_ok()
165}
166
167/// Find an available port starting from a given port
168pub 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        // Port 0 should always be available (it means "assign any port")
188        assert!(is_port_available(0));
189    }
190
191    #[test]
192    fn test_find_available_port() {
193        // Should find a port starting from 30000
194        let port = find_available_port(30000).expect("Failed to find available port");
195        assert!(port >= 30000);
196        assert!(port < 30100);
197    }
198}