1use crate::models::ExecutionPlan;
11use serde::{Deserialize, Serialize};
12use std::sync::{Arc, Mutex};
13use std::time::{Duration, Instant};
14use tracing::{debug, info};
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct ProgressUpdate {
19 pub current_step: usize,
21 pub total_steps: usize,
23 pub progress_percentage: f32,
25 pub estimated_time_remaining: Duration,
27 pub timestamp: chrono::DateTime<chrono::Utc>,
29}
30
31pub type ProgressCallback = Box<dyn Fn(ProgressUpdate) + Send + Sync>;
33
34pub struct ProgressTracker {
43 total_steps: usize,
45 current_step: usize,
47 completed_steps: usize,
49 start_time: Instant,
51 step_durations: Vec<Duration>,
53 callbacks: Arc<Mutex<Vec<ProgressCallback>>>,
55}
56
57impl ProgressTracker {
58 pub fn new(plan: &ExecutionPlan) -> Self {
66 let total_steps = plan.steps.len();
67
68 info!(total_steps = total_steps, "Creating progress tracker");
69
70 Self {
71 total_steps,
72 current_step: 0,
73 completed_steps: 0,
74 start_time: Instant::now(),
75 step_durations: Vec::new(),
76 callbacks: Arc::new(Mutex::new(Vec::new())),
77 }
78 }
79
80 pub fn on_progress<F>(&self, callback: F)
87 where
88 F: Fn(ProgressUpdate) + Send + Sync + 'static,
89 {
90 let mut callbacks = self.callbacks.lock().unwrap();
91 callbacks.push(Box::new(callback));
92
93 debug!(
94 callback_count = callbacks.len(),
95 "Progress callback registered"
96 );
97 }
98
99 pub fn step_completed(&mut self, step_duration: Duration) {
106 self.step_durations.push(step_duration);
107 self.completed_steps += 1;
108 self.current_step += 1;
109
110 debug!(
111 current_step = self.current_step,
112 completed_steps = self.completed_steps,
113 step_duration_ms = step_duration.as_millis(),
114 "Step completed"
115 );
116
117 self.notify_progress();
118 }
119
120 pub fn step_skipped(&mut self) {
124 self.step_durations.push(Duration::from_secs(0));
125 self.current_step += 1;
126
127 debug!(current_step = self.current_step, "Step skipped");
128
129 self.notify_progress();
130 }
131
132 pub fn get_progress(&self) -> ProgressUpdate {
137 let progress_percentage = if self.total_steps > 0 {
138 (self.completed_steps as f32 / self.total_steps as f32) * 100.0
139 } else {
140 0.0
141 };
142
143 let estimated_time_remaining = self.estimated_time_remaining();
144
145 ProgressUpdate {
146 current_step: self.current_step,
147 total_steps: self.total_steps,
148 progress_percentage,
149 estimated_time_remaining,
150 timestamp: chrono::Utc::now(),
151 }
152 }
153
154 pub fn current_step(&self) -> usize {
156 self.current_step
157 }
158
159 pub fn total_steps(&self) -> usize {
161 self.total_steps
162 }
163
164 pub fn completed_steps(&self) -> usize {
166 self.completed_steps
167 }
168
169 pub fn progress_percentage(&self) -> f32 {
171 if self.total_steps > 0 {
172 (self.completed_steps as f32 / self.total_steps as f32) * 100.0
173 } else {
174 0.0
175 }
176 }
177
178 pub fn estimated_time_remaining(&self) -> Duration {
180 if self.step_durations.is_empty() || self.completed_steps == 0 {
181 return Duration::from_secs(0);
183 }
184
185 let total_duration: Duration = self.step_durations.iter().sum();
187 let average_duration = total_duration / self.step_durations.len() as u32;
188
189 let remaining_steps = self.total_steps.saturating_sub(self.completed_steps);
191 average_duration * remaining_steps as u32
192 }
193
194 pub fn elapsed_time(&self) -> Duration {
196 self.start_time.elapsed()
197 }
198
199 pub fn average_step_duration(&self) -> Duration {
201 if self.step_durations.is_empty() {
202 return Duration::from_secs(0);
203 }
204
205 let total_duration: Duration = self.step_durations.iter().sum();
206 total_duration / self.step_durations.len() as u32
207 }
208
209 fn notify_progress(&self) {
211 let progress = self.get_progress();
212
213 let callbacks = self.callbacks.lock().unwrap();
214 for callback in callbacks.iter() {
215 callback(progress.clone());
216 }
217 }
218
219 pub fn reset(&mut self) {
223 self.current_step = 0;
224 self.completed_steps = 0;
225 self.start_time = Instant::now();
226 self.step_durations.clear();
227
228 debug!("Progress tracker reset");
229 }
230}
231
232impl Default for ProgressTracker {
233 fn default() -> Self {
234 Self {
235 total_steps: 0,
236 current_step: 0,
237 completed_steps: 0,
238 start_time: Instant::now(),
239 step_durations: Vec::new(),
240 callbacks: Arc::new(Mutex::new(Vec::new())),
241 }
242 }
243}
244
245#[cfg(test)]
246mod tests {
247 use super::*;
248 use crate::models::{ExecutionPlan, ExecutionStep, RiskScore, StepAction, StepStatus};
249 use std::sync::atomic::{AtomicUsize, Ordering};
250 use std::sync::Arc as StdArc;
251
252 fn create_test_plan(step_count: usize) -> ExecutionPlan {
253 let steps = (0..step_count)
254 .map(|i| ExecutionStep {
255 id: format!("step-{}", i),
256 description: format!("Step {}", i),
257 action: StepAction::RunCommand {
258 command: "echo".to_string(),
259 args: vec![format!("step {}", i)],
260 },
261 risk_score: RiskScore::default(),
262 dependencies: Vec::new(),
263 rollback_action: None,
264 status: StepStatus::Pending,
265 })
266 .collect();
267
268 ExecutionPlan::new("Test Plan".to_string(), steps)
269 }
270
271 #[test]
272 fn test_create_tracker() {
273 let plan = create_test_plan(5);
274 let tracker = ProgressTracker::new(&plan);
275
276 assert_eq!(tracker.total_steps(), 5);
277 assert_eq!(tracker.current_step(), 0);
278 assert_eq!(tracker.completed_steps(), 0);
279 assert_eq!(tracker.progress_percentage(), 0.0);
280 }
281
282 #[test]
283 fn test_step_completed() {
284 let plan = create_test_plan(5);
285 let mut tracker = ProgressTracker::new(&plan);
286
287 tracker.step_completed(Duration::from_secs(1));
288
289 assert_eq!(tracker.completed_steps(), 1);
290 assert_eq!(tracker.current_step(), 1);
291 assert_eq!(tracker.progress_percentage(), 20.0);
292 }
293
294 #[test]
295 fn test_multiple_steps_completed() {
296 let plan = create_test_plan(5);
297 let mut tracker = ProgressTracker::new(&plan);
298
299 tracker.step_completed(Duration::from_secs(1));
300 tracker.step_completed(Duration::from_secs(2));
301 tracker.step_completed(Duration::from_secs(1));
302
303 assert_eq!(tracker.completed_steps(), 3);
304 assert!((tracker.progress_percentage() - 60.0).abs() < 0.01);
305 }
306
307 #[test]
308 fn test_step_skipped() {
309 let plan = create_test_plan(5);
310 let mut tracker = ProgressTracker::new(&plan);
311
312 tracker.step_completed(Duration::from_secs(1));
313 tracker.step_skipped();
314
315 assert_eq!(tracker.completed_steps(), 1);
316 assert_eq!(tracker.current_step(), 2);
317 }
318
319 #[test]
320 fn test_progress_percentage() {
321 let plan = create_test_plan(10);
322 let mut tracker = ProgressTracker::new(&plan);
323
324 for _ in 0..5 {
325 tracker.step_completed(Duration::from_secs(1));
326 }
327
328 assert_eq!(tracker.progress_percentage(), 50.0);
329 }
330
331 #[test]
332 fn test_estimated_time_remaining() {
333 let plan = create_test_plan(10);
334 let mut tracker = ProgressTracker::new(&plan);
335
336 tracker.step_completed(Duration::from_secs(1));
338 tracker.step_completed(Duration::from_secs(1));
339
340 let estimated = tracker.estimated_time_remaining();
343 assert!(estimated.as_secs() >= 7 && estimated.as_secs() <= 9);
344 }
345
346 #[test]
347 fn test_average_step_duration() {
348 let plan = create_test_plan(5);
349 let mut tracker = ProgressTracker::new(&plan);
350
351 tracker.step_completed(Duration::from_secs(2));
352 tracker.step_completed(Duration::from_secs(4));
353
354 let average = tracker.average_step_duration();
355 assert_eq!(average, Duration::from_secs(3));
356 }
357
358 #[test]
359 fn test_elapsed_time() {
360 let plan = create_test_plan(5);
361 let tracker = ProgressTracker::new(&plan);
362
363 let elapsed = tracker.elapsed_time();
364 let _ = elapsed;
366 }
367
368 #[test]
369 fn test_progress_callback() {
370 let plan = create_test_plan(5);
371 let mut tracker = ProgressTracker::new(&plan);
372
373 let callback_count = StdArc::new(AtomicUsize::new(0));
374 let callback_count_clone = callback_count.clone();
375
376 tracker.on_progress(move |_progress| {
377 callback_count_clone.fetch_add(1, Ordering::SeqCst);
378 });
379
380 tracker.step_completed(Duration::from_secs(1));
381 tracker.step_completed(Duration::from_secs(1));
382
383 assert_eq!(callback_count.load(Ordering::SeqCst), 2);
384 }
385
386 #[test]
387 fn test_get_progress() {
388 let plan = create_test_plan(5);
389 let mut tracker = ProgressTracker::new(&plan);
390
391 tracker.step_completed(Duration::from_secs(1));
392
393 let progress = tracker.get_progress();
394 assert_eq!(progress.current_step, 1);
395 assert_eq!(progress.total_steps, 5);
396 assert_eq!(progress.progress_percentage, 20.0);
397 }
398
399 #[test]
400 fn test_reset() {
401 let plan = create_test_plan(5);
402 let mut tracker = ProgressTracker::new(&plan);
403
404 tracker.step_completed(Duration::from_secs(1));
405 tracker.step_completed(Duration::from_secs(1));
406
407 tracker.reset();
408
409 assert_eq!(tracker.completed_steps(), 0);
410 assert_eq!(tracker.current_step(), 0);
411 assert_eq!(tracker.progress_percentage(), 0.0);
412 }
413
414 #[test]
415 fn test_empty_plan() {
416 let plan = create_test_plan(0);
417 let tracker = ProgressTracker::new(&plan);
418
419 assert_eq!(tracker.total_steps(), 0);
420 assert_eq!(tracker.progress_percentage(), 0.0);
421 }
422
423 #[test]
424 fn test_progress_update_serialization() {
425 let update = ProgressUpdate {
426 current_step: 1,
427 total_steps: 5,
428 progress_percentage: 20.0,
429 estimated_time_remaining: Duration::from_secs(4),
430 timestamp: chrono::Utc::now(),
431 };
432
433 let json = serde_json::to_string(&update).unwrap();
434 let deserialized: ProgressUpdate = serde_json::from_str(&json).unwrap();
435
436 assert_eq!(deserialized.current_step, 1);
437 assert_eq!(deserialized.total_steps, 5);
438 assert_eq!(deserialized.progress_percentage, 20.0);
439 }
440}