ff_pipeline/progress.rs
1//! Pipeline progress tracking.
2//!
3//! This module provides [`Progress`], which is passed to the
4//! [`ProgressCallback`] on every processed frame, and the
5//! [`ProgressCallback`] type alias itself.
6
7/// Progress information delivered to the caller on each processed frame.
8///
9/// An instance of this struct is passed by reference to the
10/// [`ProgressCallback`] registered via
11/// [`PipelineBuilder::on_progress`](crate::PipelineBuilder::on_progress).
12/// The callback returns `true` to continue or `false` to cancel the pipeline.
13#[derive(Debug, Clone)]
14pub struct Progress {
15 /// Number of frames processed so far.
16 pub frames_processed: u64,
17
18 /// Total number of frames in the source, or `None` if the container
19 /// does not report a frame count.
20 pub total_frames: Option<u64>,
21
22 /// Wall-clock time elapsed since [`Pipeline::run`](crate::Pipeline::run) was called.
23 pub elapsed: std::time::Duration,
24}
25
26impl Progress {
27 /// Returns the completion percentage in the range `0.0..=100.0`, or `None`
28 /// when [`total_frames`](Self::total_frames) is unknown.
29 ///
30 /// # Examples
31 ///
32 /// ```
33 /// use ff_pipeline::Progress;
34 /// use std::time::Duration;
35 ///
36 /// let p = Progress { frames_processed: 25, total_frames: Some(100), elapsed: Duration::ZERO };
37 /// assert_eq!(p.percent(), Some(25.0));
38 ///
39 /// let p = Progress { frames_processed: 5, total_frames: None, elapsed: Duration::ZERO };
40 /// assert_eq!(p.percent(), None);
41 /// ```
42 #[must_use]
43 #[allow(clippy::cast_precision_loss)]
44 pub fn percent(&self) -> Option<f64> {
45 self.total_frames
46 .map(|total| (self.frames_processed as f64 / total as f64) * 100.0)
47 }
48}
49
50/// Callback invoked on every processed frame to report progress.
51///
52/// The closure receives a [`Progress`] reference and must return `true` to
53/// continue processing or `false` to request cancellation. When `false` is
54/// returned, [`Pipeline::run`](crate::Pipeline::run) stops at the next frame
55/// boundary and returns [`PipelineError::Cancelled`](crate::PipelineError::Cancelled).
56pub type ProgressCallback = Box<dyn Fn(&Progress) -> bool + Send>;
57
58#[cfg(test)]
59mod tests {
60 use super::*;
61 use std::time::Duration;
62
63 #[test]
64 fn percent_should_return_none_when_total_frames_unknown() {
65 let p = Progress {
66 frames_processed: 10,
67 total_frames: None,
68 elapsed: Duration::from_secs(1),
69 };
70 assert_eq!(p.percent(), None);
71 }
72
73 #[test]
74 fn percent_should_return_correct_value_when_total_known() {
75 let p = Progress {
76 frames_processed: 50,
77 total_frames: Some(200),
78 elapsed: Duration::from_secs(1),
79 };
80 assert!((p.percent().unwrap() - 25.0).abs() < f64::EPSILON);
81 }
82
83 #[test]
84 fn percent_should_return_100_when_complete() {
85 let p = Progress {
86 frames_processed: 100,
87 total_frames: Some(100),
88 elapsed: Duration::from_secs(5),
89 };
90 assert!((p.percent().unwrap() - 100.0).abs() < f64::EPSILON);
91 }
92
93 #[test]
94 fn percent_should_return_0_when_no_frames_processed() {
95 let p = Progress {
96 frames_processed: 0,
97 total_frames: Some(120),
98 elapsed: Duration::ZERO,
99 };
100 assert_eq!(p.percent(), Some(0.0));
101 }
102
103 #[test]
104 fn percent_should_exceed_100_when_processed_exceeds_total() {
105 // percent() makes no claim about clamping — callers are responsible.
106 let p = Progress {
107 frames_processed: 110,
108 total_frames: Some(100),
109 elapsed: Duration::from_secs(4),
110 };
111 assert!(p.percent().unwrap() > 100.0);
112 }
113
114 #[test]
115 fn callback_should_receive_progress_and_return_bool() {
116 let continue_cb: ProgressCallback = Box::new(|_p| true);
117 let cancel_cb: ProgressCallback = Box::new(|_p| false);
118
119 let p = Progress {
120 frames_processed: 1,
121 total_frames: Some(10),
122 elapsed: Duration::from_millis(33),
123 };
124
125 assert!(continue_cb(&p));
126 assert!(!cancel_cb(&p));
127 }
128}