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};
12
13#[derive(Debug)]
14/// Reporter for archive operations.
15pub struct ArchiveReporter {
16    styles: Styles,
17    verbose: bool,
18    redactor: Redactor,
19
20    linked_path_hint_emitted: bool,
21    // TODO: message-format json?
22}
23
24impl ArchiveReporter {
25    /// Creates a new reporter for archive events.
26    pub fn new(verbose: bool, redactor: Redactor) -> Self {
27        Self {
28            styles: Styles::default(),
29            verbose,
30            redactor,
31
32            linked_path_hint_emitted: false,
33        }
34    }
35
36    /// Colorizes output.
37    pub fn colorize(&mut self) {
38        self.styles.colorize();
39    }
40
41    /// Reports an archive event.
42    pub fn report_event(
43        &mut self,
44        event: ArchiveEvent<'_>,
45        mut writer: impl Write,
46    ) -> io::Result<()> {
47        match event {
48            ArchiveEvent::ArchiveStarted {
49                counts,
50                output_file,
51            } => {
52                write!(writer, "{:>12} ", "Archiving".style(self.styles.success))?;
53
54                self.report_counts(counts, &mut writer)?;
55
56                writeln!(
57                    writer,
58                    " to {}",
59                    self.redactor
60                        .redact_path(output_file)
61                        .style(self.styles.bold)
62                )?;
63            }
64            ArchiveEvent::StdlibPathError { error } => {
65                write!(writer, "{:>12} ", "Warning".style(self.styles.bold))?;
66                writeln!(
67                    writer,
68                    "could not find standard library for host (proc macro tests may not work): {error}"
69                )?;
70            }
71            ArchiveEvent::ExtraPathMissing { path, warn } => {
72                if warn {
73                    write!(writer, "{:>12} ", "Warning".style(self.styles.warning))?;
74                } else if self.verbose {
75                    write!(writer, "{:>12} ", "Skipped".style(self.styles.skipped))?;
76                } else {
77                    return Ok(()); // Skip
78                }
79
80                writeln!(
81                    writer,
82                    "ignoring extra path `{}` because it does not exist",
83                    self.redactor.redact_path(path).style(self.styles.bold),
84                )?;
85            }
86            ArchiveEvent::DirectoryAtDepthZero { path } => {
87                write!(writer, "{:>12} ", "Warning".style(self.styles.warning))?;
88                writeln!(
89                    writer,
90                    "ignoring extra path `{}` specified with depth 0 since it is a directory",
91                    self.redactor.redact_path(path).style(self.styles.bold),
92                )?;
93            }
94            ArchiveEvent::RecursionDepthExceeded {
95                step,
96                path,
97                limit,
98                warn,
99            } => {
100                if warn {
101                    write!(writer, "{:>12} ", "Warning".style(self.styles.warning))?;
102                } else if self.verbose {
103                    write!(writer, "{:>12} ", "Skipped".style(self.styles.skipped))?;
104                } else {
105                    return Ok(()); // Skip
106                }
107
108                writeln!(
109                    writer,
110                    "while archiving {step}, recursion depth exceeded at {} (limit: {limit})",
111                    self.redactor.redact_path(path).style(self.styles.bold),
112                )?;
113            }
114            ArchiveEvent::UnknownFileType { step, path } => {
115                write!(writer, "{:>12} ", "Warning".style(self.styles.warning))?;
116                writeln!(
117                    writer,
118                    "while archiving {step}, ignoring `{}` because it is not a file, \
119                     directory, or symbolic link",
120                    self.redactor.redact_path(path).style(self.styles.bold),
121                )?;
122            }
123            ArchiveEvent::LinkedPathNotFound { path, requested_by } => {
124                write!(writer, "{:>12} ", "Warning".style(self.styles.warning))?;
125                writeln!(
126                    writer,
127                    "linked path `{}` not found, requested by: {}",
128                    self.redactor.redact_path(path).style(self.styles.bold),
129                    requested_by.join(", ").style(self.styles.bold),
130                )?;
131                if !self.linked_path_hint_emitted {
132                    write!(writer, "{:>12} ", "")?;
133                    writeln!(
134                        writer,
135                        "(this is a bug in {} that should be fixed)",
136                        plural::this_crate_str(requested_by.len())
137                    )?;
138                    self.linked_path_hint_emitted = true;
139                }
140            }
141            ArchiveEvent::Archived {
142                file_count,
143                output_file,
144                elapsed,
145            } => {
146                write!(writer, "{:>12} ", "Archived".style(self.styles.success))?;
147                writeln!(
148                    writer,
149                    "{} files to {} in {}",
150                    self.redactor
151                        .redact_file_count(file_count)
152                        .style(self.styles.bold),
153                    self.redactor
154                        .redact_path(output_file)
155                        .style(self.styles.bold),
156                    self.redactor.redact_duration(elapsed),
157                )?;
158            }
159            ArchiveEvent::ExtractStarted {
160                test_binary_count,
161                non_test_binary_count,
162                build_script_out_dir_count,
163                linked_path_count,
164                dest_dir: destination_dir,
165            } => {
166                write!(writer, "{:>12} ", "Extracting".style(self.styles.success))?;
167
168                self.report_counts(
169                    ArchiveCounts {
170                        test_binary_count,
171                        non_test_binary_count,
172                        build_script_out_dir_count,
173                        linked_path_count,
174                        // TODO: we currently don't store a list of extra paths or standard libs at
175                        // manifest creation time, so we can't report this count here.
176                        extra_path_count: 0,
177                        stdlib_count: 0,
178                    },
179                    &mut writer,
180                )?;
181
182                writeln!(writer, " to {}", destination_dir.style(self.styles.bold))?;
183            }
184            ArchiveEvent::Extracted {
185                file_count,
186                dest_dir: destination_dir,
187                elapsed,
188            } => {
189                write!(writer, "{:>12} ", "Extracted".style(self.styles.success))?;
190                writeln!(
191                    writer,
192                    "{} {} to {} in {}",
193                    self.redactor
194                        .redact_file_count(file_count)
195                        .style(self.styles.bold),
196                    plural::files_str(file_count),
197                    self.redactor
198                        .redact_path(destination_dir)
199                        .style(self.styles.bold),
200                    self.redactor.redact_duration(elapsed),
201                )?;
202            }
203        }
204
205        Ok(())
206    }
207
208    fn report_counts(&mut self, counts: ArchiveCounts, mut writer: impl Write) -> io::Result<()> {
209        let ArchiveCounts {
210            test_binary_count,
211            non_test_binary_count,
212            build_script_out_dir_count,
213            linked_path_count,
214            extra_path_count,
215            stdlib_count,
216        } = counts;
217
218        let total_binary_count = test_binary_count + non_test_binary_count;
219        let non_test_text = if non_test_binary_count > 0 {
220            format!(
221                " (including {} non-test {})",
222                non_test_binary_count.style(self.styles.bold),
223                plural::binaries_str(non_test_binary_count),
224            )
225        } else {
226            "".to_owned()
227        };
228        let mut more = Vec::new();
229        if build_script_out_dir_count > 0 {
230            more.push(format!(
231                "{} build script output {}",
232                build_script_out_dir_count.style(self.styles.bold),
233                plural::directories_str(build_script_out_dir_count),
234            ));
235        }
236        if linked_path_count > 0 {
237            more.push(format!(
238                "{} linked {}",
239                linked_path_count.style(self.styles.bold),
240                plural::paths_str(linked_path_count),
241            ));
242        }
243        if extra_path_count > 0 {
244            more.push(format!(
245                "{} extra {}",
246                extra_path_count.style(self.styles.bold),
247                plural::paths_str(extra_path_count),
248            ));
249        }
250        if stdlib_count > 0 {
251            more.push(format!(
252                "{} standard {}",
253                stdlib_count.style(self.styles.bold),
254                plural::libraries_str(stdlib_count),
255            ));
256        }
257
258        write!(
259            writer,
260            "{} {}{non_test_text}",
261            total_binary_count.style(self.styles.bold),
262            plural::binaries_str(total_binary_count),
263        )?;
264
265        match more.len() {
266            0 => Ok(()),
267            1 => {
268                write!(writer, " and {}", more[0])
269            }
270            _ => {
271                write!(
272                    writer,
273                    ", {}, and {}",
274                    more[..more.len() - 1].join(", "),
275                    more.last().unwrap(),
276                )
277            }
278        }
279    }
280}
281
282#[derive(Debug, Default)]
283struct Styles {
284    bold: Style,
285    success: Style,
286    warning: Style,
287    skipped: Style,
288}
289
290impl Styles {
291    fn colorize(&mut self) {
292        self.bold = Style::new().bold();
293        self.success = Style::new().green().bold();
294        self.warning = Style::new().yellow().bold();
295        self.skipped = Style::new().bold();
296    }
297}
298
299/// An archive event.
300///
301/// Events are produced by archive and extract operations, and consumed by an [`ArchiveReporter`].
302#[derive(Clone, Debug)]
303#[non_exhaustive]
304pub enum ArchiveEvent<'a> {
305    /// The archive process started.
306    ArchiveStarted {
307        /// File counts.
308        counts: ArchiveCounts,
309
310        /// The archive output file.
311        output_file: &'a Utf8Path,
312    },
313
314    /// An error occurred while obtaining the path to a standard library.
315    StdlibPathError {
316        /// The error that occurred.
317        error: &'a str,
318    },
319
320    /// A provided extra path did not exist.
321    ExtraPathMissing {
322        /// The path that was missing.
323        path: &'a Utf8Path,
324
325        /// Whether the reporter should produce a warning about this.
326        warn: bool,
327    },
328
329    /// For an extra include, a directory was specified at depth 0.
330    DirectoryAtDepthZero {
331        /// The directory that was at depth 0.
332        path: &'a Utf8Path,
333    },
334
335    /// While performing the archive, the recursion depth was exceeded.
336    RecursionDepthExceeded {
337        /// The current step in the archive process.
338        step: ArchiveStep,
339
340        /// The path that exceeded the recursion depth.
341        path: &'a Utf8Path,
342
343        /// The recursion depth limit that was hit.
344        limit: usize,
345
346        /// Whether the reporter should produce a warning about this.
347        warn: bool,
348    },
349
350    /// The archive process encountered an unknown file type.
351    UnknownFileType {
352        /// The current step in the archive process.
353        step: ArchiveStep,
354
355        /// The path of the unknown type.
356        path: &'a Utf8Path,
357    },
358
359    /// A crate linked against a non-existent path.
360    LinkedPathNotFound {
361        /// The path of the linked file.
362        path: &'a Utf8Path,
363
364        /// The crates that linked against the path.
365        requested_by: &'a [String],
366    },
367
368    /// The archive operation completed successfully.
369    Archived {
370        /// The number of files archived.
371        file_count: usize,
372
373        /// The archive output file.
374        output_file: &'a Utf8Path,
375
376        /// How long it took to create the archive.
377        elapsed: Duration,
378    },
379
380    /// The extraction process started.
381    ExtractStarted {
382        /// The number of test binaries to extract.
383        test_binary_count: usize,
384
385        /// The number of non-test binaries to extract.
386        non_test_binary_count: usize,
387
388        /// The number of build script output directories to archive.
389        build_script_out_dir_count: usize,
390
391        /// The number of linked paths to extract.
392        linked_path_count: usize,
393
394        /// The destination directory.
395        dest_dir: &'a Utf8Path,
396    },
397
398    /// The extraction process completed successfully.
399    Extracted {
400        /// The number of files extracted.
401        file_count: usize,
402
403        /// The destination directory.
404        dest_dir: &'a Utf8Path,
405
406        /// How long it took to extract the archive.
407        elapsed: Duration,
408    },
409}
410
411/// Counts of various types of files in an archive.
412#[derive(Clone, Copy, Debug)]
413pub struct ArchiveCounts {
414    /// The number of test binaries.
415    pub test_binary_count: usize,
416
417    /// The number of non-test binaries.
418    pub non_test_binary_count: usize,
419
420    /// The number of build script output directories.
421    pub build_script_out_dir_count: usize,
422
423    /// The number of linked paths.
424    pub linked_path_count: usize,
425
426    /// The number of extra paths.
427    pub extra_path_count: usize,
428
429    /// The number of standard libraries.
430    pub stdlib_count: usize,
431}