ricecoder_execution/
progress_tracker.rs

1//! Progress tracking and reporting for execution plans
2//!
3//! Tracks execution progress and provides callbacks for UI updates.
4//! Supports reporting:
5//! - Current step and total steps
6//! - Overall progress percentage
7//! - Estimated time remaining
8//! - Progress callbacks for real-time UI updates
9
10use crate::models::ExecutionPlan;
11use serde::{Deserialize, Serialize};
12use std::sync::{Arc, Mutex};
13use std::time::{Duration, Instant};
14use tracing::{debug, info};
15
16/// Progress update event
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct ProgressUpdate {
19    /// Current step index (0-based)
20    pub current_step: usize,
21    /// Total number of steps
22    pub total_steps: usize,
23    /// Overall progress percentage (0-100)
24    pub progress_percentage: f32,
25    /// Estimated time remaining
26    pub estimated_time_remaining: Duration,
27    /// Timestamp of this update
28    pub timestamp: chrono::DateTime<chrono::Utc>,
29}
30
31/// Callback function for progress updates
32pub type ProgressCallback = Box<dyn Fn(ProgressUpdate) + Send + Sync>;
33
34/// Tracks execution progress and provides real-time updates
35///
36/// Maintains:
37/// - Current step index
38/// - Completed steps count
39/// - Execution start time
40/// - Step durations for time estimation
41/// - Progress callbacks for UI updates
42pub struct ProgressTracker {
43    /// Total number of steps in the plan
44    total_steps: usize,
45    /// Current step index (0-based)
46    current_step: usize,
47    /// Number of completed steps
48    completed_steps: usize,
49    /// Execution start time
50    start_time: Instant,
51    /// Step durations for time estimation
52    step_durations: Vec<Duration>,
53    /// Progress callbacks
54    callbacks: Arc<Mutex<Vec<ProgressCallback>>>,
55}
56
57impl ProgressTracker {
58    /// Create a new progress tracker for a plan
59    ///
60    /// # Arguments
61    /// * `plan` - The execution plan to track
62    ///
63    /// # Returns
64    /// A new ProgressTracker initialized for the plan
65    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    /// Register a progress callback
81    ///
82    /// Callbacks are called whenever progress is updated.
83    ///
84    /// # Arguments
85    /// * `callback` - Function to call on progress updates
86    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    /// Update progress to the next step
100    ///
101    /// Increments the current step and records the duration of the previous step.
102    ///
103    /// # Arguments
104    /// * `step_duration` - Duration of the completed step
105    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    /// Skip a step
121    ///
122    /// Marks a step as skipped without recording a duration.
123    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    /// Get the current progress update
133    ///
134    /// # Returns
135    /// A ProgressUpdate containing current progress information
136    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    /// Get the current step index (0-based)
155    pub fn current_step(&self) -> usize {
156        self.current_step
157    }
158
159    /// Get the total number of steps
160    pub fn total_steps(&self) -> usize {
161        self.total_steps
162    }
163
164    /// Get the number of completed steps
165    pub fn completed_steps(&self) -> usize {
166        self.completed_steps
167    }
168
169    /// Get the overall progress percentage (0-100)
170    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    /// Get the estimated time remaining
179    pub fn estimated_time_remaining(&self) -> Duration {
180        if self.step_durations.is_empty() || self.completed_steps == 0 {
181            // No data yet, estimate based on total steps
182            return Duration::from_secs(0);
183        }
184
185        // Calculate average step duration
186        let total_duration: Duration = self.step_durations.iter().sum();
187        let average_duration = total_duration / self.step_durations.len() as u32;
188
189        // Estimate remaining time
190        let remaining_steps = self.total_steps.saturating_sub(self.completed_steps);
191        average_duration * remaining_steps as u32
192    }
193
194    /// Get the total elapsed time
195    pub fn elapsed_time(&self) -> Duration {
196        self.start_time.elapsed()
197    }
198
199    /// Get the average step duration
200    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    /// Notify all registered callbacks of progress update
210    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    /// Reset the progress tracker
220    ///
221    /// Clears all progress data and resets to initial state.
222    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        // Complete 2 steps with 1 second each
337        tracker.step_completed(Duration::from_secs(1));
338        tracker.step_completed(Duration::from_secs(1));
339
340        // Average is 1 second per step
341        // 8 remaining steps should estimate to ~8 seconds
342        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        // Elapsed time should be recorded (even if very small)
365        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}