fast_yaml_parallel/
result.rs

1//! Result types for batch file processing.
2
3use std::path::PathBuf;
4use std::time::Duration;
5
6use crate::error::Error;
7
8/// Outcome of processing a single file.
9#[derive(Debug)]
10pub enum FileOutcome {
11    /// File processed successfully (content may or may not have changed)
12    Success {
13        /// Processing duration
14        duration: Duration,
15    },
16    /// File formatted and content changed
17    Changed {
18        /// Processing duration
19        duration: Duration,
20    },
21    /// File unchanged (already formatted)
22    Unchanged {
23        /// Processing duration
24        duration: Duration,
25    },
26    /// Processing failed
27    Error {
28        /// The error that occurred
29        error: Error,
30        /// Processing duration
31        duration: Duration,
32    },
33}
34
35impl FileOutcome {
36    /// Returns true if the file was successfully processed
37    pub const fn is_success(&self) -> bool {
38        !matches!(self, Self::Error { .. })
39    }
40
41    /// Returns the processing duration
42    pub const fn duration(&self) -> Duration {
43        match self {
44            Self::Success { duration }
45            | Self::Changed { duration }
46            | Self::Unchanged { duration }
47            | Self::Error { duration, .. } => *duration,
48        }
49    }
50
51    /// Returns true if the file was changed
52    pub const fn was_changed(&self) -> bool {
53        matches!(self, Self::Changed { .. })
54    }
55}
56
57/// Result for a single file with path context.
58#[derive(Debug)]
59pub struct FileResult {
60    /// Path to the processed file
61    pub path: PathBuf,
62    /// Processing outcome
63    pub outcome: FileOutcome,
64}
65
66impl FileResult {
67    /// Creates a new `FileResult`
68    pub const fn new(path: PathBuf, outcome: FileOutcome) -> Self {
69        Self { path, outcome }
70    }
71
72    /// Returns true if processing was successful
73    pub const fn is_success(&self) -> bool {
74        self.outcome.is_success()
75    }
76}
77
78/// Aggregated results from batch processing.
79#[derive(Debug, Default)]
80pub struct BatchResult {
81    /// Total number of files processed
82    pub total: usize,
83    /// Number of files successfully processed
84    pub success: usize,
85    /// Number of files changed
86    pub changed: usize,
87    /// Number of files that failed processing
88    pub failed: usize,
89    /// Total processing duration
90    pub duration: Duration,
91    /// List of errors with file paths
92    pub errors: Vec<(PathBuf, Error)>,
93}
94
95impl BatchResult {
96    /// Creates a new empty `BatchResult`
97    pub fn new() -> Self {
98        Self::default()
99    }
100
101    /// Creates a `BatchResult` from a list of `FileResult`s
102    pub fn from_results(results: Vec<FileResult>) -> Self {
103        let start = std::time::Instant::now();
104        let total = results.len();
105        let mut success = 0;
106        let mut changed = 0;
107        let mut failed = 0;
108        let mut errors = Vec::with_capacity(total);
109
110        for result in results {
111            match result.outcome {
112                FileOutcome::Success { .. } | FileOutcome::Unchanged { .. } => {
113                    success += 1;
114                }
115                FileOutcome::Changed { .. } => {
116                    success += 1;
117                    changed += 1;
118                }
119                FileOutcome::Error { error, .. } => {
120                    failed += 1;
121                    errors.push((result.path, error));
122                }
123            }
124        }
125
126        let duration = start.elapsed();
127
128        Self {
129            total,
130            success,
131            changed,
132            failed,
133            duration,
134            errors,
135        }
136    }
137
138    /// Returns true if all files were processed successfully
139    pub const fn is_success(&self) -> bool {
140        self.failed == 0
141    }
142
143    /// Calculates files processed per second
144    #[allow(clippy::cast_precision_loss)]
145    pub fn files_per_second(&self) -> f64 {
146        let secs = self.duration.as_secs_f64();
147        if secs > 0.0 {
148            self.total as f64 / secs
149        } else {
150            0.0
151        }
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158
159    #[test]
160    fn test_file_outcome_is_success() {
161        let success = FileOutcome::Success {
162            duration: Duration::from_millis(10),
163        };
164        assert!(success.is_success());
165
166        let changed = FileOutcome::Changed {
167            duration: Duration::from_millis(10),
168        };
169        assert!(changed.is_success());
170
171        let unchanged = FileOutcome::Unchanged {
172            duration: Duration::from_millis(10),
173        };
174        assert!(unchanged.is_success());
175
176        let failed = FileOutcome::Error {
177            error: Error::Format {
178                message: "test".to_string(),
179            },
180            duration: Duration::from_millis(5),
181        };
182        assert!(!failed.is_success());
183    }
184
185    #[test]
186    fn test_file_outcome_duration() {
187        let outcome = FileOutcome::Changed {
188            duration: Duration::from_millis(123),
189        };
190        assert_eq!(outcome.duration(), Duration::from_millis(123));
191    }
192
193    #[test]
194    fn test_file_outcome_was_changed() {
195        let changed = FileOutcome::Changed {
196            duration: Duration::from_millis(10),
197        };
198        assert!(changed.was_changed());
199
200        let unchanged = FileOutcome::Unchanged {
201            duration: Duration::from_millis(10),
202        };
203        assert!(!unchanged.was_changed());
204
205        let success = FileOutcome::Success {
206            duration: Duration::from_millis(5),
207        };
208        assert!(!success.was_changed());
209    }
210
211    #[test]
212    fn test_file_result_new() {
213        let path = PathBuf::from("/test/file.yaml");
214        let outcome = FileOutcome::Changed {
215            duration: Duration::from_millis(10),
216        };
217        let result = FileResult::new(path.clone(), outcome);
218        assert_eq!(result.path, path);
219        assert!(result.is_success());
220    }
221
222    #[test]
223    fn test_batch_result_from_results() {
224        let results = vec![
225            FileResult::new(
226                PathBuf::from("/test/file1.yaml"),
227                FileOutcome::Changed {
228                    duration: Duration::from_millis(10),
229                },
230            ),
231            FileResult::new(
232                PathBuf::from("/test/file2.yaml"),
233                FileOutcome::Unchanged {
234                    duration: Duration::from_millis(5),
235                },
236            ),
237            FileResult::new(
238                PathBuf::from("/test/file3.yaml"),
239                FileOutcome::Success {
240                    duration: Duration::from_millis(3),
241                },
242            ),
243            FileResult::new(
244                PathBuf::from("/test/file4.yaml"),
245                FileOutcome::Error {
246                    error: Error::Format {
247                        message: "error".to_string(),
248                    },
249                    duration: Duration::from_millis(2),
250                },
251            ),
252        ];
253
254        let batch = BatchResult::from_results(results);
255        assert_eq!(batch.total, 4);
256        assert_eq!(batch.success, 3);
257        assert_eq!(batch.changed, 1);
258        assert_eq!(batch.failed, 1);
259        assert_eq!(batch.errors.len(), 1);
260        assert!(!batch.is_success());
261    }
262
263    #[test]
264    fn test_batch_result_is_success() {
265        let mut batch = BatchResult::new();
266        assert!(batch.is_success());
267
268        batch.failed = 1;
269        assert!(!batch.is_success());
270    }
271
272    #[test]
273    fn test_batch_result_files_per_second() {
274        let batch = BatchResult {
275            total: 100,
276            success: 100,
277            changed: 50,
278            failed: 0,
279            duration: Duration::from_secs(2),
280            errors: vec![],
281        };
282        assert!((batch.files_per_second() - 50.0).abs() < f64::EPSILON);
283    }
284
285    #[test]
286    fn test_batch_result_files_per_second_zero_duration() {
287        let batch = BatchResult {
288            total: 100,
289            success: 100,
290            changed: 0,
291            failed: 0,
292            duration: Duration::from_secs(0),
293            errors: vec![],
294        };
295        assert!((batch.files_per_second() - 0.0).abs() < f64::EPSILON);
296    }
297
298    #[test]
299    fn test_batch_result_from_empty_vec() {
300        let batch = BatchResult::from_results(vec![]);
301        assert_eq!(batch.total, 0);
302        assert_eq!(batch.success, 0);
303        assert_eq!(batch.changed, 0);
304        assert_eq!(batch.failed, 0);
305        assert!(batch.is_success());
306        assert_eq!(batch.errors.len(), 0);
307    }
308
309    #[test]
310    fn test_batch_result_all_success() {
311        let results = vec![
312            FileResult::new(
313                PathBuf::from("/a.yaml"),
314                FileOutcome::Success {
315                    duration: Duration::from_millis(1),
316                },
317            ),
318            FileResult::new(
319                PathBuf::from("/b.yaml"),
320                FileOutcome::Success {
321                    duration: Duration::from_millis(2),
322                },
323            ),
324        ];
325
326        let batch = BatchResult::from_results(results);
327        assert_eq!(batch.total, 2);
328        assert_eq!(batch.success, 2);
329        assert_eq!(batch.changed, 0);
330        assert_eq!(batch.failed, 0);
331        assert!(batch.is_success());
332    }
333
334    #[test]
335    fn test_batch_result_all_failures() {
336        let results = vec![
337            FileResult::new(
338                PathBuf::from("/a.yaml"),
339                FileOutcome::Error {
340                    error: Error::Format {
341                        message: "error1".to_string(),
342                    },
343                    duration: Duration::from_millis(1),
344                },
345            ),
346            FileResult::new(
347                PathBuf::from("/b.yaml"),
348                FileOutcome::Error {
349                    error: Error::Format {
350                        message: "error2".to_string(),
351                    },
352                    duration: Duration::from_millis(2),
353                },
354            ),
355        ];
356
357        let batch = BatchResult::from_results(results);
358        assert_eq!(batch.total, 2);
359        assert_eq!(batch.success, 0);
360        assert_eq!(batch.failed, 2);
361        assert!(!batch.is_success());
362        assert_eq!(batch.errors.len(), 2);
363    }
364
365    #[test]
366    fn test_batch_result_all_changed() {
367        let results = vec![
368            FileResult::new(
369                PathBuf::from("/a.yaml"),
370                FileOutcome::Changed {
371                    duration: Duration::from_millis(1),
372                },
373            ),
374            FileResult::new(
375                PathBuf::from("/b.yaml"),
376                FileOutcome::Changed {
377                    duration: Duration::from_millis(2),
378                },
379            ),
380        ];
381
382        let batch = BatchResult::from_results(results);
383        assert_eq!(batch.total, 2);
384        assert_eq!(batch.success, 2);
385        assert_eq!(batch.changed, 2);
386        assert_eq!(batch.failed, 0);
387        assert!(batch.is_success());
388    }
389
390    #[test]
391    fn test_file_outcome_zero_duration() {
392        let outcome = FileOutcome::Success {
393            duration: Duration::ZERO,
394        };
395        assert_eq!(outcome.duration(), Duration::ZERO);
396        assert!(outcome.is_success());
397    }
398
399    #[test]
400    fn test_batch_result_files_per_second_nanoseconds() {
401        let batch = BatchResult {
402            total: 1000,
403            success: 1000,
404            changed: 0,
405            failed: 0,
406            duration: Duration::from_nanos(1),
407            errors: vec![],
408        };
409
410        let fps = batch.files_per_second();
411        assert!(fps > 0.0);
412        assert!(fps.is_finite());
413    }
414
415    // Property-based tests using proptest
416    use proptest::prelude::*;
417
418    proptest! {
419        /// Property: BatchResult.total == success + failed
420        #[test]
421        fn prop_batch_total_invariant(
422            success in 0usize..1000,
423            failed in 0usize..1000,
424        ) {
425            let total = success + failed;
426            let batch = BatchResult {
427                total,
428                success,
429                changed: 0,
430                failed,
431                duration: Duration::from_secs(1),
432                errors: vec![],
433            };
434
435            prop_assert_eq!(batch.total, batch.success + batch.failed);
436        }
437
438        /// Property: success_count is always >= 0 and <= total
439        #[test]
440        fn prop_success_count_bounds(
441            total in 0usize..1000,
442            failed in 0usize..1000,
443        ) {
444            let success = total.saturating_sub(failed);
445            let batch = BatchResult {
446                total,
447                success,
448                changed: 0,
449                failed,
450                duration: Duration::from_secs(1),
451                errors: vec![],
452            };
453
454            prop_assert!(batch.success <= batch.total);
455        }
456
457        /// Property: files_per_second is always >= 0.0
458        #[test]
459        fn prop_files_per_second_non_negative(
460            total in 0usize..1000,
461            duration_ms in 0u64..10000,
462        ) {
463            let batch = BatchResult {
464                total,
465                success: total,
466                changed: 0,
467                failed: 0,
468                duration: Duration::from_millis(duration_ms),
469                errors: vec![],
470            };
471
472            let fps = batch.files_per_second();
473            prop_assert!(fps >= 0.0);
474            prop_assert!(fps.is_finite());
475        }
476
477        /// Property: changed <= success
478        #[test]
479        fn prop_changed_le_success(
480            total in 0usize..1000,
481            success in 0usize..1000,
482            changed in 0usize..1000,
483        ) {
484            let success = success.min(total);
485            let changed = changed.min(success);
486            let batch = BatchResult {
487                total,
488                success,
489                changed,
490                failed: total.saturating_sub(success),
491                duration: Duration::from_secs(1),
492                errors: vec![],
493            };
494
495            prop_assert!(batch.changed <= batch.success);
496        }
497
498        /// Property: errors.len() == failed
499        #[test]
500        fn prop_errors_len_eq_failed(
501            success in 0usize..100,
502            failed in 0usize..100,
503        ) {
504            let total = success + failed;
505            let mut errors = Vec::with_capacity(failed);
506            for i in 0..failed {
507                errors.push((
508                    PathBuf::from(format!("/file{i}.yaml")),
509                    Error::Format { message: format!("error{i}") }
510                ));
511            }
512
513            let batch = BatchResult {
514                total,
515                success,
516                changed: 0,
517                failed,
518                duration: Duration::from_secs(1),
519                errors,
520            };
521
522            prop_assert_eq!(batch.errors.len(), batch.failed);
523        }
524    }
525}