Skip to main content

pulith_fetch/progress/
progress.rs

1use std::fmt;
2
3use crate::config::fetch_options::FetchPhase;
4
5/// Represents the current state of a download operation.
6///
7/// This struct is passed to progress callbacks and provides information about
8/// the current phase, bytes downloaded, and retry status.
9#[derive(Debug, Clone, PartialEq)]
10pub struct Progress {
11    /// Current phase of the download.
12    pub phase: FetchPhase,
13
14    /// Number of bytes written to the staging file.
15    pub bytes_downloaded: u64,
16
17    /// Total expected bytes, if known from Content-Length header.
18    ///
19    /// This may be `None` if the server doesn't provide Content-Length
20    /// (e.g., when using chunked transfer encoding).
21    pub total_bytes: Option<u64>,
22
23    /// Current retry attempt (0 = no retries yet, first attempt).
24    pub retry_count: u32,
25
26    /// Performance metrics for this operation.
27    pub performance_metrics: Option<PerformanceMetrics>,
28}
29
30/// Performance metrics for download operations.
31#[derive(Debug, Clone, PartialEq, Default)]
32pub struct PerformanceMetrics {
33    /// Current download rate in bytes per second
34    pub current_rate_bps: Option<f64>,
35
36    /// Average download rate since start in bytes per second
37    pub average_rate_bps: Option<f64>,
38
39    /// Current bandwidth limit in bytes per second (if throttled)
40    pub bandwidth_limit_bps: Option<u64>,
41
42    /// Bandwidth utilization as a percentage (0.0 to 1.0)
43    pub bandwidth_utilization: Option<f64>,
44
45    /// Time spent in each phase (in milliseconds)
46    pub phase_timings: PhaseTimings,
47
48    /// Number of times rate was adjusted by adaptive algorithm
49    pub rate_adjustments: u32,
50
51    /// Network latency in milliseconds
52    pub network_latency_ms: Option<u64>,
53
54    /// Time to establish connection in milliseconds
55    pub connection_time_ms: Option<u64>,
56}
57
58/// Timing information for different phases of a download operation.
59#[derive(Debug, Clone, PartialEq, Default)]
60pub struct PhaseTimings {
61    /// Time spent connecting to the server (in milliseconds)
62    pub connecting_ms: u64,
63
64    /// Time spent downloading data (in milliseconds)
65    pub downloading_ms: u64,
66
67    /// Time spent verifying checksums (in milliseconds)
68    pub verifying_ms: u64,
69
70    /// Time spent committing the final file (in milliseconds)
71    pub committing_ms: u64,
72}
73
74impl PhaseTimings {
75    /// Returns the total time spent across all phases (in milliseconds).
76    #[must_use]
77    pub fn total_ms(&self) -> u64 {
78        self.connecting_ms + self.downloading_ms + self.verifying_ms + self.committing_ms
79    }
80}
81
82impl Progress {
83    /// Calculate the percentage of completion.
84    ///
85    /// Returns `None` if `total_bytes` is unknown.
86    #[must_use]
87    pub fn percentage(&self) -> Option<f64> {
88        self.total_bytes.map(|total| {
89            if total == 0 {
90                // For empty files, report 100% when completed, 0% otherwise
91                if self.is_completed() { 100.0 } else { 0.0 }
92            } else {
93                (self.bytes_downloaded as f64 / total as f64) * 100.0
94            }
95        })
96    }
97
98    /// Returns `true` if the download has completed successfully.
99    #[must_use]
100    pub fn is_completed(&self) -> bool {
101        self.phase == FetchPhase::Completed
102    }
103
104    /// Returns `true` if a retry is in progress.
105    #[must_use]
106    pub fn is_retrying(&self) -> bool {
107        self.retry_count > 0
108    }
109}
110
111impl fmt::Display for Progress {
112    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
113        match self.percentage() {
114            Some(pct) => write!(
115                f,
116                "{}: {:.1}% ({}/{} bytes, retry {})",
117                self.phase,
118                pct,
119                self.bytes_downloaded,
120                self.total_bytes.unwrap_or(0),
121                self.retry_count
122            ),
123            None => write!(
124                f,
125                "{}: {}/{} bytes (retry {})",
126                self.phase,
127                self.bytes_downloaded,
128                self.total_bytes.unwrap_or(0),
129                self.retry_count
130            ),
131        }
132    }
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138
139    #[test]
140    fn test_progress_percentage() {
141        let progress = Progress {
142            phase: FetchPhase::Downloading,
143            bytes_downloaded: 50,
144            total_bytes: Some(100),
145            retry_count: 0,
146            performance_metrics: None,
147        };
148        assert_eq!(progress.percentage(), Some(50.0));
149
150        // Test with zero total bytes
151        let progress = Progress {
152            phase: FetchPhase::Downloading,
153            bytes_downloaded: 0,
154            total_bytes: Some(0),
155            retry_count: 0,
156            performance_metrics: None,
157        };
158        assert_eq!(progress.percentage(), Some(0.0));
159
160        // Test completed empty file
161        let progress = Progress {
162            phase: FetchPhase::Completed,
163            bytes_downloaded: 0,
164            total_bytes: Some(0),
165            retry_count: 0,
166            performance_metrics: None,
167        };
168        assert_eq!(progress.percentage(), Some(100.0));
169
170        // Test with unknown total
171        let progress = Progress {
172            phase: FetchPhase::Downloading,
173            bytes_downloaded: 50,
174            total_bytes: None,
175            retry_count: 0,
176            performance_metrics: None,
177        };
178        assert_eq!(progress.percentage(), None);
179    }
180
181    #[test]
182    fn test_is_completed() {
183        let progress = Progress {
184            phase: FetchPhase::Completed,
185            bytes_downloaded: 100,
186            total_bytes: Some(100),
187            retry_count: 0,
188            performance_metrics: None,
189        };
190        assert!(progress.is_completed());
191
192        let progress = Progress {
193            phase: FetchPhase::Downloading,
194            bytes_downloaded: 100,
195            total_bytes: Some(100),
196            retry_count: 0,
197            performance_metrics: None,
198        };
199        assert!(!progress.is_completed());
200    }
201
202    #[test]
203    fn test_is_retrying() {
204        let progress = Progress {
205            phase: FetchPhase::Downloading,
206            bytes_downloaded: 50,
207            total_bytes: Some(100),
208            retry_count: 1,
209            performance_metrics: None,
210        };
211        assert!(progress.is_retrying());
212
213        let progress = Progress {
214            phase: FetchPhase::Downloading,
215            bytes_downloaded: 50,
216            total_bytes: Some(100),
217            retry_count: 0,
218            performance_metrics: None,
219        };
220        assert!(!progress.is_retrying());
221    }
222
223    #[test]
224    fn test_performance_metrics_default() {
225        let metrics = PerformanceMetrics::default();
226        assert!(metrics.current_rate_bps.is_none());
227        assert!(metrics.average_rate_bps.is_none());
228        assert!(metrics.bandwidth_limit_bps.is_none());
229        assert!(metrics.bandwidth_utilization.is_none());
230        assert_eq!(metrics.rate_adjustments, 0);
231        assert!(metrics.network_latency_ms.is_none());
232        assert!(metrics.connection_time_ms.is_none());
233    }
234}