Skip to main content

cuenv_ci/executor/
mod.rs

1//! CI Pipeline Executor
2//!
3//! Orchestrates CI pipeline execution with provider integration.
4
5pub mod config;
6mod orchestrator;
7pub mod runner;
8pub mod secrets;
9
10pub use config::CIExecutorConfig;
11pub use orchestrator::run_ci;
12pub use runner::TaskOutput;
13pub use secrets::{EnvSecretResolver, MockSecretResolver, SaltConfig, SecretResolver};
14
15use thiserror::Error;
16
17/// Error types for CI execution
18#[derive(Debug, Error)]
19pub enum ExecutorError {
20    /// Compilation error
21    #[error("Failed to compile project to IR: {0}")]
22    Compilation(String),
23
24    /// Secret resolution error
25    #[error(transparent)]
26    Secret(#[from] secrets::SecretError),
27
28    /// Task execution error
29    #[error(transparent)]
30    Runner(#[from] runner::RunnerError),
31
32    /// Task panicked during execution
33    #[error("Task panicked: {0}")]
34    TaskPanic(String),
35
36    /// Pipeline not found
37    #[error("Pipeline '{name}' not found. Available: {available}")]
38    PipelineNotFound { name: String, available: String },
39
40    /// No CI configuration
41    #[error("Project has no CI configuration")]
42    NoCIConfig,
43}
44
45impl From<ExecutorError> for cuenv_core::Error {
46    fn from(err: ExecutorError) -> Self {
47        match err {
48            ExecutorError::Compilation(msg) => Self::configuration(msg),
49            ExecutorError::Secret(e) => Self::secret_resolution(e.to_string()),
50            ExecutorError::Runner(e) => Self::execution(e.to_string()),
51            ExecutorError::TaskPanic(msg) => Self::execution(format!("Task panicked: {msg}")),
52            ExecutorError::PipelineNotFound { name, available } => Self::configuration(format!(
53                "Pipeline '{name}' not found. Available: {available}"
54            )),
55            ExecutorError::NoCIConfig => Self::configuration("Project has no CI configuration"),
56        }
57    }
58}
59
60/// Result of pipeline execution
61#[derive(Debug)]
62pub struct PipelineResult {
63    /// Whether all tasks succeeded
64    pub success: bool,
65    /// Results for each task
66    pub tasks: Vec<TaskOutput>,
67    /// Total execution time in milliseconds
68    pub duration_ms: u64,
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74
75    // ==========================================================================
76    // ExecutorError display tests
77    // ==========================================================================
78
79    #[test]
80    fn test_executor_error_compilation_display() {
81        let err = ExecutorError::Compilation("Syntax error in line 5".to_string());
82        let msg = err.to_string();
83        assert!(msg.contains("Failed to compile"));
84        assert!(msg.contains("Syntax error in line 5"));
85    }
86
87    #[test]
88    fn test_executor_error_task_panic_display() {
89        let err = ExecutorError::TaskPanic("thread 'main' panicked".to_string());
90        let msg = err.to_string();
91        assert!(msg.contains("Task panicked"));
92        assert!(msg.contains("thread 'main' panicked"));
93    }
94
95    #[test]
96    fn test_executor_error_pipeline_not_found_display() {
97        let err = ExecutorError::PipelineNotFound {
98            name: "build".to_string(),
99            available: "default, test, deploy".to_string(),
100        };
101        let msg = err.to_string();
102        assert!(msg.contains("Pipeline 'build' not found"));
103        assert!(msg.contains("default, test, deploy"));
104    }
105
106    #[test]
107    fn test_executor_error_no_ci_config_display() {
108        let err = ExecutorError::NoCIConfig;
109        let msg = err.to_string();
110        assert!(msg.contains("has no CI configuration"));
111    }
112
113    // ==========================================================================
114    // PipelineResult tests
115    // ==========================================================================
116
117    #[test]
118    fn test_pipeline_result_fields() {
119        let result = PipelineResult {
120            success: true,
121            tasks: vec![TaskOutput::dry_run("task1".to_string())],
122            duration_ms: 1500,
123        };
124
125        assert!(result.success);
126        assert_eq!(result.tasks.len(), 1);
127        assert_eq!(result.duration_ms, 1500);
128    }
129
130    #[test]
131    fn test_pipeline_result_failed() {
132        let result = PipelineResult {
133            success: false,
134            tasks: vec![],
135            duration_ms: 0,
136        };
137
138        assert!(!result.success);
139    }
140
141    // ==========================================================================
142    // CIExecutorConfig builder tests
143    // ==========================================================================
144
145    #[test]
146    fn test_executor_config_builder() {
147        let config = CIExecutorConfig::new(std::path::PathBuf::from("/project"))
148            .with_max_parallel(8)
149            .with_dry_run(cuenv_core::DryRun::Yes);
150
151        assert_eq!(config.max_parallel, 8);
152        assert!(config.dry_run.is_dry_run());
153    }
154
155    #[test]
156    fn test_executor_config_default() {
157        let config = CIExecutorConfig::default();
158        assert!(!config.dry_run.is_dry_run());
159        assert!(config.max_parallel >= 1);
160    }
161
162    #[test]
163    fn test_executor_config_with_capture_output() {
164        let config = CIExecutorConfig::new(std::path::PathBuf::from("/project"))
165            .with_capture_output(cuenv_core::OutputCapture::Capture);
166
167        assert!(config.capture_output.should_capture());
168    }
169
170    // ==========================================================================
171    // TaskOutput helper tests
172    // ==========================================================================
173
174    #[test]
175    fn test_task_output_dry_run() {
176        let output = TaskOutput::dry_run("my-task".to_string());
177        assert!(output.success);
178        assert_eq!(output.task_id, "my-task");
179    }
180
181    #[test]
182    fn test_task_output_from_cache() {
183        let output = TaskOutput::from_cache("cached-task".to_string(), 500);
184        assert!(output.success);
185        assert_eq!(output.task_id, "cached-task");
186        assert_eq!(output.duration_ms, 500);
187    }
188}