1use std::path::PathBuf;
4use std::time::Duration;
5
6use crate::error::Error;
7
8#[derive(Debug)]
10pub enum FileOutcome {
11 Success {
13 duration: Duration,
15 },
16 Changed {
18 duration: Duration,
20 },
21 Unchanged {
23 duration: Duration,
25 },
26 Error {
28 error: Error,
30 duration: Duration,
32 },
33}
34
35impl FileOutcome {
36 pub const fn is_success(&self) -> bool {
38 !matches!(self, Self::Error { .. })
39 }
40
41 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 pub const fn was_changed(&self) -> bool {
53 matches!(self, Self::Changed { .. })
54 }
55}
56
57#[derive(Debug)]
59pub struct FileResult {
60 pub path: PathBuf,
62 pub outcome: FileOutcome,
64}
65
66impl FileResult {
67 pub const fn new(path: PathBuf, outcome: FileOutcome) -> Self {
69 Self { path, outcome }
70 }
71
72 pub const fn is_success(&self) -> bool {
74 self.outcome.is_success()
75 }
76}
77
78#[derive(Debug, Default)]
80pub struct BatchResult {
81 pub total: usize,
83 pub success: usize,
85 pub changed: usize,
87 pub failed: usize,
89 pub duration: Duration,
91 pub errors: Vec<(PathBuf, Error)>,
93}
94
95impl BatchResult {
96 pub fn new() -> Self {
98 Self::default()
99 }
100
101 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 pub const fn is_success(&self) -> bool {
140 self.failed == 0
141 }
142
143 #[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 use proptest::prelude::*;
417
418 proptest! {
419 #[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 #[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 #[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 #[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 #[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}