1use super::ArchiveStep;
5use crate::{helpers::plural, redact::Redactor, write_str::WriteStr};
6use camino::Utf8Path;
7use owo_colors::{OwoColorize, Style};
8use std::{io, time::Duration};
9use swrite::{SWrite, swrite};
10
11#[derive(Debug)]
12pub struct ArchiveReporter {
14 styles: Styles,
15 verbose: bool,
16 redactor: Redactor,
17
18 linked_path_hint_emitted: bool,
19 }
21
22impl ArchiveReporter {
23 pub fn new(verbose: bool, redactor: Redactor) -> Self {
25 Self {
26 styles: Styles::default(),
27 verbose,
28 redactor,
29
30 linked_path_hint_emitted: false,
31 }
32 }
33
34 pub fn colorize(&mut self) {
36 self.styles.colorize();
37 }
38
39 pub fn report_event(
41 &mut self,
42 event: ArchiveEvent<'_>,
43 writer: &mut dyn WriteStr,
44 ) -> io::Result<()> {
45 match event {
46 ArchiveEvent::ArchiveStarted {
47 counts,
48 output_file,
49 } => {
50 write!(writer, "{:>12} ", "Archiving".style(self.styles.success))?;
51
52 self.report_counts(counts, writer)?;
53
54 writeln!(
55 writer,
56 " to {}",
57 self.redactor
58 .redact_path(output_file)
59 .style(self.styles.bold)
60 )?;
61 }
62 ArchiveEvent::StdlibPathError { error } => {
63 write!(writer, "{:>12} ", "Warning".style(self.styles.bold))?;
64 writeln!(
65 writer,
66 "could not find standard library for host (proc macro tests may not work): {error}"
67 )?;
68 }
69 ArchiveEvent::ExtraPathMissing { path, warn } => {
70 if warn {
71 write!(writer, "{:>12} ", "Warning".style(self.styles.warning))?;
72 } else if self.verbose {
73 write!(writer, "{:>12} ", "Skipped".style(self.styles.skipped))?;
74 } else {
75 return Ok(()); }
77
78 writeln!(
79 writer,
80 "ignoring extra path `{}` because it does not exist",
81 self.redactor.redact_path(path).style(self.styles.bold),
82 )?;
83 }
84 ArchiveEvent::DirectoryAtDepthZero { path } => {
85 write!(writer, "{:>12} ", "Warning".style(self.styles.warning))?;
86 writeln!(
87 writer,
88 "ignoring extra path `{}` specified with depth 0 since it is a directory",
89 self.redactor.redact_path(path).style(self.styles.bold),
90 )?;
91 }
92 ArchiveEvent::RecursionDepthExceeded {
93 step,
94 path,
95 limit,
96 warn,
97 } => {
98 if warn {
99 write!(writer, "{:>12} ", "Warning".style(self.styles.warning))?;
100 } else if self.verbose {
101 write!(writer, "{:>12} ", "Skipped".style(self.styles.skipped))?;
102 } else {
103 return Ok(()); }
105
106 writeln!(
107 writer,
108 "while archiving {step}, recursion depth exceeded at {} (limit: {limit})",
109 self.redactor.redact_path(path).style(self.styles.bold),
110 )?;
111 }
112 ArchiveEvent::UnknownFileType { step, path } => {
113 write!(writer, "{:>12} ", "Warning".style(self.styles.warning))?;
114 writeln!(
115 writer,
116 "while archiving {step}, ignoring `{}` because it is not a file, \
117 directory, or symbolic link",
118 self.redactor.redact_path(path).style(self.styles.bold),
119 )?;
120 }
121 ArchiveEvent::LinkedPathNotFound { path, requested_by } => {
122 write!(writer, "{:>12} ", "Warning".style(self.styles.warning))?;
123 writeln!(
124 writer,
125 "linked path `{}` not found, requested by: {}",
126 self.redactor.redact_path(path).style(self.styles.bold),
127 requested_by.join(", ").style(self.styles.bold),
128 )?;
129 if !self.linked_path_hint_emitted {
130 write!(writer, "{:>12} ", "")?;
131 writeln!(
132 writer,
133 "(this is a bug in {} that should be fixed)",
134 plural::this_crate_str(requested_by.len())
135 )?;
136 self.linked_path_hint_emitted = true;
137 }
138 }
139 ArchiveEvent::Archived {
140 file_count,
141 output_file,
142 elapsed,
143 } => {
144 write!(writer, "{:>12} ", "Archived".style(self.styles.success))?;
145 writeln!(
146 writer,
147 "{} files to {} in {}",
148 self.redactor
149 .redact_file_count(file_count)
150 .style(self.styles.bold),
151 self.redactor
152 .redact_path(output_file)
153 .style(self.styles.bold),
154 self.redactor.redact_duration(elapsed),
155 )?;
156 }
157 ArchiveEvent::ExtractStarted {
158 test_binary_count,
159 non_test_binary_count,
160 build_script_out_dir_count,
161 linked_path_count,
162 dest_dir: destination_dir,
163 } => {
164 write!(writer, "{:>12} ", "Extracting".style(self.styles.success))?;
165
166 self.report_counts(
167 ArchiveCounts {
168 test_binary_count,
169 filter_counts: ArchiveFilterCounts::default(),
172 non_test_binary_count,
173 build_script_out_dir_count,
174 linked_path_count,
175 extra_path_count: 0,
178 stdlib_count: 0,
179 },
180 writer,
181 )?;
182
183 writeln!(writer, " to {}", destination_dir.style(self.styles.bold))?;
184 }
185 ArchiveEvent::Extracted {
186 file_count,
187 dest_dir: destination_dir,
188 elapsed,
189 } => {
190 write!(writer, "{:>12} ", "Extracted".style(self.styles.success))?;
191 writeln!(
192 writer,
193 "{} {} to {} in {}",
194 self.redactor
195 .redact_file_count(file_count)
196 .style(self.styles.bold),
197 plural::files_str(file_count),
198 self.redactor
199 .redact_path(destination_dir)
200 .style(self.styles.bold),
201 self.redactor.redact_duration(elapsed),
202 )?;
203 }
204 }
205
206 Ok(())
207 }
208
209 fn report_counts(
210 &mut self,
211 counts: ArchiveCounts,
212 writer: &mut dyn WriteStr,
213 ) -> io::Result<()> {
214 let ArchiveCounts {
215 test_binary_count,
216 filter_counts:
217 ArchiveFilterCounts {
218 filtered_out_test_binary_count,
219 filtered_out_non_test_binary_count,
220 filtered_out_build_script_out_dir_count,
221 },
222 non_test_binary_count,
223 build_script_out_dir_count,
224 linked_path_count,
225 extra_path_count,
226 stdlib_count,
227 } = counts;
228
229 let total_binary_count = test_binary_count + non_test_binary_count;
230 let mut in_parens = Vec::new();
231 if non_test_binary_count > 0 {
232 in_parens.push(format!(
233 "including {} non-test {}",
234 non_test_binary_count.style(self.styles.bold),
235 plural::binaries_str(non_test_binary_count),
236 ));
237 }
238 if filtered_out_test_binary_count > 0 || filtered_out_non_test_binary_count > 0 {
239 let mut filtered_out = Vec::new();
240 if filtered_out_test_binary_count > 0 {
241 filtered_out.push(format!(
242 "{} test {}",
243 filtered_out_test_binary_count.style(self.styles.bold),
244 plural::binaries_str(filtered_out_test_binary_count),
245 ));
246 }
247 if filtered_out_non_test_binary_count > 0 {
248 filtered_out.push(format!(
249 "{} non-test {}",
250 filtered_out_non_test_binary_count.style(self.styles.bold),
251 plural::binaries_str(filtered_out_non_test_binary_count),
252 ));
253 }
254
255 in_parens.push(format!("{} filtered out", filtered_out.join(" and ")));
256 }
257 let mut more = Vec::new();
258 if build_script_out_dir_count > 0 {
259 let mut s = format!(
260 "{} build script output {}",
261 build_script_out_dir_count.style(self.styles.bold),
262 plural::directories_str(build_script_out_dir_count),
263 );
264 if filtered_out_build_script_out_dir_count > 0 {
265 swrite!(
266 s,
267 " ({} filtered out)",
268 filtered_out_build_script_out_dir_count.style(self.styles.bold)
269 );
270 }
271 more.push(s);
272 }
273 if linked_path_count > 0 {
274 more.push(format!(
275 "{} linked {}",
276 linked_path_count.style(self.styles.bold),
277 plural::paths_str(linked_path_count),
278 ));
279 }
280 if extra_path_count > 0 {
281 more.push(format!(
282 "{} extra {}",
283 extra_path_count.style(self.styles.bold),
284 plural::paths_str(extra_path_count),
285 ));
286 }
287 if stdlib_count > 0 {
288 more.push(format!(
289 "{} standard {}",
290 stdlib_count.style(self.styles.bold),
291 plural::libraries_str(stdlib_count),
292 ));
293 }
294
295 let parens_text = if in_parens.is_empty() {
296 String::new()
297 } else {
298 format!(" ({})", in_parens.join("; "))
299 };
300
301 write!(
302 writer,
303 "{} {}{parens_text}",
304 total_binary_count.style(self.styles.bold),
305 plural::binaries_str(total_binary_count),
306 )?;
307
308 match more.len() {
309 0 => Ok(()),
310 1 => {
311 write!(writer, " and {}", more[0])
312 }
313 _ => {
314 write!(
315 writer,
316 ", {}, and {}",
317 more[..more.len() - 1].join(", "),
318 more.last().unwrap(),
319 )
320 }
321 }
322 }
323}
324
325#[derive(Debug, Default)]
326struct Styles {
327 bold: Style,
328 success: Style,
329 warning: Style,
330 skipped: Style,
331}
332
333impl Styles {
334 fn colorize(&mut self) {
335 self.bold = Style::new().bold();
336 self.success = Style::new().green().bold();
337 self.warning = Style::new().yellow().bold();
338 self.skipped = Style::new().bold();
339 }
340}
341
342#[derive(Clone, Debug)]
346#[non_exhaustive]
347pub enum ArchiveEvent<'a> {
348 ArchiveStarted {
350 counts: ArchiveCounts,
352
353 output_file: &'a Utf8Path,
355 },
356
357 StdlibPathError {
359 error: &'a str,
361 },
362
363 ExtraPathMissing {
365 path: &'a Utf8Path,
367
368 warn: bool,
370 },
371
372 DirectoryAtDepthZero {
374 path: &'a Utf8Path,
376 },
377
378 RecursionDepthExceeded {
380 step: ArchiveStep,
382
383 path: &'a Utf8Path,
385
386 limit: usize,
388
389 warn: bool,
391 },
392
393 UnknownFileType {
395 step: ArchiveStep,
397
398 path: &'a Utf8Path,
400 },
401
402 LinkedPathNotFound {
404 path: &'a Utf8Path,
406
407 requested_by: &'a [String],
409 },
410
411 Archived {
413 file_count: usize,
415
416 output_file: &'a Utf8Path,
418
419 elapsed: Duration,
421 },
422
423 ExtractStarted {
425 test_binary_count: usize,
427
428 non_test_binary_count: usize,
430
431 build_script_out_dir_count: usize,
433
434 linked_path_count: usize,
436
437 dest_dir: &'a Utf8Path,
439 },
440
441 Extracted {
443 file_count: usize,
445
446 dest_dir: &'a Utf8Path,
448
449 elapsed: Duration,
451 },
452}
453
454#[derive(Clone, Copy, Debug, Default)]
456pub struct ArchiveCounts {
457 pub test_binary_count: usize,
460
461 pub filter_counts: ArchiveFilterCounts,
463
464 pub non_test_binary_count: usize,
466
467 pub build_script_out_dir_count: usize,
469
470 pub linked_path_count: usize,
472
473 pub extra_path_count: usize,
475
476 pub stdlib_count: usize,
478}
479
480#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
482pub struct ArchiveFilterCounts {
483 pub filtered_out_test_binary_count: usize,
485
486 pub filtered_out_non_test_binary_count: usize,
488
489 pub filtered_out_build_script_out_dir_count: usize,
491}
492
493#[cfg(test)]
494mod tests {
495 use super::*;
496 use test_case::test_case;
497
498 #[test_case(
499 ArchiveCounts {
500 test_binary_count: 1,
501 ..Default::default()
502 },
503 "1 binary"
504 ; "single test binary"
505 )]
506 #[test_case(
507 ArchiveCounts {
508 test_binary_count: 5,
509 ..Default::default()
510 },
511 "5 binaries"
512 ; "multiple test binaries"
513 )]
514 #[test_case(
515 ArchiveCounts {
516 test_binary_count: 5,
517 non_test_binary_count: 2,
518 ..Default::default()
519 },
520 "7 binaries (including 2 non-test binaries)"
521 ; "with non-test binaries"
522 )]
523 #[test_case(
524 ArchiveCounts {
525 test_binary_count: 5,
526 filter_counts: ArchiveFilterCounts {
527 filtered_out_test_binary_count: 2,
528 ..Default::default()
529 },
530 ..Default::default()
531 },
532 "5 binaries (2 test binaries filtered out)"
533 ; "with filtered out test binaries"
534 )]
535 #[test_case(
536 ArchiveCounts {
537 test_binary_count: 5,
538 filter_counts: ArchiveFilterCounts {
539 filtered_out_non_test_binary_count: 1,
540 ..Default::default()
541 },
542 ..Default::default()
543 },
544 "5 binaries (1 non-test binary filtered out)"
545 ; "with filtered out non-test binary"
546 )]
547 #[test_case(
548 ArchiveCounts {
549 test_binary_count: 5,
550 filter_counts: ArchiveFilterCounts {
551 filtered_out_test_binary_count: 2,
552 filtered_out_non_test_binary_count: 3,
553 ..Default::default()
554 },
555 ..Default::default()
556 },
557 "5 binaries (2 test binaries and 3 non-test binaries filtered out)"
558 ; "with both types filtered out"
559 )]
560 #[test_case(
561 ArchiveCounts {
562 test_binary_count: 5,
563 filter_counts: ArchiveFilterCounts {
564 filtered_out_test_binary_count: 1,
565 filtered_out_non_test_binary_count: 2,
566 ..Default::default()
567 },
568 non_test_binary_count: 3,
569 ..Default::default()
570 },
571 "8 binaries (including 3 non-test binaries; 1 test binary and 2 non-test binaries filtered out)"
572 ; "with non-test binaries and both types filtered out"
573 )]
574 #[test_case(
575 ArchiveCounts {
576 filter_counts: ArchiveFilterCounts {
577 filtered_out_non_test_binary_count: 2,
578 ..Default::default()
579 },
580 ..Default::default()
581 },
582 "0 binaries (2 non-test binaries filtered out)"
583 ; "zero binaries with filtered out"
584 )]
585 #[test_case(
586 ArchiveCounts {
587 test_binary_count: 5,
588 build_script_out_dir_count: 1,
589 ..Default::default()
590 },
591 "5 binaries and 1 build script output directory"
592 ; "with single more item"
593 )]
594 #[test_case(
595 ArchiveCounts {
596 test_binary_count: 5,
597 build_script_out_dir_count: 3,
598 filter_counts: ArchiveFilterCounts {
599 filtered_out_build_script_out_dir_count: 2,
600 ..Default::default()
601 },
602 ..Default::default()
603 },
604 "5 binaries and 3 build script output directories (2 filtered out)"
605 ; "with filtered out build script out dirs"
606 )]
607 #[test_case(
608 ArchiveCounts {
609 test_binary_count: 5,
610 linked_path_count: 3,
611 ..Default::default()
612 },
613 "5 binaries and 3 linked paths"
614 ; "with linked paths"
615 )]
616 #[test_case(
617 ArchiveCounts {
618 test_binary_count: 5,
619 build_script_out_dir_count: 2,
620 linked_path_count: 3,
621 extra_path_count: 1,
622 stdlib_count: 4,
623 ..Default::default()
624 },
625 "5 binaries, 2 build script output directories, 3 linked paths, 1 extra path, and 4 standard libraries"
626 ; "with multiple more items"
627 )]
628 #[test_case(
629 ArchiveCounts {
630 test_binary_count: 4,
631 filter_counts: ArchiveFilterCounts {
632 filtered_out_test_binary_count: 1,
633 filtered_out_non_test_binary_count: 2,
634 filtered_out_build_script_out_dir_count: 1,
635 },
636 non_test_binary_count: 2,
637 build_script_out_dir_count: 3,
638 linked_path_count: 2,
639 extra_path_count: 1,
640 stdlib_count: 2,
641 },
642 "6 binaries (including 2 non-test binaries; 1 test binary and 2 non-test binaries filtered out), 3 build script output directories (1 filtered out), 2 linked paths, 1 extra path, and 2 standard libraries"
643 ; "all fields combined"
644 )]
645 #[test_case(
646 ArchiveCounts::default(),
647 "0 binaries"
648 ; "all zeros"
649 )]
650 fn test_report_counts(counts: ArchiveCounts, expected: &str) {
651 let mut reporter = ArchiveReporter::new(false, Redactor::noop());
652 let mut buffer = String::new();
653
654 reporter.report_counts(counts, &mut buffer).unwrap();
655
656 assert_eq!(buffer, expected);
657 }
658}