Skip to main content

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}