nextest_runner/reuse_build/
archive_reporter.rs

1// Copyright (c) The nextest Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use 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)]
15/// Reporter for archive operations.
16pub struct ArchiveReporter {
17    styles: Styles,
18    verbose: bool,
19    redactor: Redactor,
20
21    linked_path_hint_emitted: bool,
22    // TODO: message-format json?
23}
24
25impl ArchiveReporter {
26    /// Creates a new reporter for archive events.
27    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    /// Colorizes output.
38    pub fn colorize(&mut self) {
39        self.styles.colorize();
40    }
41
42    /// Reports an archive event.
43    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(()); // Skip
79                }
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(()); // Skip
107                }
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                        // We don't track filtered-out test binaries during
173                        // extraction.
174                        filter_counts: ArchiveFilterCounts::default(),
175                        non_test_binary_count,
176                        build_script_out_dir_count,
177                        linked_path_count,
178                        // TODO: we currently don't store a list of extra paths or standard libs at
179                        // manifest creation time, so we can't report this count here.
180                        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/// An archive event.
342///
343/// Events are produced by archive and extract operations, and consumed by an [`ArchiveReporter`].
344#[derive(Clone, Debug)]
345#[non_exhaustive]
346pub enum ArchiveEvent<'a> {
347    /// The archive process started.
348    ArchiveStarted {
349        /// File counts.
350        counts: ArchiveCounts,
351
352        /// The archive output file.
353        output_file: &'a Utf8Path,
354    },
355
356    /// An error occurred while obtaining the path to a standard library.
357    StdlibPathError {
358        /// The error that occurred.
359        error: &'a str,
360    },
361
362    /// A provided extra path did not exist.
363    ExtraPathMissing {
364        /// The path that was missing.
365        path: &'a Utf8Path,
366
367        /// Whether the reporter should produce a warning about this.
368        warn: bool,
369    },
370
371    /// For an extra include, a directory was specified at depth 0.
372    DirectoryAtDepthZero {
373        /// The directory that was at depth 0.
374        path: &'a Utf8Path,
375    },
376
377    /// While performing the archive, the recursion depth was exceeded.
378    RecursionDepthExceeded {
379        /// The current step in the archive process.
380        step: ArchiveStep,
381
382        /// The path that exceeded the recursion depth.
383        path: &'a Utf8Path,
384
385        /// The recursion depth limit that was hit.
386        limit: usize,
387
388        /// Whether the reporter should produce a warning about this.
389        warn: bool,
390    },
391
392    /// The archive process encountered an unknown file type.
393    UnknownFileType {
394        /// The current step in the archive process.
395        step: ArchiveStep,
396
397        /// The path of the unknown type.
398        path: &'a Utf8Path,
399    },
400
401    /// A crate linked against a non-existent path.
402    LinkedPathNotFound {
403        /// The path of the linked file.
404        path: &'a Utf8Path,
405
406        /// The crates that linked against the path.
407        requested_by: &'a [String],
408    },
409
410    /// The archive operation completed successfully.
411    Archived {
412        /// The number of files archived.
413        file_count: usize,
414
415        /// The archive output file.
416        output_file: &'a Utf8Path,
417
418        /// How long it took to create the archive.
419        elapsed: Duration,
420    },
421
422    /// The extraction process started.
423    ExtractStarted {
424        /// The number of test binaries to extract.
425        test_binary_count: usize,
426
427        /// The number of non-test binaries to extract.
428        non_test_binary_count: usize,
429
430        /// The number of build script output directories to archive.
431        build_script_out_dir_count: usize,
432
433        /// The number of linked paths to extract.
434        linked_path_count: usize,
435
436        /// The destination directory.
437        dest_dir: &'a Utf8Path,
438    },
439
440    /// The extraction process completed successfully.
441    Extracted {
442        /// The number of files extracted.
443        file_count: usize,
444
445        /// The destination directory.
446        dest_dir: &'a Utf8Path,
447
448        /// How long it took to extract the archive.
449        elapsed: Duration,
450    },
451}
452
453/// Counts of various types of files in an archive.
454#[derive(Clone, Copy, Debug, Default)]
455pub struct ArchiveCounts {
456    /// The number of test binaries that will be included in the archive, not
457    /// including filtered out test binaries.
458    pub test_binary_count: usize,
459
460    /// Counts for filtered out binaries.
461    pub filter_counts: ArchiveFilterCounts,
462
463    /// The number of non-test binaries.
464    pub non_test_binary_count: usize,
465
466    /// The number of build script output directories.
467    pub build_script_out_dir_count: usize,
468
469    /// The number of linked paths.
470    pub linked_path_count: usize,
471
472    /// The number of extra paths.
473    pub extra_path_count: usize,
474
475    /// The number of standard libraries.
476    pub stdlib_count: usize,
477}
478
479/// Counts the number of filtered out binaries.
480#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
481pub struct ArchiveFilterCounts {
482    /// The number of filtered out test binaries.
483    pub filtered_out_test_binary_count: usize,
484
485    /// The number of filtered out non-test binaries.
486    pub filtered_out_non_test_binary_count: usize,
487
488    /// The number of filtered out build script output directories.
489    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}