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