1#![allow(dead_code)]
2
3use std::collections::HashMap;
10use std::time::{Duration, Instant};
11
12#[derive(Debug, Clone, PartialEq, Eq, Hash)]
14pub enum ProxyState {
15 Queued,
17 InProgress,
19 Completed,
21 Failed,
23 Cancelled,
25 Paused,
27 Retrying,
29}
30
31impl std::fmt::Display for ProxyState {
32 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33 let label = match self {
34 Self::Queued => "Queued",
35 Self::InProgress => "In Progress",
36 Self::Completed => "Completed",
37 Self::Failed => "Failed",
38 Self::Cancelled => "Cancelled",
39 Self::Paused => "Paused",
40 Self::Retrying => "Retrying",
41 };
42 write!(f, "{label}")
43 }
44}
45
46#[derive(Debug, Clone)]
48pub struct ProxyJobStatus {
49 pub job_id: String,
51 pub source_path: String,
53 pub output_path: String,
55 pub state: ProxyState,
57 pub progress_percent: f64,
59 pub frames_processed: u64,
61 pub total_frames: u64,
63 pub error_message: Option<String>,
65 pub retry_count: u32,
67 pub max_retries: u32,
69}
70
71impl ProxyJobStatus {
72 pub fn new(job_id: &str, source: &str, output: &str) -> Self {
74 Self {
75 job_id: job_id.to_string(),
76 source_path: source.to_string(),
77 output_path: output.to_string(),
78 state: ProxyState::Queued,
79 progress_percent: 0.0,
80 frames_processed: 0,
81 total_frames: 0,
82 error_message: None,
83 retry_count: 0,
84 max_retries: 3,
85 }
86 }
87
88 pub fn with_total_frames(mut self, total: u64) -> Self {
90 self.total_frames = total;
91 self
92 }
93
94 pub fn with_max_retries(mut self, max: u32) -> Self {
96 self.max_retries = max;
97 self
98 }
99
100 pub fn is_terminal(&self) -> bool {
102 matches!(
103 self.state,
104 ProxyState::Completed | ProxyState::Failed | ProxyState::Cancelled
105 )
106 }
107
108 pub fn can_retry(&self) -> bool {
110 self.state == ProxyState::Failed && self.retry_count < self.max_retries
111 }
112
113 #[allow(clippy::cast_precision_loss)]
115 pub fn update_progress(&mut self, frames: u64) {
116 self.frames_processed = frames;
117 if self.total_frames > 0 {
118 self.progress_percent = (frames as f64 / self.total_frames as f64) * 100.0;
119 if self.progress_percent > 100.0 {
120 self.progress_percent = 100.0;
121 }
122 }
123 }
124}
125
126#[derive(Debug)]
128pub struct ProxyStatusTracker {
129 jobs: HashMap<String, ProxyJobStatus>,
131 created_at: Instant,
133}
134
135impl ProxyStatusTracker {
136 pub fn new() -> Self {
138 Self {
139 jobs: HashMap::new(),
140 created_at: Instant::now(),
141 }
142 }
143
144 pub fn add_job(&mut self, status: ProxyJobStatus) {
146 self.jobs.insert(status.job_id.clone(), status);
147 }
148
149 pub fn get_job(&self, job_id: &str) -> Option<&ProxyJobStatus> {
151 self.jobs.get(job_id)
152 }
153
154 pub fn transition(&mut self, job_id: &str, new_state: ProxyState) -> bool {
156 if let Some(job) = self.jobs.get_mut(job_id) {
157 if job.is_terminal() && new_state != ProxyState::Retrying {
158 return false;
159 }
160 if new_state == ProxyState::Retrying {
161 job.retry_count += 1;
162 }
163 if new_state == ProxyState::Completed {
164 job.progress_percent = 100.0;
165 }
166 job.state = new_state;
167 true
168 } else {
169 false
170 }
171 }
172
173 pub fn fail_job(&mut self, job_id: &str, error: &str) -> bool {
175 if let Some(job) = self.jobs.get_mut(job_id) {
176 job.state = ProxyState::Failed;
177 job.error_message = Some(error.to_string());
178 true
179 } else {
180 false
181 }
182 }
183
184 pub fn update_progress(&mut self, job_id: &str, frames: u64) -> bool {
186 if let Some(job) = self.jobs.get_mut(job_id) {
187 job.update_progress(frames);
188 true
189 } else {
190 false
191 }
192 }
193
194 pub fn job_count(&self) -> usize {
196 self.jobs.len()
197 }
198
199 pub fn count_by_state(&self) -> HashMap<ProxyState, usize> {
201 let mut counts: HashMap<ProxyState, usize> = HashMap::new();
202 for job in self.jobs.values() {
203 *counts.entry(job.state.clone()).or_insert(0) += 1;
204 }
205 counts
206 }
207
208 #[allow(clippy::cast_precision_loss)]
210 pub fn overall_progress(&self) -> f64 {
211 let active: Vec<&ProxyJobStatus> =
212 self.jobs.values().filter(|j| !j.is_terminal()).collect();
213 if active.is_empty() {
214 return 100.0;
215 }
216 let sum: f64 = active.iter().map(|j| j.progress_percent).sum();
217 sum / active.len() as f64
218 }
219
220 pub fn retryable_jobs(&self) -> Vec<&ProxyJobStatus> {
222 self.jobs.values().filter(|j| j.can_retry()).collect()
223 }
224
225 pub fn elapsed(&self) -> Duration {
227 self.created_at.elapsed()
228 }
229
230 pub fn clear_terminal(&mut self) -> usize {
232 let before = self.jobs.len();
233 self.jobs.retain(|_, j| !j.is_terminal());
234 before - self.jobs.len()
235 }
236}
237
238impl Default for ProxyStatusTracker {
239 fn default() -> Self {
240 Self::new()
241 }
242}
243
244#[cfg(test)]
245mod tests {
246 use super::*;
247
248 fn make_job(id: &str) -> ProxyJobStatus {
249 ProxyJobStatus::new(id, "/src/clip.mov", "/proxy/clip.mp4")
250 .with_total_frames(1000)
251 .with_max_retries(2)
252 }
253
254 #[test]
255 fn test_new_job_is_queued() {
256 let job = make_job("j1");
257 assert_eq!(job.state, ProxyState::Queued);
258 assert!((job.progress_percent - 0.0).abs() < f64::EPSILON);
259 }
260
261 #[test]
262 fn test_is_terminal() {
263 let mut job = make_job("j1");
264 assert!(!job.is_terminal());
265 job.state = ProxyState::Completed;
266 assert!(job.is_terminal());
267 job.state = ProxyState::Failed;
268 assert!(job.is_terminal());
269 job.state = ProxyState::Cancelled;
270 assert!(job.is_terminal());
271 }
272
273 #[test]
274 fn test_can_retry() {
275 let mut job = make_job("j1");
276 job.state = ProxyState::Failed;
277 job.retry_count = 0;
278 assert!(job.can_retry());
279 job.retry_count = 2;
280 assert!(!job.can_retry());
281 }
282
283 #[test]
284 fn test_update_progress() {
285 let mut job = make_job("j1");
286 job.update_progress(500);
287 assert!((job.progress_percent - 50.0).abs() < f64::EPSILON);
288 assert_eq!(job.frames_processed, 500);
289 }
290
291 #[test]
292 fn test_progress_caps_at_100() {
293 let mut job = make_job("j1");
294 job.update_progress(2000);
295 assert!((job.progress_percent - 100.0).abs() < f64::EPSILON);
296 }
297
298 #[test]
299 fn test_tracker_add_and_get() {
300 let mut tracker = ProxyStatusTracker::new();
301 tracker.add_job(make_job("j1"));
302 assert_eq!(tracker.job_count(), 1);
303 assert!(tracker.get_job("j1").is_some());
304 assert!(tracker.get_job("j999").is_none());
305 }
306
307 #[test]
308 fn test_transition() {
309 let mut tracker = ProxyStatusTracker::new();
310 tracker.add_job(make_job("j1"));
311 assert!(tracker.transition("j1", ProxyState::InProgress));
312 assert_eq!(
313 tracker.get_job("j1").expect("should succeed in test").state,
314 ProxyState::InProgress
315 );
316 }
317
318 #[test]
319 fn test_transition_terminal_blocked() {
320 let mut tracker = ProxyStatusTracker::new();
321 tracker.add_job(make_job("j1"));
322 tracker.transition("j1", ProxyState::Completed);
323 assert!(!tracker.transition("j1", ProxyState::InProgress));
325 }
326
327 #[test]
328 fn test_transition_retry_allowed() {
329 let mut tracker = ProxyStatusTracker::new();
330 tracker.add_job(make_job("j1"));
331 tracker.transition("j1", ProxyState::Failed);
332 assert!(tracker.transition("j1", ProxyState::Retrying));
334 assert_eq!(
335 tracker
336 .get_job("j1")
337 .expect("should succeed in test")
338 .retry_count,
339 1
340 );
341 }
342
343 #[test]
344 fn test_fail_job() {
345 let mut tracker = ProxyStatusTracker::new();
346 tracker.add_job(make_job("j1"));
347 assert!(tracker.fail_job("j1", "codec not found"));
348 let job = tracker.get_job("j1").expect("should succeed in test");
349 assert_eq!(job.state, ProxyState::Failed);
350 assert_eq!(job.error_message.as_deref(), Some("codec not found"));
351 }
352
353 #[test]
354 fn test_fail_nonexistent_job() {
355 let mut tracker = ProxyStatusTracker::new();
356 assert!(!tracker.fail_job("nonexistent", "err"));
357 }
358
359 #[test]
360 fn test_count_by_state() {
361 let mut tracker = ProxyStatusTracker::new();
362 tracker.add_job(make_job("j1"));
363 tracker.add_job(make_job("j2"));
364 tracker.add_job(make_job("j3"));
365 tracker.transition("j2", ProxyState::InProgress);
366 tracker.transition("j3", ProxyState::Completed);
367
368 let counts = tracker.count_by_state();
369 assert_eq!(*counts.get(&ProxyState::Queued).unwrap_or(&0), 1);
370 assert_eq!(*counts.get(&ProxyState::InProgress).unwrap_or(&0), 1);
371 assert_eq!(*counts.get(&ProxyState::Completed).unwrap_or(&0), 1);
372 }
373
374 #[test]
375 fn test_overall_progress() {
376 let mut tracker = ProxyStatusTracker::new();
377 tracker.add_job(make_job("j1"));
378 tracker.add_job(make_job("j2"));
379 tracker.update_progress("j1", 500); tracker.update_progress("j2", 250); let overall = tracker.overall_progress();
382 assert!((overall - 37.5).abs() < f64::EPSILON);
383 }
384
385 #[test]
386 fn test_overall_progress_all_done() {
387 let mut tracker = ProxyStatusTracker::new();
388 tracker.add_job(make_job("j1"));
389 tracker.transition("j1", ProxyState::Completed);
390 assert!((tracker.overall_progress() - 100.0).abs() < f64::EPSILON);
391 }
392
393 #[test]
394 fn test_retryable_jobs() {
395 let mut tracker = ProxyStatusTracker::new();
396 tracker.add_job(make_job("j1"));
397 tracker.add_job(make_job("j2"));
398 tracker.fail_job("j1", "err");
399 let retryable = tracker.retryable_jobs();
400 assert_eq!(retryable.len(), 1);
401 assert_eq!(retryable[0].job_id, "j1");
402 }
403
404 #[test]
405 fn test_clear_terminal() {
406 let mut tracker = ProxyStatusTracker::new();
407 tracker.add_job(make_job("j1"));
408 tracker.add_job(make_job("j2"));
409 tracker.add_job(make_job("j3"));
410 tracker.transition("j1", ProxyState::Completed);
411 tracker.fail_job("j2", "err");
412 let cleared = tracker.clear_terminal();
413 assert_eq!(cleared, 2);
414 assert_eq!(tracker.job_count(), 1);
415 }
416
417 #[test]
418 fn test_proxy_state_display() {
419 assert_eq!(format!("{}", ProxyState::Queued), "Queued");
420 assert_eq!(format!("{}", ProxyState::InProgress), "In Progress");
421 assert_eq!(format!("{}", ProxyState::Completed), "Completed");
422 assert_eq!(format!("{}", ProxyState::Failed), "Failed");
423 }
424
425 #[test]
426 fn test_default_tracker() {
427 let tracker = ProxyStatusTracker::default();
428 assert_eq!(tracker.job_count(), 0);
429 }
430}