1use crate::error::{GoblinError, Result};
2use crate::script::Script;
3use async_trait::async_trait;
4use std::collections::HashMap;
5use std::process::Stdio;
6use tokio::process::Command;
7use tokio::time::{timeout, Duration};
8use tracing::{debug, info, warn};
9
10#[derive(Debug, Clone)]
12pub struct ExecutionResult {
13 pub script_name: String,
14 pub stdout: String,
15 pub stderr: String,
16 pub exit_code: i32,
17 pub duration: Duration,
18}
19
20impl ExecutionResult {
21 pub fn is_success(&self) -> bool {
23 self.exit_code == 0
24 }
25
26 pub fn get_output(&self) -> String {
28 if self.stdout.trim().is_empty() && !self.stderr.trim().is_empty() {
29 self.stderr.clone()
30 } else {
31 self.stdout.clone()
32 }
33 }
34}
35
36#[async_trait]
38pub trait Executor {
39 async fn execute_script(&self, script: &Script, args: &[String]) -> Result<ExecutionResult>;
41
42 async fn run_test(&self, script: &Script) -> Result<bool>;
44}
45
46pub struct DefaultExecutor {
48 environment: HashMap<String, String>,
50}
51
52impl DefaultExecutor {
53 pub fn new() -> Self {
55 Self {
56 environment: HashMap::new(),
57 }
58 }
59
60 pub fn with_environment(environment: HashMap<String, String>) -> Self {
62 Self { environment }
63 }
64
65 pub fn add_env(&mut self, key: String, value: String) {
67 self.environment.insert(key, value);
68 }
69
70 fn parse_command(command: &str) -> (String, Vec<String>) {
72 let parts: Vec<&str> = command.split_whitespace().collect();
73 if parts.is_empty() {
74 return (String::new(), Vec::new());
75 }
76
77 let program = parts[0].to_string();
78 let args = parts[1..].iter().map(|s| s.to_string()).collect();
79
80 (program, args)
81 }
82
83 fn create_command(&self, script: &Script, args: &[String]) -> Command {
85 let (program, cmd_args) = Self::parse_command(&script.command);
87
88 let mut command = Command::new(program);
89 command.args(cmd_args);
91 command.args(args);
93 command.current_dir(script.working_directory());
94 command.stdout(Stdio::piped());
95 command.stderr(Stdio::piped());
96 command.stdin(Stdio::null());
97
98 for (key, value) in &self.environment {
100 command.env(key, value);
101 }
102
103 command
104 }
105
106 async fn execute_command_with_timeout(
108 &self,
109 mut command: Command,
110 script_timeout: Duration,
111 script_name: &str,
112 ) -> Result<ExecutionResult> {
113 let start_time = std::time::Instant::now();
114
115 debug!("Executing command for script: {}", script_name);
116
117 let child = command
118 .spawn()
119 .map_err(|e| GoblinError::script_execution_failed(script_name, format!("Failed to spawn process: {}", e)))?;
120
121 let output = timeout(script_timeout, child.wait_with_output()).await
122 .map_err(|_| GoblinError::script_timeout(script_name, script_timeout))?
123 .map_err(|e| GoblinError::script_execution_failed(script_name, format!("Process error: {}", e)))?;
124
125 let duration = start_time.elapsed();
126 let duration = Duration::from_millis(duration.as_millis() as u64);
127
128 let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
129 let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
130 let exit_code = output.status.code().unwrap_or(-1);
131
132 debug!(
133 "Script {} completed in {:?} with exit code: {}",
134 script_name, duration, exit_code
135 );
136
137 Ok(ExecutionResult {
138 script_name: script_name.to_string(),
139 stdout,
140 stderr,
141 exit_code,
142 duration,
143 })
144 }
145}
146
147impl Default for DefaultExecutor {
148 fn default() -> Self {
149 Self::new()
150 }
151}
152
153#[async_trait]
154impl Executor for DefaultExecutor {
155 async fn execute_script(&self, script: &Script, args: &[String]) -> Result<ExecutionResult> {
156 info!("Executing script: {} with args: {:?}", script.name, args);
157
158 script.validate()?;
160
161 if script.require_test {
163 info!("Running required test for script: {}", script.name);
164 let test_passed = self.run_test(script).await?;
165 if !test_passed {
166 return Err(GoblinError::test_failed(&script.name));
167 }
168 info!("Test passed for script: {}", script.name);
169 }
170
171 let command = self.create_command(script, args);
172 let result = self
173 .execute_command_with_timeout(command, script.timeout, &script.name)
174 .await?;
175
176 if !result.is_success() {
177 let error_msg = if !result.stderr.is_empty() {
178 result.stderr.clone()
179 } else {
180 format!("Script exited with code: {}", result.exit_code)
181 };
182 return Err(GoblinError::script_execution_failed(&script.name, error_msg));
183 }
184
185 info!(
186 "Script {} completed successfully in {:?}",
187 script.name, result.duration
188 );
189
190 Ok(result)
191 }
192
193 async fn run_test(&self, script: &Script) -> Result<bool> {
194 let test_command = match script.get_test_command() {
195 Some(cmd) => cmd,
196 None => {
197 warn!("No test command configured for script: {}", script.name);
198 return Ok(true); }
200 };
201
202 debug!("Running test for script: {}", script.name);
203
204 let (program, args) = Self::parse_command(test_command);
205 let mut command = Command::new(program);
206 command.args(args);
207 command.current_dir(script.working_directory());
208 command.stdout(Stdio::piped());
209 command.stderr(Stdio::piped());
210
211 for (key, value) in &self.environment {
213 command.env(key, value);
214 }
215
216 let test_timeout = Duration::min(script.timeout, Duration::from_secs(30));
218
219 let result = self
220 .execute_command_with_timeout(command, test_timeout, &format!("{}_test", script.name))
221 .await?;
222
223 let test_passed = result.is_success()
225 && (result.stdout.to_lowercase().contains("true") || result.stdout.trim().is_empty());
226
227 if test_passed {
228 debug!("Test passed for script: {}", script.name);
229 } else {
230 debug!(
231 "Test failed for script: {} (exit_code: {}, stdout: '{}', stderr: '{}')",
232 script.name, result.exit_code, result.stdout, result.stderr
233 );
234 }
235
236 Ok(test_passed)
237 }
238}
239
240#[cfg(test)]
242pub struct MockExecutor {
243 pub results: HashMap<String, ExecutionResult>,
244 pub test_results: HashMap<String, bool>,
245}
246
247#[cfg(test)]
248impl MockExecutor {
249 pub fn new() -> Self {
250 Self {
251 results: HashMap::new(),
252 test_results: HashMap::new(),
253 }
254 }
255
256 pub fn add_result(&mut self, script_name: String, result: ExecutionResult) {
257 self.results.insert(script_name, result);
258 }
259
260 pub fn add_test_result(&mut self, script_name: String, passes: bool) {
261 self.test_results.insert(script_name, passes);
262 }
263}
264
265#[cfg(test)]
266#[async_trait]
267impl Executor for MockExecutor {
268 async fn execute_script(&self, script: &Script, _args: &[String]) -> Result<ExecutionResult> {
269 self.results
270 .get(&script.name)
271 .cloned()
272 .ok_or_else(|| GoblinError::script_not_found(&script.name))
273 }
274
275 async fn run_test(&self, script: &Script) -> Result<bool> {
276 Ok(self.test_results.get(&script.name).copied().unwrap_or(true))
277 }
278}
279
280#[cfg(test)]
281mod tests {
282 use super::*;
283 use crate::script::{Script, ScriptConfig};
284 use std::path::PathBuf;
285
286 #[tokio::test]
287 async fn test_mock_executor() {
288 let mut executor = MockExecutor::new();
289
290 let result = ExecutionResult {
291 script_name: "test_script".to_string(),
292 stdout: "Hello, World!".to_string(),
293 stderr: String::new(),
294 exit_code: 0,
295 duration: Duration::from_millis(100),
296 };
297
298 executor.add_result("test_script".to_string(), result.clone());
299
300 let config = ScriptConfig {
301 name: "test_script".to_string(),
302 command: "echo Hello".to_string(),
303 timeout: 30,
304 test_command: None,
305 require_test: false,
306 };
307
308 let script = Script::new(config, PathBuf::new());
309 let exec_result = executor.execute_script(&script, &[]).await.unwrap();
310
311 assert_eq!(exec_result.script_name, "test_script");
312 assert_eq!(exec_result.stdout, "Hello, World!");
313 assert_eq!(exec_result.exit_code, 0);
314 assert!(exec_result.is_success());
315 }
316
317 #[test]
318 fn test_parse_command() {
319 let (program, args) = DefaultExecutor::parse_command("deno run --allow-all main.ts");
320 assert_eq!(program, "deno");
321 assert_eq!(args, vec!["run", "--allow-all", "main.ts"]);
322
323 let (program, args) = DefaultExecutor::parse_command("echo hello");
324 assert_eq!(program, "echo");
325 assert_eq!(args, vec!["hello"]);
326
327 let (program, args) = DefaultExecutor::parse_command("simple_command");
328 assert_eq!(program, "simple_command");
329 assert!(args.is_empty());
330 }
331
332 #[test]
333 fn test_execution_result() {
334 let result = ExecutionResult {
335 script_name: "test".to_string(),
336 stdout: "output".to_string(),
337 stderr: String::new(),
338 exit_code: 0,
339 duration: Duration::from_millis(100),
340 };
341
342 assert!(result.is_success());
343 assert_eq!(result.get_output(), "output");
344
345 let result_with_error = ExecutionResult {
346 script_name: "test".to_string(),
347 stdout: String::new(),
348 stderr: "error output".to_string(),
349 exit_code: 1,
350 duration: Duration::from_millis(100),
351 };
352
353 assert!(!result_with_error.is_success());
354 assert_eq!(result_with_error.get_output(), "error output");
355 }
356}