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