exarch_core/creation/
report.rs

1//! Archive creation operation reporting.
2
3use std::time::Duration;
4
5/// Report of an archive creation operation.
6///
7/// Contains statistics and metadata about the creation process.
8///
9/// # Examples
10///
11/// ```
12/// use exarch_core::creation::CreationReport;
13///
14/// let mut report = CreationReport::default();
15/// report.files_added = 10;
16/// report.bytes_written = 1024;
17/// report.bytes_compressed = 512;
18///
19/// assert_eq!(report.compression_ratio(), 2.0);
20/// assert_eq!(report.compression_percentage(), 50.0);
21/// ```
22#[derive(Debug, Clone, Default)]
23pub struct CreationReport {
24    /// Number of files added to the archive.
25    pub files_added: usize,
26
27    /// Number of directories added to the archive.
28    pub directories_added: usize,
29
30    /// Number of symlinks added to the archive.
31    pub symlinks_added: usize,
32
33    /// Total bytes written to the archive (uncompressed).
34    pub bytes_written: u64,
35
36    /// Total bytes in the final archive (compressed).
37    pub bytes_compressed: u64,
38
39    /// Duration of the creation operation.
40    pub duration: Duration,
41
42    /// Number of files skipped (due to filters or errors).
43    pub files_skipped: usize,
44
45    /// Warnings generated during creation.
46    pub warnings: Vec<String>,
47}
48
49impl CreationReport {
50    /// Creates a new empty creation report.
51    #[must_use]
52    pub fn new() -> Self {
53        Self::default()
54    }
55
56    /// Adds a warning message to the report.
57    ///
58    /// # Examples
59    ///
60    /// ```
61    /// use exarch_core::creation::CreationReport;
62    ///
63    /// let mut report = CreationReport::new();
64    /// report.add_warning("File too large, skipped");
65    /// assert!(report.has_warnings());
66    /// ```
67    pub fn add_warning(&mut self, msg: impl Into<String>) {
68        self.warnings.push(msg.into());
69    }
70
71    /// Returns whether any warnings were generated.
72    ///
73    /// # Examples
74    ///
75    /// ```
76    /// use exarch_core::creation::CreationReport;
77    ///
78    /// let mut report = CreationReport::new();
79    /// assert!(!report.has_warnings());
80    ///
81    /// report.add_warning("test warning");
82    /// assert!(report.has_warnings());
83    /// ```
84    #[must_use]
85    pub fn has_warnings(&self) -> bool {
86        !self.warnings.is_empty()
87    }
88
89    /// Returns the compression ratio (uncompressed / compressed).
90    ///
91    /// Returns 0.0 if `bytes_compressed` is 0 or `bytes_written` is 0.
92    ///
93    /// # Examples
94    ///
95    /// ```
96    /// use exarch_core::creation::CreationReport;
97    ///
98    /// let mut report = CreationReport::new();
99    /// report.bytes_written = 1000;
100    /// report.bytes_compressed = 500;
101    /// assert_eq!(report.compression_ratio(), 2.0);
102    ///
103    /// // Edge case: zero compressed size
104    /// report.bytes_compressed = 0;
105    /// assert_eq!(report.compression_ratio(), 0.0);
106    ///
107    /// // Edge case: equal sizes (no compression)
108    /// report.bytes_written = 1000;
109    /// report.bytes_compressed = 1000;
110    /// assert_eq!(report.compression_ratio(), 1.0);
111    /// ```
112    #[must_use]
113    pub fn compression_ratio(&self) -> f64 {
114        if self.bytes_compressed == 0 || self.bytes_written == 0 {
115            return 0.0;
116        }
117        self.bytes_written as f64 / self.bytes_compressed as f64
118    }
119
120    /// Returns the compression percentage (space saved).
121    ///
122    /// Returns 0.0 if `bytes_written` is 0.
123    /// Returns 100.0 if `bytes_compressed` is 0 (perfect compression).
124    ///
125    /// # Examples
126    ///
127    /// ```
128    /// use exarch_core::creation::CreationReport;
129    ///
130    /// let mut report = CreationReport::new();
131    /// report.bytes_written = 1000;
132    /// report.bytes_compressed = 500;
133    /// assert_eq!(report.compression_percentage(), 50.0);
134    ///
135    /// // Edge case: no compression
136    /// report.bytes_compressed = 1000;
137    /// assert_eq!(report.compression_percentage(), 0.0);
138    ///
139    /// // Edge case: perfect compression
140    /// report.bytes_compressed = 0;
141    /// assert_eq!(report.compression_percentage(), 100.0);
142    /// ```
143    #[must_use]
144    pub fn compression_percentage(&self) -> f64 {
145        if self.bytes_written == 0 {
146            return 0.0;
147        }
148        if self.bytes_compressed == 0 {
149            return 100.0;
150        }
151        let saved = self.bytes_written.saturating_sub(self.bytes_compressed);
152        (saved as f64 / self.bytes_written as f64) * 100.0
153    }
154
155    /// Returns total number of items added.
156    ///
157    /// # Examples
158    ///
159    /// ```
160    /// use exarch_core::creation::CreationReport;
161    ///
162    /// let mut report = CreationReport::new();
163    /// report.files_added = 10;
164    /// report.directories_added = 5;
165    /// report.symlinks_added = 2;
166    /// assert_eq!(report.total_items(), 17);
167    /// ```
168    #[must_use]
169    pub fn total_items(&self) -> usize {
170        self.files_added + self.directories_added + self.symlinks_added
171    }
172}
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177
178    #[test]
179    fn test_creation_report_default() {
180        let report = CreationReport::default();
181        assert_eq!(report.files_added, 0);
182        assert_eq!(report.directories_added, 0);
183        assert_eq!(report.symlinks_added, 0);
184        assert_eq!(report.bytes_written, 0);
185        assert_eq!(report.bytes_compressed, 0);
186        assert_eq!(report.duration, Duration::default());
187        assert_eq!(report.files_skipped, 0);
188        assert!(report.warnings.is_empty());
189        assert!(!report.has_warnings());
190    }
191
192    #[test]
193    fn test_creation_report_new() {
194        let report = CreationReport::new();
195        assert_eq!(report.files_added, 0);
196        assert!(!report.has_warnings());
197    }
198
199    #[test]
200    #[allow(clippy::float_cmp)]
201    fn test_creation_report_compression_ratio() {
202        let mut report = CreationReport::new();
203
204        // Normal case: 2:1 compression
205        report.bytes_written = 1000;
206        report.bytes_compressed = 500;
207        assert_eq!(report.compression_ratio(), 2.0);
208
209        // Edge case: no compression (1:1)
210        report.bytes_written = 1000;
211        report.bytes_compressed = 1000;
212        assert_eq!(report.compression_ratio(), 1.0);
213
214        // Edge case: expansion (worse than no compression)
215        report.bytes_written = 500;
216        report.bytes_compressed = 1000;
217        assert_eq!(report.compression_ratio(), 0.5);
218
219        // Edge case: zero compressed size
220        report.bytes_written = 1000;
221        report.bytes_compressed = 0;
222        assert_eq!(report.compression_ratio(), 0.0);
223
224        // Edge case: zero written size
225        report.bytes_written = 0;
226        report.bytes_compressed = 500;
227        assert_eq!(report.compression_ratio(), 0.0);
228
229        // Edge case: both zero
230        report.bytes_written = 0;
231        report.bytes_compressed = 0;
232        assert_eq!(report.compression_ratio(), 0.0);
233    }
234
235    #[test]
236    #[allow(clippy::float_cmp)]
237    fn test_creation_report_compression_percentage() {
238        let mut report = CreationReport::new();
239
240        // 50% compression
241        report.bytes_written = 1000;
242        report.bytes_compressed = 500;
243        assert_eq!(report.compression_percentage(), 50.0);
244
245        // 75% compression
246        report.bytes_written = 1000;
247        report.bytes_compressed = 250;
248        assert_eq!(report.compression_percentage(), 75.0);
249
250        // No compression
251        report.bytes_written = 1000;
252        report.bytes_compressed = 1000;
253        assert_eq!(report.compression_percentage(), 0.0);
254
255        // Expansion (negative compression)
256        report.bytes_written = 500;
257        report.bytes_compressed = 1000;
258        assert_eq!(report.compression_percentage(), 0.0);
259
260        // Edge case: perfect compression
261        report.bytes_written = 1000;
262        report.bytes_compressed = 0;
263        assert_eq!(report.compression_percentage(), 100.0);
264
265        // Edge case: zero written
266        report.bytes_written = 0;
267        report.bytes_compressed = 500;
268        assert_eq!(report.compression_percentage(), 0.0);
269
270        // Edge case: both zero
271        report.bytes_written = 0;
272        report.bytes_compressed = 0;
273        assert_eq!(report.compression_percentage(), 0.0);
274    }
275
276    #[test]
277    fn test_creation_report_warnings() {
278        let mut report = CreationReport::new();
279        assert!(!report.has_warnings());
280
281        report.add_warning("Warning 1");
282        assert!(report.has_warnings());
283        assert_eq!(report.warnings.len(), 1);
284        assert_eq!(report.warnings[0], "Warning 1");
285
286        report.add_warning("Warning 2".to_string());
287        assert_eq!(report.warnings.len(), 2);
288        assert_eq!(report.warnings[1], "Warning 2");
289
290        let string_ref = String::from("Warning 3");
291        report.add_warning(&string_ref);
292        assert_eq!(report.warnings.len(), 3);
293    }
294
295    #[test]
296    fn test_creation_report_total_items() {
297        let mut report = CreationReport::new();
298        assert_eq!(report.total_items(), 0);
299
300        report.files_added = 10;
301        report.directories_added = 5;
302        report.symlinks_added = 2;
303        assert_eq!(report.total_items(), 17);
304
305        report.files_added = 0;
306        assert_eq!(report.total_items(), 7);
307    }
308
309    #[test]
310    fn test_creation_report_real_scenario() {
311        let mut report = CreationReport::new();
312        report.files_added = 100;
313        report.directories_added = 20;
314        report.symlinks_added = 5;
315        report.bytes_written = 10 * 1024 * 1024; // 10 MB
316        report.bytes_compressed = 3 * 1024 * 1024; // 3 MB
317        report.duration = Duration::from_secs(2);
318        report.files_skipped = 3;
319        report.add_warning("Skipped 3 files due to size limit");
320
321        assert_eq!(report.total_items(), 125);
322        assert!(report.has_warnings());
323        assert_eq!(report.warnings.len(), 1);
324
325        // Compression ratio should be ~3.33 (10MB / 3MB)
326        let ratio = report.compression_ratio();
327        assert!((ratio - 3.333).abs() < 0.01);
328
329        // Compression percentage should be ~70% ((10-3)/10 * 100)
330        let percentage = report.compression_percentage();
331        assert!((percentage - 70.0).abs() < 0.1);
332    }
333}