cuenv_ci/report/
terminal.rs1use async_trait::async_trait;
7use std::sync::RwLock;
8
9use super::{
10 PipelineReport,
11 progress::{LivePipelineProgress, LiveTaskProgress, LiveTaskStatus, ProgressReporter},
12};
13
14pub struct TerminalReporter {
19 progress: RwLock<Option<LivePipelineProgress>>,
21 verbose: bool,
23}
24
25impl TerminalReporter {
26 #[must_use]
28 pub fn new() -> Self {
29 Self {
30 progress: RwLock::new(None),
31 verbose: false,
32 }
33 }
34
35 #[must_use]
37 pub fn verbose() -> Self {
38 Self {
39 progress: RwLock::new(None),
40 verbose: true,
41 }
42 }
43
44 fn format_task_line(progress: &LiveTaskProgress) -> String {
46 let icon = progress.status.icon();
47 let duration = progress
48 .duration
49 .map(|d| format!(" ({:.2}s)", d.as_secs_f64()))
50 .unwrap_or_default();
51
52 format!("{} {}{}", icon, progress.name, duration)
53 }
54}
55
56impl Default for TerminalReporter {
57 fn default() -> Self {
58 Self::new()
59 }
60}
61
62#[async_trait]
63impl ProgressReporter for TerminalReporter {
64 async fn pipeline_started(&self, name: &str, task_count: usize) {
65 let progress = LivePipelineProgress::new(name, task_count);
66 if let Ok(mut guard) = self.progress.write() {
67 *guard = Some(progress);
68 }
69
70 tracing::info!(pipeline = name, tasks = task_count, "Starting CI pipeline");
71 }
72
73 async fn task_started(&self, task_id: &str, task_name: &str) {
74 if let Ok(mut guard) = self.progress.write()
75 && let Some(ref mut progress) = *guard
76 {
77 let task = LiveTaskProgress::pending(task_id, task_name).running();
78 progress.tasks.push(task);
79 }
80
81 if self.verbose {
82 tracing::info!(task = task_id, "Starting task: {}", task_name);
83 }
84 }
85
86 async fn task_completed(&self, task_progress: &LiveTaskProgress) {
87 if let Ok(mut guard) = self.progress.write()
88 && let Some(ref mut progress) = *guard
89 {
90 progress.completed_tasks += 1;
91 if task_progress.status == LiveTaskStatus::Cached {
92 progress.cached_tasks += 1;
93 }
94
95 if let Some(task) = progress.tasks.iter_mut().find(|t| t.id == task_progress.id) {
97 *task = task_progress.clone();
98 }
99 }
100
101 let line = Self::format_task_line(task_progress);
102 match task_progress.status {
103 LiveTaskStatus::Success => {
104 tracing::info!(task = %task_progress.id, "{}", line);
105 }
106 LiveTaskStatus::Failed => {
107 if let Some(ref error) = task_progress.error {
108 tracing::error!(task = %task_progress.id, error = %error, "{}", line);
109 } else {
110 tracing::error!(task = %task_progress.id, "{}", line);
111 }
112 }
113 LiveTaskStatus::Cached => {
114 tracing::info!(task = %task_progress.id, "{} (cached)", line);
115 }
116 _ => {
117 tracing::info!(task = %task_progress.id, "{}", line);
118 }
119 }
120 }
121
122 async fn task_cached(&self, task_id: &str, task_name: &str) {
123 if let Ok(mut guard) = self.progress.write()
124 && let Some(ref mut progress) = *guard
125 {
126 progress.completed_tasks += 1;
127 progress.cached_tasks += 1;
128
129 let task = LiveTaskProgress::pending(task_id, task_name).cached();
130 progress.tasks.push(task);
131 }
132
133 tracing::info!(
134 task = task_id,
135 "{} {} (cached)",
136 LiveTaskStatus::Cached.icon(),
137 task_name
138 );
139 }
140
141 async fn task_progress(&self, task_id: &str, message: &str) {
142 if self.verbose {
143 tracing::debug!(task = task_id, "{}", message);
144 }
145 }
146
147 #[allow(clippy::cast_precision_loss)] async fn pipeline_completed(&self, report: &PipelineReport) {
149 let total = report.tasks.len();
150 let failed = report
151 .tasks
152 .iter()
153 .filter(|t| t.status == super::TaskStatus::Failed)
154 .count();
155 let cached = report.cache_hits();
156 let duration_secs = report.duration_ms.map_or(0.0, |ms| ms as f64 / 1000.0);
157
158 if report.status == super::PipelineStatus::Success {
159 tracing::info!(
160 pipeline = %report.pipeline,
161 total = total,
162 cached = cached,
163 duration = format!("{:.2}s", duration_secs),
164 "Pipeline completed successfully"
165 );
166 } else {
167 tracing::error!(
168 pipeline = %report.pipeline,
169 total = total,
170 failed = failed,
171 duration = format!("{:.2}s", duration_secs),
172 "Pipeline failed"
173 );
174 }
175 }
176}
177
178#[cfg(test)]
179#[allow(clippy::unwrap_used)] mod tests {
181 use super::*;
182 use std::time::Duration;
183
184 #[test]
185 fn test_terminal_reporter_new() {
186 let reporter = TerminalReporter::new();
187 assert!(!reporter.verbose);
188 }
189
190 #[test]
191 fn test_terminal_reporter_verbose() {
192 let reporter = TerminalReporter::verbose();
193 assert!(reporter.verbose);
194 }
195
196 #[test]
197 fn test_terminal_reporter_default() {
198 let reporter = TerminalReporter::default();
199 assert!(!reporter.verbose);
200 }
201
202 #[test]
203 fn test_format_task_line_success() {
204 let progress = LiveTaskProgress::pending("build", "Build project")
205 .completed(true, Duration::from_secs(5));
206 let line = TerminalReporter::format_task_line(&progress);
207 assert!(line.contains("Build project"));
208 assert!(line.contains("5.00s"));
209 }
210
211 #[test]
212 fn test_format_task_line_no_duration() {
213 let progress = LiveTaskProgress::pending("build", "Build project");
214 let line = TerminalReporter::format_task_line(&progress);
215 assert!(line.contains("Build project"));
216 assert!(!line.contains("s)")); }
218
219 #[tokio::test]
220 async fn test_terminal_reporter_pipeline_lifecycle() {
221 let reporter = TerminalReporter::new();
222
223 reporter.pipeline_started("test", 2).await;
224
225 {
226 let guard = reporter.progress.read().unwrap();
227 let progress = guard.as_ref().unwrap();
228 assert_eq!(progress.name, "test");
229 assert_eq!(progress.total_tasks, 2);
230 }
231
232 reporter.task_started("t1", "Task 1").await;
233
234 {
235 let guard = reporter.progress.read().unwrap();
236 let progress = guard.as_ref().unwrap();
237 assert_eq!(progress.tasks.len(), 1);
238 assert_eq!(progress.tasks[0].status, LiveTaskStatus::Running);
239 }
240
241 let task =
242 LiveTaskProgress::pending("t1", "Task 1").completed(true, Duration::from_secs(1));
243 reporter.task_completed(&task).await;
244
245 {
246 let guard = reporter.progress.read().unwrap();
247 let progress = guard.as_ref().unwrap();
248 assert_eq!(progress.completed_tasks, 1);
249 }
250 }
251
252 #[tokio::test]
253 async fn test_terminal_reporter_cached_task() {
254 let reporter = TerminalReporter::new();
255
256 reporter.pipeline_started("test", 1).await;
257 reporter.task_cached("t1", "Task 1").await;
258
259 {
260 let guard = reporter.progress.read().unwrap();
261 let progress = guard.as_ref().unwrap();
262 assert_eq!(progress.completed_tasks, 1);
263 assert_eq!(progress.cached_tasks, 1);
264 }
265 }
266}