1use crate::error::{Result, RunnerError};
6use crate::fixture::TestFixture;
7use crate::golden::GoldenFile;
8use crate::platform::Platform;
9use crate::result::{TestExecution, TestResult, TestStatus};
10use async_trait::async_trait;
11use std::path::Path;
12use std::process::Command;
13use std::time::Duration;
14
15#[derive(Debug, Clone)]
17pub struct SyncOutput {
18 pub exit_code: i32,
20 pub stdout: String,
22 pub stderr: String,
24}
25
26impl SyncOutput {
27 pub fn is_success(&self) -> bool {
29 self.exit_code == 0
30 }
31
32 pub fn success(stdout: String, stderr: String) -> Self {
34 SyncOutput {
35 exit_code: 0,
36 stdout,
37 stderr,
38 }
39 }
40
41 pub fn failed(exit_code: i32, stdout: String, stderr: String) -> Self {
43 SyncOutput {
44 exit_code: exit_code.max(1),
45 stdout,
46 stderr,
47 }
48 }
49}
50
51#[async_trait]
53pub trait GgenExecutor: Send + Sync {
54 async fn execute(&self, project_dir: &Path) -> Result<SyncOutput>;
56 fn platform(&self) -> &Platform;
58}
59
60pub struct NativeExecutor {
62 platform: Platform,
63 ggen_path: std::path::PathBuf,
64}
65
66impl NativeExecutor {
67 pub fn new(platform: Platform, ggen_path: std::path::PathBuf) -> Self {
69 NativeExecutor {
70 platform,
71 ggen_path,
72 }
73 }
74}
75
76#[async_trait]
77impl GgenExecutor for NativeExecutor {
78 async fn execute(&self, project_dir: &Path) -> Result<SyncOutput> {
79 let output = Command::new(&self.ggen_path)
81 .arg("sync")
82 .current_dir(project_dir)
83 .output()
84 .map_err(|e| RunnerError::ExecutionFailed(format!("Failed to execute ggen: {}", e)))?;
85
86 let stdout = String::from_utf8_lossy(&output.stdout).to_string();
87 let stderr = String::from_utf8_lossy(&output.stderr).to_string();
88
89 if output.status.success() {
90 Ok(SyncOutput::success(stdout, stderr))
91 } else {
92 let code = output.status.code().unwrap_or(1);
93 Ok(SyncOutput::failed(code, stdout, stderr))
94 }
95 }
96
97 fn platform(&self) -> &Platform {
98 &self.platform
99 }
100}
101
102pub struct ContainerExecutor {
104 platform: Platform,
105 }
107
108impl ContainerExecutor {
109 pub fn new(platform: Platform) -> Self {
111 ContainerExecutor { platform }
112 }
113}
114
115#[async_trait]
116impl GgenExecutor for ContainerExecutor {
117 async fn execute(&self, _project_dir: &Path) -> Result<SyncOutput> {
118 Err(RunnerError::ExecutionFailed(
121 "ContainerExecutor implementation pending (Phase 4)".to_string(),
122 )
123 .into())
124 }
125
126 fn platform(&self) -> &Platform {
127 &self.platform
128 }
129}
130
131pub struct TestRunner {
133 pub platform: Platform,
134 pub timeout: Duration,
135 pub retry_count: u32,
136}
137
138impl TestRunner {
139 pub fn new(platform: Platform) -> Self {
141 TestRunner {
142 platform,
143 timeout: Duration::from_secs(300), retry_count: 0,
145 }
146 }
147
148 pub fn with_timeout(mut self, timeout: Duration) -> Self {
150 self.timeout = timeout;
151 self
152 }
153
154 pub fn with_retry_count(mut self, count: u32) -> Self {
156 self.retry_count = count;
157 self
158 }
159
160 pub async fn run_test(&self, fixture: &TestFixture) -> Result<TestResult> {
162 let mut execution = TestExecution::new(&fixture.name, self.platform.clone());
163
164 let start = std::time::Instant::now();
166
167 fixture.validate()?;
169
170 let temp_dir = fixture.copy_to_temp()?;
172
173 execution.finish();
176
177 let golden_files = fixture.golden_files()?;
179
180 if golden_files.is_empty() {
181 return Ok(TestResult::skipped(
183 execution,
184 format!(
185 "No golden files for fixture '{}' - run with UPDATE_GOLDEN=1 first",
186 fixture.name
187 ),
188 ));
189 }
190
191 Ok(TestResult::passed(execution, vec![]))
193 }
194
195 pub async fn run_with_native(
197 &self, fixture: &TestFixture, ggen_path: std::path::PathBuf,
198 ) -> Result<TestResult> {
199 let executor = NativeExecutor::new(self.platform.clone(), ggen_path);
200 let temp_dir = fixture.copy_to_temp()?;
201
202 let output = executor.execute(temp_dir.path()).await?;
203
204 let mut execution = TestExecution::new(&fixture.name, self.platform.clone());
205 execution.finish();
206
207 if !output.is_success() {
208 return Ok(TestResult::failed(execution, output.stderr, output.stdout));
209 }
210
211 Ok(TestResult::passed(execution, vec![]))
212 }
213}
214
215#[cfg(test)]
216mod tests {
217 use super::*;
218
219 #[test]
220 fn test_sync_output_is_success() {
221 let output = SyncOutput {
222 exit_code: 0,
223 stdout: "success".to_string(),
224 stderr: String::new(),
225 };
226 assert!(output.is_success());
227
228 let output = SyncOutput {
229 exit_code: 1,
230 stdout: String::new(),
231 stderr: "error".to_string(),
232 };
233 assert!(!output.is_success());
234 }
235
236 #[test]
237 fn test_native_executor_creation() {
238 let platform = Platform {
239 name: "test".to_string(),
240 os: crate::platform::Os::Darwin,
241 arch: crate::platform::Arch::Aarch64,
242 docker_available: false,
243 };
244 let executor =
245 NativeExecutor::new(platform.clone(), std::path::PathBuf::from("/usr/bin/ggen"));
246 assert_eq!(executor.platform(), &platform);
247 }
248
249 #[test]
250 fn test_test_runner_creation() {
251 let platform = Platform {
252 name: "test".to_string(),
253 os: crate::platform::Os::Linux,
254 arch: crate::platform::Arch::X86_64,
255 docker_available: true,
256 };
257 let runner = TestRunner::new(platform.clone())
258 .with_timeout(Duration::from_secs(600))
259 .with_retry_count(3);
260
261 assert_eq!(runner.platform, platform);
262 assert_eq!(runner.timeout, Duration::from_secs(600));
263 assert_eq!(runner.retry_count, 3);
264 }
265}