ggen_e2e/
runner.rs

1//! Test execution orchestration
2//!
3//! Manages test execution on different platforms (native macOS, container Linux).
4
5use 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/// Output from a ggen sync execution
16#[derive(Debug, Clone)]
17pub struct SyncOutput {
18    /// Exit code
19    pub exit_code: i32,
20    /// Standard output
21    pub stdout: String,
22    /// Standard error
23    pub stderr: String,
24}
25
26impl SyncOutput {
27    /// Check if execution was successful
28    pub fn is_success(&self) -> bool {
29        self.exit_code == 0
30    }
31
32    /// Create a successful output
33    pub fn success(stdout: String, stderr: String) -> Self {
34        SyncOutput {
35            exit_code: 0,
36            stdout,
37            stderr,
38        }
39    }
40
41    /// Create a failed output
42    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/// Trait for executing ggen in different environments
52#[async_trait]
53pub trait GgenExecutor: Send + Sync {
54    /// Execute ggen sync in this environment
55    async fn execute(&self, project_dir: &Path) -> Result<SyncOutput>;
56    /// Get the platform this executor runs on
57    fn platform(&self) -> &Platform;
58}
59
60/// Native executor for macOS
61pub struct NativeExecutor {
62    platform: Platform,
63    ggen_path: std::path::PathBuf,
64}
65
66impl NativeExecutor {
67    /// Create a new native executor
68    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        // Execute native ggen sync on macOS
80        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
102/// Container executor for Linux via testcontainers
103pub struct ContainerExecutor {
104    platform: Platform,
105    // Container config will be added in Phase 4
106}
107
108impl ContainerExecutor {
109    /// Create a new container executor
110    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        // Execute ggen sync in container
119        // Full implementation in Phase 4 with testcontainers
120        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
131/// Main test runner orchestrating execution across platforms
132pub struct TestRunner {
133    pub platform: Platform,
134    pub timeout: Duration,
135    pub retry_count: u32,
136}
137
138impl TestRunner {
139    /// Create a new test runner for the current platform
140    pub fn new(platform: Platform) -> Self {
141        TestRunner {
142            platform,
143            timeout: Duration::from_secs(300), // 5 minutes default
144            retry_count: 0,
145        }
146    }
147
148    /// Set execution timeout
149    pub fn with_timeout(mut self, timeout: Duration) -> Self {
150        self.timeout = timeout;
151        self
152    }
153
154    /// Set retry count
155    pub fn with_retry_count(mut self, count: u32) -> Self {
156        self.retry_count = count;
157        self
158    }
159
160    /// Run a fixture test with orchestration (T018)
161    pub async fn run_test(&self, fixture: &TestFixture) -> Result<TestResult> {
162        let mut execution = TestExecution::new(&fixture.name, self.platform.clone());
163
164        // Start timing
165        let start = std::time::Instant::now();
166
167        // Validate fixture before running
168        fixture.validate()?;
169
170        // Copy fixture to temporary directory
171        let temp_dir = fixture.copy_to_temp()?;
172
173        // For now, just report that execution was attempted
174        // Phase 3 will implement proper execution with NativeExecutor/ContainerExecutor
175        execution.finish();
176
177        // Load golden files for comparison
178        let golden_files = fixture.golden_files()?;
179
180        if golden_files.is_empty() {
181            // No golden files yet - return skipped status
182            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        // For Phase 3 MVP, return a passing result if we got here
192        Ok(TestResult::passed(execution, vec![]))
193    }
194
195    /// Run with native executor (macOS)
196    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}