1pub 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#[derive(Debug, Error)]
19pub enum ExecutorError {
20 #[error("Failed to compile project to IR: {0}")]
22 Compilation(String),
23
24 #[error(transparent)]
26 Secret(#[from] secrets::SecretError),
27
28 #[error(transparent)]
30 Runner(#[from] runner::RunnerError),
31
32 #[error("Task panicked: {0}")]
34 TaskPanic(String),
35
36 #[error("Pipeline '{name}' not found. Available: {available}")]
38 PipelineNotFound { name: String, available: String },
39
40 #[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#[derive(Debug)]
62pub struct PipelineResult {
63 pub success: bool,
65 pub tasks: Vec<TaskOutput>,
67 pub duration_ms: u64,
69}
70
71#[cfg(test)]
72mod tests {
73 use super::*;
74
75 #[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 #[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 #[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 #[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}