ricecoder_workflows/
progress.rs

1//! Progress tracking and status reporting for workflows
2
3use crate::models::{WorkflowState, WorkflowStatus};
4use chrono::{DateTime, Duration, Utc};
5use serde::{Deserialize, Serialize};
6
7/// Tracks workflow progress and provides status information
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct ProgressTracker {
10    /// Total number of steps in the workflow
11    pub total_steps: usize,
12    /// Step durations in milliseconds (for estimation)
13    pub step_durations: Vec<u64>,
14}
15
16/// Status report for a workflow
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct StatusReport {
19    /// Current executing step ID
20    pub current_step: Option<String>,
21    /// Progress percentage (0-100)
22    pub progress_percentage: u32,
23    /// Estimated completion time
24    pub estimated_completion_time: Option<DateTime<Utc>>,
25    /// Workflow status
26    pub workflow_status: WorkflowStatus,
27    /// Number of completed steps
28    pub completed_steps_count: usize,
29    /// Total steps in workflow
30    pub total_steps: usize,
31}
32
33impl ProgressTracker {
34    /// Create a new progress tracker
35    pub fn new(total_steps: usize) -> Self {
36        ProgressTracker {
37            total_steps,
38            step_durations: Vec::new(),
39        }
40    }
41
42    /// Record a step duration
43    pub fn record_step_duration(&mut self, duration_ms: u64) {
44        self.step_durations.push(duration_ms);
45    }
46
47    /// Calculate progress percentage (0-100)
48    pub fn calculate_progress(&self, completed_steps: usize) -> u32 {
49        if self.total_steps == 0 {
50            return 0;
51        }
52
53        ((completed_steps as u32 * 100) / self.total_steps as u32).min(100)
54    }
55
56    /// Estimate completion time based on step durations
57    pub fn estimate_completion_time(
58        &self,
59        state: &WorkflowState,
60        now: DateTime<Utc>,
61    ) -> Option<DateTime<Utc>> {
62        if self.step_durations.is_empty() || self.total_steps == 0 {
63            return None;
64        }
65
66        // Calculate average step duration
67        let total_duration: u64 = self.step_durations.iter().sum();
68        let avg_duration_ms = total_duration / self.step_durations.len() as u64;
69
70        // Calculate remaining steps
71        let remaining_steps = self.total_steps.saturating_sub(state.completed_steps.len());
72
73        // Estimate remaining time
74        let estimated_remaining_ms = remaining_steps as u64 * avg_duration_ms;
75
76        // Add to current time
77        now.checked_add_signed(Duration::milliseconds(estimated_remaining_ms as i64))
78    }
79
80    /// Generate a status report
81    pub fn generate_status_report(
82        &self,
83        state: &WorkflowState,
84        now: DateTime<Utc>,
85    ) -> StatusReport {
86        let completed_steps_count = state.completed_steps.len();
87        let progress_percentage = self.calculate_progress(completed_steps_count);
88        let estimated_completion_time = self.estimate_completion_time(state, now);
89
90        StatusReport {
91            current_step: state.current_step.clone(),
92            progress_percentage,
93            estimated_completion_time,
94            workflow_status: state.status,
95            completed_steps_count,
96            total_steps: self.total_steps,
97        }
98    }
99
100    /// Get average step duration in milliseconds
101    pub fn get_average_step_duration(&self) -> Option<u64> {
102        if self.step_durations.is_empty() {
103            return None;
104        }
105
106        let total: u64 = self.step_durations.iter().sum();
107        Some(total / self.step_durations.len() as u64)
108    }
109
110    /// Get minimum step duration in milliseconds
111    pub fn get_min_step_duration(&self) -> Option<u64> {
112        self.step_durations.iter().copied().min()
113    }
114
115    /// Get maximum step duration in milliseconds
116    pub fn get_max_step_duration(&self) -> Option<u64> {
117        self.step_durations.iter().copied().max()
118    }
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124
125    fn create_test_workflow_state() -> WorkflowState {
126        WorkflowState {
127            workflow_id: "test-workflow".to_string(),
128            status: WorkflowStatus::Running,
129            current_step: Some("step1".to_string()),
130            completed_steps: vec!["step0".to_string()],
131            step_results: Default::default(),
132            started_at: Utc::now(),
133            updated_at: Utc::now(),
134        }
135    }
136
137    #[test]
138    fn test_create_progress_tracker() {
139        let tracker = ProgressTracker::new(10);
140        assert_eq!(tracker.total_steps, 10);
141        assert!(tracker.step_durations.is_empty());
142    }
143
144    #[test]
145    fn test_record_step_duration() {
146        let mut tracker = ProgressTracker::new(10);
147        tracker.record_step_duration(100);
148        tracker.record_step_duration(200);
149
150        assert_eq!(tracker.step_durations.len(), 2);
151        assert_eq!(tracker.step_durations[0], 100);
152        assert_eq!(tracker.step_durations[1], 200);
153    }
154
155    #[test]
156    fn test_calculate_progress() {
157        let tracker = ProgressTracker::new(10);
158
159        assert_eq!(tracker.calculate_progress(0), 0);
160        assert_eq!(tracker.calculate_progress(5), 50);
161        assert_eq!(tracker.calculate_progress(10), 100);
162        assert_eq!(tracker.calculate_progress(15), 100); // Capped at 100
163    }
164
165    #[test]
166    fn test_calculate_progress_zero_steps() {
167        let tracker = ProgressTracker::new(0);
168        assert_eq!(tracker.calculate_progress(0), 0);
169    }
170
171    #[test]
172    fn test_get_average_step_duration() {
173        let mut tracker = ProgressTracker::new(10);
174        tracker.record_step_duration(100);
175        tracker.record_step_duration(200);
176        tracker.record_step_duration(300);
177
178        assert_eq!(tracker.get_average_step_duration(), Some(200));
179    }
180
181    #[test]
182    fn test_get_average_step_duration_empty() {
183        let tracker = ProgressTracker::new(10);
184        assert_eq!(tracker.get_average_step_duration(), None);
185    }
186
187    #[test]
188    fn test_get_min_step_duration() {
189        let mut tracker = ProgressTracker::new(10);
190        tracker.record_step_duration(100);
191        tracker.record_step_duration(200);
192        tracker.record_step_duration(50);
193
194        assert_eq!(tracker.get_min_step_duration(), Some(50));
195    }
196
197    #[test]
198    fn test_get_max_step_duration() {
199        let mut tracker = ProgressTracker::new(10);
200        tracker.record_step_duration(100);
201        tracker.record_step_duration(200);
202        tracker.record_step_duration(50);
203
204        assert_eq!(tracker.get_max_step_duration(), Some(200));
205    }
206
207    #[test]
208    fn test_estimate_completion_time() {
209        let mut tracker = ProgressTracker::new(10);
210        tracker.record_step_duration(100);
211        tracker.record_step_duration(100);
212
213        let state = create_test_workflow_state();
214        let now = Utc::now();
215
216        let estimated = tracker.estimate_completion_time(&state, now);
217        assert!(estimated.is_some());
218
219        // With 1 completed step out of 10, and avg 100ms per step,
220        // remaining 9 steps should take ~900ms
221        let estimated_time = estimated.unwrap();
222        let diff = estimated_time.signed_duration_since(now);
223        assert!(diff.num_milliseconds() > 800 && diff.num_milliseconds() < 1000);
224    }
225
226    #[test]
227    fn test_estimate_completion_time_no_durations() {
228        let tracker = ProgressTracker::new(10);
229        let state = create_test_workflow_state();
230        let now = Utc::now();
231
232        let estimated = tracker.estimate_completion_time(&state, now);
233        assert!(estimated.is_none());
234    }
235
236    #[test]
237    fn test_generate_status_report() {
238        let mut tracker = ProgressTracker::new(10);
239        tracker.record_step_duration(100);
240
241        let state = create_test_workflow_state();
242        let now = Utc::now();
243
244        let report = tracker.generate_status_report(&state, now);
245
246        assert_eq!(report.current_step, Some("step1".to_string()));
247        assert_eq!(report.progress_percentage, 10); // 1 out of 10
248        assert_eq!(report.completed_steps_count, 1);
249        assert_eq!(report.total_steps, 10);
250        assert_eq!(report.workflow_status, WorkflowStatus::Running);
251        assert!(report.estimated_completion_time.is_some());
252    }
253}