Skip to main content

nextest_runner/reporter/displayer/
imp.rs

1// Copyright (c) The nextest Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Prints out and aggregates test execution statuses.
5//!
6//! The main structure in this module is [`TestReporter`].
7
8use super::{
9    ChildOutputSpec, FinalStatusLevel, OutputStoreFinal, StatusLevel, StatusLevels,
10    UnitOutputReporter,
11    config::{DisplayConfig, ProgressDisplay},
12    formatters::{
13        DisplayBracketedDuration, DisplayDurationBy, DisplaySlowDuration, DisplayUnitKind,
14        write_final_warnings, write_skip_counts,
15    },
16    progress::{
17        MaxProgressRunning, ProgressBarState, progress_bar_msg, progress_str,
18        terminal_progress_value, write_summary_str,
19    },
20    unit_output::{OutputDisplayOverrides, TestOutputDisplay},
21};
22use crate::{
23    config::{
24        elements::{FlakyResult, LeakTimeoutResult, SlowTimeoutResult},
25        overrides::CompiledDefaultFilter,
26        scripts::ScriptId,
27    },
28    errors::WriteEventError,
29    helpers::{
30        DisplayCounterIndex, DisplayScriptInstance, DisplayTestInstance, DurationRounding,
31        ThemeCharacters, decimal_char_width, plural,
32        progress::{ShowTerminalProgress, TerminalProgress},
33    },
34    indenter::indented,
35    list::TestInstanceId,
36    output_spec::{LiveSpec, RecordingSpec},
37    record::{LoadOutput, OutputEventKind, ReplayHeader, ShortestRunIdPrefix},
38    redact::Redactor,
39    reporter::{events::*, helpers::Styles, imp::ReporterOutput},
40    run_mode::NextestRunMode,
41    runner::StressCount,
42    write_str::WriteStr,
43};
44use debug_ignore::DebugIgnore;
45use nextest_metadata::MismatchReason;
46use owo_colors::OwoColorize;
47use std::{
48    borrow::Cow,
49    cmp::{Ordering, Reverse},
50    io::{self, BufWriter, IsTerminal, Write},
51    time::Duration,
52};
53
54/// The kind of displayer being used.
55#[derive(Copy, Clone, Debug, Eq, PartialEq)]
56pub(crate) enum DisplayerKind {
57    /// The displayer is showing output from a live test run.
58    Live,
59
60    /// The displayer is showing output from a replay of a recorded run.
61    ///
62    /// In replay mode, if output was not captured during the original run, a
63    /// helpful message is displayed to indicate this.
64    Replay,
65}
66
67pub(crate) struct DisplayReporterBuilder {
68    pub(crate) mode: NextestRunMode,
69    pub(crate) default_filter: CompiledDefaultFilter,
70    pub(crate) display_config: DisplayConfig,
71    pub(crate) run_count: usize,
72    pub(crate) success_output: Option<TestOutputDisplay>,
73    pub(crate) failure_output: Option<TestOutputDisplay>,
74    pub(crate) should_colorize: bool,
75    pub(crate) verbose: bool,
76    pub(crate) no_output_indent: bool,
77    pub(crate) max_progress_running: MaxProgressRunning,
78    pub(crate) show_term_progress: ShowTerminalProgress,
79    pub(crate) displayer_kind: DisplayerKind,
80    pub(crate) redactor: Redactor,
81}
82
83impl DisplayReporterBuilder {
84    pub(crate) fn build<'a>(self, output: ReporterOutput<'a>) -> DisplayReporter<'a> {
85        let mut styles: Box<Styles> = Box::default();
86        if self.should_colorize {
87            styles.colorize();
88        }
89
90        let theme_characters = match &output {
91            ReporterOutput::Terminal => ThemeCharacters::detect(supports_unicode::Stream::Stderr),
92            ReporterOutput::Writer { use_unicode, .. } => {
93                let mut tc = ThemeCharacters::default();
94                if *use_unicode {
95                    tc.use_unicode();
96                }
97                tc
98            }
99        };
100
101        let is_terminal = matches!(&output, ReporterOutput::Terminal) && io::stderr().is_terminal();
102        let is_ci = is_ci::uncached();
103
104        let resolved = self.display_config.resolve(is_terminal, is_ci);
105
106        let output = match output {
107            ReporterOutput::Terminal => {
108                let progress_bar = if resolved.progress_display == ProgressDisplay::Bar {
109                    Some(ProgressBarState::new(
110                        self.mode,
111                        self.run_count,
112                        theme_characters.progress_chars(),
113                        self.max_progress_running,
114                    ))
115                } else {
116                    None
117                };
118                let term_progress = TerminalProgress::new(self.show_term_progress);
119                ReporterOutputImpl::Terminal {
120                    progress_bar: progress_bar.map(Box::new),
121                    term_progress,
122                }
123            }
124            ReporterOutput::Writer { writer, .. } => ReporterOutputImpl::Writer(writer),
125        };
126
127        let no_capture = self.display_config.no_capture;
128
129        // success_output is meaningless if the runner isn't capturing any
130        // output. However, failure output is still meaningful for exec fail
131        // events.
132        let overrides = OutputDisplayOverrides {
133            force_success_output: match no_capture {
134                true => Some(TestOutputDisplay::Never),
135                false => self.success_output,
136            },
137            force_failure_output: match no_capture {
138                true => Some(TestOutputDisplay::Never),
139                false => self.failure_output,
140            },
141            force_exec_fail_output: match no_capture {
142                true => Some(TestOutputDisplay::Immediate),
143                false => self.failure_output,
144            },
145        };
146
147        let counter_width = matches!(resolved.progress_display, ProgressDisplay::Counter)
148            .then_some(decimal_char_width(self.run_count));
149
150        DisplayReporter {
151            inner: DisplayReporterImpl {
152                mode: self.mode,
153                default_filter: self.default_filter,
154                status_levels: resolved.status_levels,
155                no_capture,
156                verbose: self.verbose,
157                no_output_indent: self.no_output_indent,
158                counter_width,
159                styles,
160                theme_characters,
161                cancel_status: None,
162                unit_output: UnitOutputReporter::new(overrides, self.displayer_kind),
163                final_outputs: DebugIgnore(Vec::new()),
164                run_id_unique_prefix: None,
165                redactor: self.redactor,
166            },
167            output,
168        }
169    }
170}
171
172/// Functionality to report test results to stderr, JUnit, and/or structured,
173/// machine-readable results to stdout.
174pub(crate) struct DisplayReporter<'a> {
175    inner: DisplayReporterImpl<'a>,
176    output: ReporterOutputImpl<'a>,
177}
178
179impl<'a> DisplayReporter<'a> {
180    pub(crate) fn tick(&mut self) {
181        self.output.tick(&self.inner.styles);
182    }
183
184    pub(crate) fn write_event(&mut self, event: &TestEvent<'a>) -> Result<(), WriteEventError> {
185        match &mut self.output {
186            ReporterOutputImpl::Terminal {
187                progress_bar,
188                term_progress,
189            } => {
190                if let Some(term_progress) = term_progress {
191                    term_progress.set(terminal_progress_value(event));
192                }
193
194                if let Some(state) = progress_bar {
195                    // Write to a string that will be printed as a log line.
196                    let mut buf = String::new();
197                    self.inner
198                        .write_event_impl(event, &mut buf)
199                        .map_err(WriteEventError::Io)?;
200
201                    state.update_progress_bar(event, &self.inner.styles);
202                    state.write_buf(&buf);
203                    Ok(())
204                } else {
205                    // Write to a buffered stderr.
206                    let mut writer = BufWriter::new(std::io::stderr());
207                    self.inner
208                        .write_event_impl(event, &mut writer)
209                        .map_err(WriteEventError::Io)?;
210                    writer.flush().map_err(WriteEventError::Io)
211                }
212            }
213            ReporterOutputImpl::Writer(writer) => {
214                self.inner
215                    .write_event_impl(event, *writer)
216                    .map_err(WriteEventError::Io)?;
217                writer.write_str_flush().map_err(WriteEventError::Io)
218            }
219        }
220    }
221
222    pub(crate) fn finish(&mut self) {
223        self.output.finish_and_clear_bar();
224    }
225
226    /// Sets the unique prefix for the run ID.
227    ///
228    /// This is used to highlight the unique prefix portion of the run ID
229    /// in the `RunStarted` output when a recording session is active.
230    pub(crate) fn set_run_id_unique_prefix(&mut self, prefix: ShortestRunIdPrefix) {
231        self.inner.run_id_unique_prefix = Some(prefix);
232    }
233
234    /// Writes a replay header to the output.
235    ///
236    /// This is used by `ReplayReporter` to display replay-specific information
237    /// before processing recorded events.
238    pub(crate) fn write_replay_header(
239        &mut self,
240        header: &ReplayHeader,
241    ) -> Result<(), WriteEventError> {
242        self.write_impl(|writer, styles, _theme_chars| {
243            // Write "Replaying" line with unique prefix highlighting.
244            write!(writer, "{:>12} ", "Replaying".style(styles.pass))?;
245            let run_id_display = if let Some(prefix_info) = &header.unique_prefix {
246                // Highlight the unique prefix portion of the full run ID.
247                format!(
248                    "{}{}",
249                    prefix_info.prefix.style(styles.run_id_prefix),
250                    prefix_info.rest.style(styles.run_id_rest),
251                )
252            } else {
253                // No prefix info available, show the full ID without highlighting.
254                header.run_id.to_string().style(styles.count).to_string()
255            };
256            writeln!(writer, "recorded run {}", run_id_display)?;
257
258            // Write "Started" line with status.
259            let status_str = header.status.short_status_str();
260            write!(writer, "{:>12} ", "Started".style(styles.pass))?;
261            writeln!(
262                writer,
263                "{}  status: {}",
264                header.started_at.format("%Y-%m-%d %H:%M:%S"),
265                status_str.style(styles.count)
266            )?;
267
268            Ok(())
269        })
270    }
271
272    /// Returns an [`OutputLoadDecider`] for this reporter.
273    ///
274    /// The decider examines event metadata and the reporter's display
275    /// configuration to decide whether output should be loaded from the
276    /// archive during replay.
277    pub(crate) fn output_load_decider(&self) -> OutputLoadDecider {
278        OutputLoadDecider {
279            status_level: self.inner.status_levels.status_level,
280            overrides: self.inner.unit_output.overrides(),
281        }
282    }
283
284    /// Internal helper for writing through the output with access to styles.
285    fn write_impl<F>(&mut self, f: F) -> Result<(), WriteEventError>
286    where
287        F: FnOnce(&mut dyn WriteStr, &Styles, &ThemeCharacters) -> io::Result<()>,
288    {
289        match &mut self.output {
290            ReporterOutputImpl::Terminal { progress_bar, .. } => {
291                if let Some(state) = progress_bar {
292                    // Write to a string that will be printed as a log line.
293                    let mut buf = String::new();
294                    f(&mut buf, &self.inner.styles, &self.inner.theme_characters)
295                        .map_err(WriteEventError::Io)?;
296                    state.write_buf(&buf);
297                    Ok(())
298                } else {
299                    // Write to a buffered stderr.
300                    let mut writer = BufWriter::new(std::io::stderr());
301                    f(
302                        &mut writer,
303                        &self.inner.styles,
304                        &self.inner.theme_characters,
305                    )
306                    .map_err(WriteEventError::Io)?;
307                    writer.flush().map_err(WriteEventError::Io)
308                }
309            }
310            ReporterOutputImpl::Writer(writer) => {
311                f(*writer, &self.inner.styles, &self.inner.theme_characters)
312                    .map_err(WriteEventError::Io)?;
313                writer.write_str_flush().map_err(WriteEventError::Io)
314            }
315        }
316    }
317}
318
319/// Configuration needed to decide whether to load output during replay.
320///
321/// This captures the reporter's display configuration so the replay loop can
322/// skip decompressing output from the archive when it will never be shown. The
323/// decision is conservative: `LoadOutput::Load` is returned whenever there is
324/// any chance the output will be displayed, either immediately or at the end of
325/// the run.
326///
327/// Currently, replays only use a display reporter, and do not use JUnit or
328/// libtest reporters. If and when support for those is added to replay, this
329/// decider must be updated to account for their output requirements as well.
330#[derive(Debug)]
331pub struct OutputLoadDecider {
332    pub(super) status_level: StatusLevel,
333    pub(super) overrides: OutputDisplayOverrides,
334}
335
336impl OutputLoadDecider {
337    /// Decides whether output should be loaded for a given output event.
338    pub fn should_load_output(&self, kind: &OutputEventKind<RecordingSpec>) -> LoadOutput {
339        match kind {
340            OutputEventKind::SetupScriptFinished { run_status, .. } => {
341                Self::should_load_for_setup_script(&run_status.result)
342            }
343            OutputEventKind::TestAttemptFailedWillRetry { failure_output, .. } => {
344                let display = self.overrides.failure_output(*failure_output);
345                Self::should_load_for_retry(display, self.status_level)
346            }
347            OutputEventKind::TestFinished {
348                success_output,
349                failure_output,
350                run_statuses,
351                ..
352            } => self.should_load_for_test_finished(*success_output, *failure_output, run_statuses),
353        }
354    }
355
356    pub(super) fn should_load_for_test_finished(
357        &self,
358        success_output: TestOutputDisplay,
359        failure_output: TestOutputDisplay,
360        run_statuses: &ExecutionStatuses<RecordingSpec>,
361    ) -> LoadOutput {
362        let describe = run_statuses.describe();
363
364        let display =
365            self.overrides
366                .resolve_for_describe(success_output, failure_output, &describe);
367
368        Self::should_load_for_display(display)
369    }
370
371    /// Core decision logic for whether to load output for a setup script.
372    ///
373    /// The displayer always shows output for failing setup scripts and
374    /// never for successful ones.
375    ///
376    /// This method is factored out for testing.
377    pub(super) fn should_load_for_setup_script(result: &ExecutionResultDescription) -> LoadOutput {
378        // The displayer always shows output for failing setup scripts.
379        if result.is_success() {
380            LoadOutput::Skip
381        } else {
382            LoadOutput::Load
383        }
384    }
385
386    /// Core decision logic for whether to load output for a retry attempt.
387    ///
388    /// The displayer shows retry output iff `status_level >= Retry` and
389    /// the resolved failure output is immediate.
390    ///
391    /// This method is factored out for testing.
392    pub(super) fn should_load_for_retry(
393        display: TestOutputDisplay,
394        status_level: StatusLevel,
395    ) -> LoadOutput {
396        if display.is_immediate() && status_level >= StatusLevel::Retry {
397            LoadOutput::Load
398        } else {
399            LoadOutput::Skip
400        }
401    }
402
403    /// Core decision logic for whether to load output for a finished test.
404    ///
405    /// This method is factored out for testing.
406    pub(super) fn should_load_for_display(display: TestOutputDisplay) -> LoadOutput {
407        let is_immediate = display.is_immediate();
408        let is_final = display.is_final();
409
410        // We ignore cancel_status because we cannot know it without tracking it
411        // ourselves, and cancellation only hides output, never shows more.
412        // (This is verified by the cancellation_only_hides_output test).
413        if is_immediate || is_final {
414            LoadOutput::Load
415        } else {
416            LoadOutput::Skip
417        }
418    }
419}
420
421enum ReporterOutputImpl<'a> {
422    Terminal {
423        // Reporter-specific progress bar state. None if the progress bar is not
424        // enabled (which can include the terminal not being a TTY).
425        progress_bar: Option<Box<ProgressBarState>>,
426        // OSC 9 code progress reporting.
427        term_progress: Option<TerminalProgress>,
428    },
429    Writer(&'a mut (dyn WriteStr + Send)),
430}
431
432impl ReporterOutputImpl<'_> {
433    fn tick(&mut self, styles: &Styles) {
434        match self {
435            ReporterOutputImpl::Terminal {
436                progress_bar,
437                term_progress,
438            } => {
439                if let Some(state) = progress_bar {
440                    state.tick(styles);
441                }
442                if let Some(term_progress) = term_progress {
443                    // In this case, write the last value directly to stderr.
444                    // This is a very small amount of data so buffering is not
445                    // required. It also doesn't have newlines or any visible
446                    // text, so it can be directly written out to stderr without
447                    // going through the progress bar (which screws up
448                    // indicatif's calculations).
449                    term_progress.emit()
450                }
451            }
452            ReporterOutputImpl::Writer(_) => {
453                // No ticking for writers.
454            }
455        }
456    }
457
458    fn finish_and_clear_bar(&self) {
459        match self {
460            ReporterOutputImpl::Terminal {
461                progress_bar,
462                term_progress,
463            } => {
464                if let Some(state) = progress_bar {
465                    state.finish_and_clear();
466                }
467                if let Some(term_progress) = term_progress {
468                    // The last value is expected to be Remove.
469                    term_progress.emit()
470                }
471            }
472            ReporterOutputImpl::Writer(_) => {
473                // No progress bar to clear.
474            }
475        }
476    }
477
478    #[cfg(test)]
479    fn writer_mut(&mut self) -> Option<&mut (dyn WriteStr + Send)> {
480        match self {
481            Self::Writer(writer) => Some(*writer),
482            _ => None,
483        }
484    }
485}
486
487#[derive(Debug)]
488enum FinalOutput {
489    Skipped(#[expect(dead_code)] MismatchReason),
490    Executed {
491        run_statuses: ExecutionStatuses<LiveSpec>,
492        display_output: bool,
493    },
494}
495
496impl FinalOutput {
497    fn final_status_level(&self) -> FinalStatusLevel {
498        match self {
499            Self::Skipped(_) => FinalStatusLevel::Skip,
500            Self::Executed { run_statuses, .. } => run_statuses.describe().final_status_level(),
501        }
502    }
503}
504
505struct FinalOutputEntry<'a> {
506    stress_index: Option<StressIndex>,
507    counter: TestInstanceCounter,
508    instance: TestInstanceId<'a>,
509    output: FinalOutput,
510}
511
512/// Sort final output entries for display.
513///
514/// The sort key is:
515///
516/// 1. Final status level (reversed, so that failing tests are printed last).
517/// 2. Stress index.
518/// 3. Counter, but only when the counter is displayed.
519/// 4. Test instance.
520///
521/// Note that the counter comparison only matters between entries with the same
522/// status level. In practice, `TestInstanceCounter::Padded` is only used for
523/// `FinalOutput::Skipped` entries (which have `FinalStatusLevel::Skip`), while
524/// `TestInstanceCounter::Counter` is only used for `FinalOutput::Executed`
525/// entries (which never have `FinalStatusLevel::Skip`). So `Padded` and
526/// `Counter` are never compared with each other.
527fn sort_final_outputs(entries: &mut [FinalOutputEntry<'_>], include_counter: bool) {
528    entries.sort_unstable_by(|a, b| {
529        Reverse(a.output.final_status_level())
530            .cmp(&Reverse(b.output.final_status_level()))
531            .then_with(|| a.stress_index.cmp(&b.stress_index))
532            .then_with(|| {
533                if include_counter {
534                    a.counter.cmp(&b.counter)
535                } else {
536                    Ordering::Equal
537                }
538            })
539            .then_with(|| a.instance.cmp(&b.instance))
540    });
541}
542
543struct DisplayReporterImpl<'a> {
544    mode: NextestRunMode,
545    default_filter: CompiledDefaultFilter,
546    status_levels: StatusLevels,
547    no_capture: bool,
548    verbose: bool,
549    no_output_indent: bool,
550    // None if no counter is displayed. If a counter is displayed, this is the
551    // width of the total number of tests to run.
552    counter_width: Option<usize>,
553    styles: Box<Styles>,
554    theme_characters: ThemeCharacters,
555    cancel_status: Option<CancelReason>,
556    unit_output: UnitOutputReporter,
557    final_outputs: DebugIgnore<Vec<FinalOutputEntry<'a>>>,
558    // The unique prefix for the current run ID, if a recording session is active.
559    // Used for highlighting the run ID in RunStarted output.
560    run_id_unique_prefix: Option<ShortestRunIdPrefix>,
561    redactor: Redactor,
562}
563
564impl<'a> DisplayReporterImpl<'a> {
565    fn write_event_impl(
566        &mut self,
567        event: &TestEvent<'a>,
568        writer: &mut dyn WriteStr,
569    ) -> io::Result<()> {
570        match &event.kind {
571            TestEventKind::RunStarted {
572                test_list,
573                run_id,
574                profile_name,
575                cli_args: _,
576                stress_condition: _,
577            } => {
578                writeln!(writer, "{}", self.theme_characters.hbar(12))?;
579                write!(writer, "{:>12} ", "Nextest run".style(self.styles.pass))?;
580
581                // Display the run ID with unique prefix highlighting if a recording
582                // session is active, otherwise use plain styling.
583                let run_id_display = if let Some(prefix_info) = &self.run_id_unique_prefix {
584                    format!(
585                        "{}{}",
586                        prefix_info.prefix.style(self.styles.run_id_prefix),
587                        prefix_info.rest.style(self.styles.run_id_rest),
588                    )
589                } else {
590                    run_id.style(self.styles.count).to_string()
591                };
592
593                writeln!(
594                    writer,
595                    "ID {} with nextest profile: {}",
596                    run_id_display,
597                    profile_name.style(self.styles.count),
598                )?;
599
600                write!(writer, "{:>12} ", "Starting".style(self.styles.pass))?;
601
602                let count_style = self.styles.count;
603
604                let tests_str = plural::tests_str(self.mode, test_list.run_count());
605                let binaries_str = plural::binaries_str(test_list.listed_binary_count());
606
607                write!(
608                    writer,
609                    "{} {tests_str} across {} {binaries_str}",
610                    test_list.run_count().style(count_style),
611                    test_list.listed_binary_count().style(count_style),
612                )?;
613
614                write_skip_counts(
615                    self.mode,
616                    test_list.skip_counts(),
617                    &self.default_filter,
618                    &self.styles,
619                    writer,
620                )?;
621
622                writeln!(writer)?;
623            }
624            TestEventKind::StressSubRunStarted { progress } => {
625                write!(
626                    writer,
627                    "{}\n{:>12} ",
628                    self.theme_characters.hbar(12),
629                    "Stress test".style(self.styles.pass)
630                )?;
631
632                match progress {
633                    StressProgress::Count {
634                        total: StressCount::Count { count },
635                        elapsed,
636                        completed,
637                    } => {
638                        write!(
639                            writer,
640                            "iteration {}/{} ({} elapsed so far",
641                            (completed + 1).style(self.styles.count),
642                            count.style(self.styles.count),
643                            self.redactor
644                                .redact_hhmmss_duration(*elapsed, DurationRounding::Floor)
645                                .style(self.styles.count),
646                        )?;
647                    }
648                    StressProgress::Count {
649                        total: StressCount::Infinite,
650                        elapsed,
651                        completed,
652                    } => {
653                        write!(
654                            writer,
655                            "iteration {} ({} elapsed so far",
656                            (completed + 1).style(self.styles.count),
657                            self.redactor
658                                .redact_hhmmss_duration(*elapsed, DurationRounding::Floor)
659                                .style(self.styles.count),
660                        )?;
661                    }
662                    StressProgress::Time {
663                        total,
664                        elapsed,
665                        completed,
666                    } => {
667                        write!(
668                            writer,
669                            "iteration {} ({}/{} elapsed so far",
670                            (completed + 1).style(self.styles.count),
671                            self.redactor
672                                .redact_hhmmss_duration(*elapsed, DurationRounding::Floor)
673                                .style(self.styles.count),
674                            self.redactor
675                                .redact_hhmmss_duration(*total, DurationRounding::Floor)
676                                .style(self.styles.count),
677                        )?;
678                    }
679                }
680
681                if let Some(remaining) = progress.remaining() {
682                    match remaining {
683                        StressRemaining::Count(n) => {
684                            write!(
685                                writer,
686                                ", {} {} remaining",
687                                n.style(self.styles.count),
688                                plural::iterations_str(n.get()),
689                            )?;
690                        }
691                        StressRemaining::Infinite => {
692                            // There isn't anything to display here.
693                        }
694                        StressRemaining::Time(t) => {
695                            // Display the remaining time as a ceiling so that
696                            // we show something like:
697                            //
698                            // 00:02:05/00:30:00 elapsed so far, 00:27:55 remaining
699                            //
700                            // rather than
701                            //
702                            // 00:02:05/00:30:00 elapsed so far, 00:27:54 remaining
703                            write!(
704                                writer,
705                                ", {} remaining",
706                                self.redactor
707                                    .redact_hhmmss_duration(t, DurationRounding::Ceiling)
708                                    .style(self.styles.count)
709                            )?;
710                        }
711                    }
712                }
713
714                writeln!(writer, ")")?;
715            }
716            TestEventKind::SetupScriptStarted {
717                stress_index,
718                index,
719                total,
720                script_id,
721                program,
722                args,
723                ..
724            } => {
725                writeln!(
726                    writer,
727                    "{:>12} [{:>9}] {}",
728                    "SETUP".style(self.styles.pass),
729                    // index + 1 so that it displays as e.g. "1/2" and "2/2".
730                    format!("{}/{}", index + 1, total),
731                    self.display_script_instance(*stress_index, script_id.clone(), program, args)
732                )?;
733            }
734            TestEventKind::SetupScriptSlow {
735                stress_index,
736                script_id,
737                program,
738                args,
739                elapsed,
740                will_terminate,
741            } => {
742                if !*will_terminate && self.status_levels.status_level >= StatusLevel::Slow {
743                    write!(writer, "{:>12} ", "SETUP SLOW".style(self.styles.skip))?;
744                    writeln!(
745                        writer,
746                        "{}{}",
747                        DisplaySlowDuration(*elapsed),
748                        self.display_script_instance(
749                            *stress_index,
750                            script_id.clone(),
751                            program,
752                            args
753                        )
754                    )?;
755                } else if *will_terminate && self.status_levels.status_level >= StatusLevel::Fail {
756                    write!(writer, "{:>12} ", "TERMINATING".style(self.styles.fail))?;
757                    writeln!(
758                        writer,
759                        "{}{}",
760                        DisplaySlowDuration(*elapsed),
761                        self.display_script_instance(
762                            *stress_index,
763                            script_id.clone(),
764                            program,
765                            args
766                        )
767                    )?;
768                }
769                // If neither condition is met (!will_terminate with
770                // status_level < Slow, or will_terminate with status_level
771                // < Fail), nothing is printed.
772            }
773            TestEventKind::SetupScriptFinished {
774                stress_index,
775                script_id,
776                program,
777                args,
778                run_status,
779                ..
780            } => {
781                self.write_setup_script_status_line(
782                    *stress_index,
783                    script_id,
784                    program,
785                    args,
786                    run_status,
787                    writer,
788                )?;
789                // Always display failing setup script output if it exists. We
790                // may change this in the future.
791                if !run_status.result.is_success() {
792                    self.write_setup_script_execute_status(run_status, writer)?;
793                }
794            }
795            TestEventKind::TestStarted {
796                stress_index,
797                test_instance,
798                current_stats,
799                command_line,
800                ..
801            } => {
802                // In no-capture and verbose modes, print out a test start
803                // event.
804                if self.no_capture || self.verbose {
805                    // The spacing is to align test instances.
806                    writeln!(
807                        writer,
808                        "{:>12} [         ] {}",
809                        "START".style(self.styles.pass),
810                        self.display_test_instance(
811                            *stress_index,
812                            TestInstanceCounter::Counter {
813                                // --no-capture implies tests being run
814                                // serially, so the current test is the number
815                                // of finished tests plus one.
816                                current: current_stats.finished_count + 1,
817                                total: current_stats.initial_run_count,
818                            },
819                            *test_instance
820                        ),
821                    )?;
822                }
823
824                if self.verbose {
825                    self.write_command_line(command_line, writer)?;
826                }
827            }
828            TestEventKind::TestSlow {
829                stress_index,
830                test_instance,
831                retry_data,
832                elapsed,
833                will_terminate,
834            } => {
835                if !*will_terminate && self.status_levels.status_level >= StatusLevel::Slow {
836                    // Don't show TRY N SLOW for the first attempt -- it isn't
837                    // very relevant in the common case that the test passes
838                    // later.
839                    if retry_data.attempt > 1 {
840                        write!(
841                            writer,
842                            "{:>12} ",
843                            format!("TRY {} SLOW", retry_data.attempt).style(self.styles.skip)
844                        )?;
845                    } else {
846                        write!(writer, "{:>12} ", "SLOW".style(self.styles.skip))?;
847                    }
848                    writeln!(
849                        writer,
850                        "{}{}",
851                        DisplaySlowDuration(*elapsed),
852                        self.display_test_instance(
853                            *stress_index,
854                            TestInstanceCounter::Padded,
855                            *test_instance
856                        )
857                    )?;
858                } else if *will_terminate {
859                    let (required_status_level, style) = if retry_data.is_last_attempt() {
860                        (StatusLevel::Fail, self.styles.fail)
861                    } else {
862                        (StatusLevel::Retry, self.styles.retry)
863                    };
864                    if self.status_levels.status_level >= required_status_level {
865                        // *Do* show TRY N TRMNTG for the first attempt, since
866                        // we will retry the test later.
867                        if retry_data.total_attempts > 1
868                            && self.status_levels.status_level > required_status_level
869                        {
870                            write!(
871                                writer,
872                                "{:>12} ",
873                                format!("TRY {} TRMNTG", retry_data.attempt).style(style)
874                            )?;
875                        } else {
876                            write!(writer, "{:>12} ", "TERMINATING".style(style))?;
877                        };
878                        writeln!(
879                            writer,
880                            "{}{}",
881                            DisplaySlowDuration(*elapsed),
882                            self.display_test_instance(
883                                *stress_index,
884                                TestInstanceCounter::Padded,
885                                *test_instance
886                            )
887                        )?;
888                    }
889                }
890                // If neither condition is met (!will_terminate with
891                // status_level < Slow, or will_terminate with status_level
892                // below the required level), nothing is printed.
893            }
894
895            TestEventKind::TestAttemptFailedWillRetry {
896                stress_index,
897                test_instance,
898                run_status,
899                delay_before_next_attempt,
900                failure_output,
901                running: _,
902            } => {
903                if self.status_levels.status_level >= StatusLevel::Retry {
904                    let try_status_string = format!(
905                        "TRY {} {}",
906                        run_status.retry_data.attempt,
907                        short_status_str(&run_status.result),
908                    );
909
910                    // Print the try status and time taken.
911                    write!(
912                        writer,
913                        "{:>12} {}",
914                        try_status_string.style(self.styles.retry),
915                        DisplayBracketedDuration(run_status.time_taken),
916                    )?;
917
918                    // Print the name of the test.
919                    writeln!(
920                        writer,
921                        "{}",
922                        self.display_test_instance(
923                            *stress_index,
924                            TestInstanceCounter::Padded,
925                            *test_instance
926                        )
927                    )?;
928
929                    // This test is guaranteed to have failed.
930                    assert!(
931                        !run_status.result.is_success(),
932                        "only failing tests are retried"
933                    );
934                    if self
935                        .unit_output
936                        .overrides()
937                        .failure_output(*failure_output)
938                        .is_immediate()
939                    {
940                        self.write_test_execute_status(run_status, true, writer)?;
941                    }
942
943                    // The final output doesn't show retries, so don't store this result in
944                    // final_outputs.
945
946                    if !delay_before_next_attempt.is_zero() {
947                        // Print a "DELAY {}/{}" line.
948                        let delay_string = format!(
949                            "DELAY {}/{}",
950                            run_status.retry_data.attempt + 1,
951                            run_status.retry_data.total_attempts,
952                        );
953                        write!(
954                            writer,
955                            "{:>12} {}",
956                            delay_string.style(self.styles.retry),
957                            DisplayDurationBy(*delay_before_next_attempt)
958                        )?;
959
960                        // Print the name of the test.
961                        writeln!(
962                            writer,
963                            "{}",
964                            self.display_test_instance(
965                                *stress_index,
966                                TestInstanceCounter::Padded,
967                                *test_instance
968                            )
969                        )?;
970                    }
971                }
972            }
973            TestEventKind::TestRetryStarted {
974                stress_index,
975                test_instance,
976                slot_assignment: _,
977                retry_data: RetryData { attempt, .. },
978                running: _,
979                command_line,
980            } => {
981                // In no-capture and verbose modes, print out a retry start event.
982                if self.no_capture || self.verbose {
983                    let retry_string = format!("TRY {attempt} START");
984                    writeln!(
985                        writer,
986                        "{:>12} [         ] {}",
987                        retry_string.style(self.styles.retry),
988                        self.display_test_instance(
989                            *stress_index,
990                            TestInstanceCounter::Padded,
991                            *test_instance
992                        )
993                    )?;
994                }
995
996                if self.verbose {
997                    self.write_command_line(command_line, writer)?;
998                }
999            }
1000            TestEventKind::TestFinished {
1001                stress_index,
1002                test_instance,
1003                success_output,
1004                failure_output,
1005                run_statuses,
1006                current_stats,
1007                ..
1008            } => {
1009                let describe = run_statuses.describe();
1010                let last_status = run_statuses.last_status();
1011                let test_output_display = self.unit_output.overrides().resolve_for_describe(
1012                    *success_output,
1013                    *failure_output,
1014                    &describe,
1015                );
1016
1017                let output_on_test_finished = self.status_levels.compute_output_on_test_finished(
1018                    test_output_display,
1019                    self.cancel_status,
1020                    describe.status_level(),
1021                    describe.final_status_level(),
1022                    &last_status.result,
1023                );
1024
1025                let counter = TestInstanceCounter::Counter {
1026                    current: current_stats.finished_count,
1027                    total: current_stats.initial_run_count,
1028                };
1029
1030                if output_on_test_finished.write_status_line {
1031                    self.write_status_line(
1032                        *stress_index,
1033                        counter,
1034                        *test_instance,
1035                        describe,
1036                        writer,
1037                    )?;
1038                }
1039                if output_on_test_finished.show_immediate {
1040                    self.write_test_execute_status(last_status, false, writer)?;
1041                }
1042                if let OutputStoreFinal::Yes { display_output } =
1043                    output_on_test_finished.store_final
1044                {
1045                    self.final_outputs.push(FinalOutputEntry {
1046                        stress_index: *stress_index,
1047                        counter,
1048                        instance: *test_instance,
1049                        output: FinalOutput::Executed {
1050                            run_statuses: run_statuses.clone(),
1051                            display_output,
1052                        },
1053                    });
1054                }
1055            }
1056            TestEventKind::TestSkipped {
1057                stress_index,
1058                test_instance,
1059                reason,
1060                ..
1061            } => {
1062                if self.status_levels.status_level >= StatusLevel::Skip {
1063                    self.write_skip_line(*stress_index, *test_instance, writer)?;
1064                }
1065                if self.status_levels.final_status_level >= FinalStatusLevel::Skip {
1066                    self.final_outputs.push(FinalOutputEntry {
1067                        stress_index: *stress_index,
1068                        counter: TestInstanceCounter::Padded,
1069                        instance: *test_instance,
1070                        output: FinalOutput::Skipped(*reason),
1071                    });
1072                }
1073            }
1074            TestEventKind::RunBeginCancel {
1075                setup_scripts_running,
1076                current_stats,
1077                running,
1078            } => {
1079                self.cancel_status = self.cancel_status.max(current_stats.cancel_reason);
1080
1081                write!(writer, "{:>12} ", "Cancelling".style(self.styles.fail))?;
1082                if let Some(reason) = current_stats.cancel_reason {
1083                    write!(
1084                        writer,
1085                        "due to {}: ",
1086                        reason.to_static_str().style(self.styles.fail)
1087                    )?;
1088                }
1089
1090                let immediately_terminating_text =
1091                    if current_stats.cancel_reason == Some(CancelReason::TestFailureImmediate) {
1092                        format!("immediately {} ", "terminating".style(self.styles.fail))
1093                    } else {
1094                        String::new()
1095                    };
1096
1097                // At the moment, we can have either setup scripts or tests running, but not both.
1098                if *setup_scripts_running > 0 {
1099                    let s = plural::setup_scripts_str(*setup_scripts_running);
1100                    write!(
1101                        writer,
1102                        "{immediately_terminating_text}{} {s} still running",
1103                        setup_scripts_running.style(self.styles.count),
1104                    )?;
1105                } else if *running > 0 {
1106                    let tests_str = plural::tests_str(self.mode, *running);
1107                    write!(
1108                        writer,
1109                        "{immediately_terminating_text}{} {tests_str} still running",
1110                        running.style(self.styles.count),
1111                    )?;
1112                }
1113                writeln!(writer)?;
1114            }
1115            TestEventKind::RunBeginKill {
1116                setup_scripts_running,
1117                current_stats,
1118                running,
1119            } => {
1120                self.cancel_status = self.cancel_status.max(current_stats.cancel_reason);
1121
1122                write!(writer, "{:>12} ", "Killing".style(self.styles.fail),)?;
1123                if let Some(reason) = current_stats.cancel_reason {
1124                    write!(
1125                        writer,
1126                        "due to {}: ",
1127                        reason.to_static_str().style(self.styles.fail)
1128                    )?;
1129                }
1130
1131                // At the moment, we can have either setup scripts or tests running, but not both.
1132                if *setup_scripts_running > 0 {
1133                    let s = plural::setup_scripts_str(*setup_scripts_running);
1134                    write!(
1135                        writer,
1136                        ": {} {s} still running",
1137                        setup_scripts_running.style(self.styles.count),
1138                    )?;
1139                } else if *running > 0 {
1140                    let tests_str = plural::tests_str(self.mode, *running);
1141                    write!(
1142                        writer,
1143                        ": {} {tests_str} still running",
1144                        running.style(self.styles.count),
1145                    )?;
1146                }
1147                writeln!(writer)?;
1148            }
1149            TestEventKind::RunPaused {
1150                setup_scripts_running,
1151                running,
1152            } => {
1153                write!(
1154                    writer,
1155                    "{:>12} due to {}",
1156                    "Pausing".style(self.styles.pass),
1157                    "signal".style(self.styles.count)
1158                )?;
1159
1160                // At the moment, we can have either setup scripts or tests running, but not both.
1161                if *setup_scripts_running > 0 {
1162                    let s = plural::setup_scripts_str(*setup_scripts_running);
1163                    write!(
1164                        writer,
1165                        ": {} {s} running",
1166                        setup_scripts_running.style(self.styles.count),
1167                    )?;
1168                } else if *running > 0 {
1169                    let tests_str = plural::tests_str(self.mode, *running);
1170                    write!(
1171                        writer,
1172                        ": {} {tests_str} running",
1173                        running.style(self.styles.count),
1174                    )?;
1175                }
1176                writeln!(writer)?;
1177            }
1178            TestEventKind::RunContinued {
1179                setup_scripts_running,
1180                running,
1181            } => {
1182                write!(
1183                    writer,
1184                    "{:>12} due to {}",
1185                    "Continuing".style(self.styles.pass),
1186                    "signal".style(self.styles.count)
1187                )?;
1188
1189                // At the moment, we can have either setup scripts or tests running, but not both.
1190                if *setup_scripts_running > 0 {
1191                    let s = plural::setup_scripts_str(*setup_scripts_running);
1192                    write!(
1193                        writer,
1194                        ": {} {s} running",
1195                        setup_scripts_running.style(self.styles.count),
1196                    )?;
1197                } else if *running > 0 {
1198                    let tests_str = plural::tests_str(self.mode, *running);
1199                    write!(
1200                        writer,
1201                        ": {} {tests_str} running",
1202                        running.style(self.styles.count),
1203                    )?;
1204                }
1205                writeln!(writer)?;
1206            }
1207            TestEventKind::InfoStarted { total, run_stats } => {
1208                let info_style = if run_stats.has_failures() {
1209                    self.styles.fail
1210                } else {
1211                    self.styles.pass
1212                };
1213
1214                let hbar = self.theme_characters.hbar(12);
1215
1216                write!(writer, "{hbar}\n{}: ", "info".style(info_style))?;
1217
1218                // TODO: display setup_scripts_running as well
1219                writeln!(
1220                    writer,
1221                    "{} in {:.3?}s",
1222                    // Using "total" here for the number of running units is a
1223                    // slight fudge, but it prevents situations where (due to
1224                    // races with unit tasks exiting) the numbers don't exactly
1225                    // match up. It's also not dishonest -- there really are
1226                    // these many units currently running.
1227                    progress_bar_msg(run_stats, *total, &self.styles),
1228                    event.elapsed.as_secs_f64(),
1229                )?;
1230            }
1231            TestEventKind::InfoResponse {
1232                index,
1233                total,
1234                response,
1235            } => {
1236                self.write_info_response(*index, *total, response, writer)?;
1237            }
1238            TestEventKind::InfoFinished { missing } => {
1239                let hbar = self.theme_characters.hbar(12);
1240
1241                if *missing > 0 {
1242                    // This should ordinarily not happen, but it's possible if
1243                    // some of the unit futures are slow to respond.
1244                    writeln!(
1245                        writer,
1246                        "{}: missing {} responses",
1247                        "info".style(self.styles.skip),
1248                        missing.style(self.styles.count)
1249                    )?;
1250                }
1251
1252                writeln!(writer, "{hbar}")?;
1253            }
1254            TestEventKind::InputEnter {
1255                current_stats,
1256                running,
1257            } => {
1258                // Print everything that would be shown in the progress bar,
1259                // except for the bar itself.
1260                writeln!(
1261                    writer,
1262                    "{}",
1263                    progress_str(event.elapsed, current_stats, *running, &self.styles)
1264                )?;
1265            }
1266            TestEventKind::StressSubRunFinished {
1267                progress,
1268                sub_elapsed,
1269                sub_stats,
1270            } => {
1271                let stats_summary = sub_stats.summarize_final();
1272                let summary_style = match stats_summary {
1273                    FinalRunStats::Success => self.styles.pass,
1274                    FinalRunStats::NoTestsRun => self.styles.skip,
1275                    FinalRunStats::Failed { .. } | FinalRunStats::Cancelled { .. } => {
1276                        self.styles.fail
1277                    }
1278                };
1279
1280                write!(
1281                    writer,
1282                    "{:>12} {}",
1283                    "Stress test".style(summary_style),
1284                    DisplayBracketedDuration(*sub_elapsed),
1285                )?;
1286                match progress {
1287                    StressProgress::Count {
1288                        total: StressCount::Count { count },
1289                        elapsed: _,
1290                        completed,
1291                    } => {
1292                        write!(
1293                            writer,
1294                            "iteration {}/{}: ",
1295                            // We do not add +1 to completed here because it
1296                            // represents the number of stress runs actually
1297                            // completed.
1298                            completed.style(self.styles.count),
1299                            count.style(self.styles.count),
1300                        )?;
1301                    }
1302                    StressProgress::Count {
1303                        total: StressCount::Infinite,
1304                        elapsed: _,
1305                        completed,
1306                    } => {
1307                        write!(
1308                            writer,
1309                            "iteration {}: ",
1310                            // We do not add +1 to completed here because it
1311                            // represents the number of stress runs actually
1312                            // completed.
1313                            completed.style(self.styles.count),
1314                        )?;
1315                    }
1316                    StressProgress::Time {
1317                        total: _,
1318                        elapsed: _,
1319                        completed,
1320                    } => {
1321                        write!(
1322                            writer,
1323                            "iteration {}: ",
1324                            // We do not add +1 to completed here because it
1325                            // represents the number of stress runs actually
1326                            // completed.
1327                            completed.style(self.styles.count),
1328                        )?;
1329                    }
1330                }
1331
1332                write!(
1333                    writer,
1334                    "{}",
1335                    sub_stats.finished_count.style(self.styles.count)
1336                )?;
1337                if sub_stats.finished_count != sub_stats.initial_run_count {
1338                    write!(
1339                        writer,
1340                        "/{}",
1341                        sub_stats.initial_run_count.style(self.styles.count)
1342                    )?;
1343                }
1344
1345                // Both initial and finished counts must be 1 for the singular form.
1346                let tests_str = plural::tests_plural_if(
1347                    self.mode,
1348                    sub_stats.initial_run_count != 1 || sub_stats.finished_count != 1,
1349                );
1350
1351                let mut summary_str = String::new();
1352                write_summary_str(sub_stats, &self.styles, &mut summary_str);
1353                writeln!(writer, " {tests_str} run: {summary_str}")?;
1354            }
1355            TestEventKind::RunFinished {
1356                start_time: _start_time,
1357                elapsed,
1358                run_stats,
1359                outstanding_not_seen: tests_not_seen,
1360                ..
1361            } => {
1362                match run_stats {
1363                    RunFinishedStats::Single(run_stats) => {
1364                        let stats_summary = run_stats.summarize_final();
1365                        let summary_style = match stats_summary {
1366                            FinalRunStats::Success => self.styles.pass,
1367                            FinalRunStats::NoTestsRun => self.styles.skip,
1368                            FinalRunStats::Failed { .. } | FinalRunStats::Cancelled { .. } => {
1369                                self.styles.fail
1370                            }
1371                        };
1372                        write!(
1373                            writer,
1374                            "{}\n{:>12} ",
1375                            self.theme_characters.hbar(12),
1376                            "Summary".style(summary_style)
1377                        )?;
1378
1379                        // Next, print the total time taken.
1380                        // * > means right-align.
1381                        // * 8 is the number of characters to pad to.
1382                        // * .3 means print two digits after the decimal point.
1383                        write!(writer, "[{:>8.3?}s] ", elapsed.as_secs_f64())?;
1384
1385                        write!(
1386                            writer,
1387                            "{}",
1388                            run_stats.finished_count.style(self.styles.count)
1389                        )?;
1390                        if run_stats.finished_count != run_stats.initial_run_count {
1391                            write!(
1392                                writer,
1393                                "/{}",
1394                                run_stats.initial_run_count.style(self.styles.count)
1395                            )?;
1396                        }
1397
1398                        // Both initial and finished counts must be 1 for the singular form.
1399                        let tests_str = plural::tests_plural_if(
1400                            self.mode,
1401                            run_stats.initial_run_count != 1 || run_stats.finished_count != 1,
1402                        );
1403
1404                        let mut summary_str = String::new();
1405                        write_summary_str(run_stats, &self.styles, &mut summary_str);
1406                        writeln!(writer, " {tests_str} run: {summary_str}")?;
1407                    }
1408                    RunFinishedStats::Stress(stats) => {
1409                        let stats_summary = stats.summarize_final();
1410                        let summary_style = match stats_summary {
1411                            StressFinalRunStats::Success => self.styles.pass,
1412                            StressFinalRunStats::NoTestsRun => self.styles.skip,
1413                            StressFinalRunStats::Cancelled | StressFinalRunStats::Failed => {
1414                                self.styles.fail
1415                            }
1416                        };
1417
1418                        write!(
1419                            writer,
1420                            "{}\n{:>12} ",
1421                            self.theme_characters.hbar(12),
1422                            "Summary".style(summary_style),
1423                        )?;
1424
1425                        // Next, print the total time taken.
1426                        // * > means right-align.
1427                        // * 8 is the number of characters to pad to.
1428                        // * .3 means print two digits after the decimal point.
1429                        write!(writer, "[{:>8.3?}s] ", elapsed.as_secs_f64())?;
1430
1431                        write!(
1432                            writer,
1433                            "{}",
1434                            stats.completed.current.style(self.styles.count),
1435                        )?;
1436                        let iterations_str = if let Some(total) = stats.completed.total {
1437                            write!(writer, "/{}", total.style(self.styles.count))?;
1438                            plural::iterations_str(total.get())
1439                        } else {
1440                            plural::iterations_str(stats.completed.current)
1441                        };
1442                        write!(
1443                            writer,
1444                            " stress run {iterations_str}: {} {}",
1445                            stats.success_count.style(self.styles.count),
1446                            "passed".style(self.styles.pass),
1447                        )?;
1448                        if stats.failed_count > 0 {
1449                            write!(
1450                                writer,
1451                                ", {} {}",
1452                                stats.failed_count.style(self.styles.count),
1453                                "failed".style(self.styles.fail),
1454                            )?;
1455                        }
1456
1457                        match stats.last_final_stats {
1458                            FinalRunStats::Cancelled { reason, kind: _ } => {
1459                                if let Some(reason) = reason {
1460                                    write!(
1461                                        writer,
1462                                        "; cancelled due to {}",
1463                                        reason.to_static_str().style(self.styles.fail),
1464                                    )?;
1465                                }
1466                            }
1467                            FinalRunStats::Failed { .. }
1468                            | FinalRunStats::Success
1469                            | FinalRunStats::NoTestsRun => {}
1470                        }
1471
1472                        writeln!(writer)?;
1473                    }
1474                }
1475
1476                // Don't print out test outputs after Ctrl-C, but *do* print them after SIGTERM or
1477                // SIGHUP since those tend to be automated tasks performing kills.
1478                if self.cancel_status < Some(CancelReason::Interrupt) {
1479                    // Sort the final outputs for a friendlier experience.
1480                    sort_final_outputs(&mut self.final_outputs, self.counter_width.is_some());
1481
1482                    for entry in &*self.final_outputs {
1483                        match &entry.output {
1484                            FinalOutput::Skipped(_) => {
1485                                self.write_skip_line(entry.stress_index, entry.instance, writer)?;
1486                            }
1487                            FinalOutput::Executed {
1488                                run_statuses,
1489                                display_output,
1490                            } => {
1491                                let last_status = run_statuses.last_status();
1492
1493                                self.write_final_status_line(
1494                                    entry.stress_index,
1495                                    entry.counter,
1496                                    entry.instance,
1497                                    run_statuses.describe(),
1498                                    writer,
1499                                )?;
1500                                if *display_output {
1501                                    self.write_test_execute_status(last_status, false, writer)?;
1502                                }
1503                            }
1504                        }
1505                    }
1506                }
1507
1508                if let Some(not_seen) = tests_not_seen
1509                    && not_seen.total_not_seen > 0
1510                {
1511                    writeln!(
1512                        writer,
1513                        "{:>12} {} outstanding {} not seen during this rerun:",
1514                        "Note".style(self.styles.skip),
1515                        not_seen.total_not_seen.style(self.styles.count),
1516                        plural::tests_str(self.mode, not_seen.total_not_seen),
1517                    )?;
1518
1519                    for t in &not_seen.not_seen {
1520                        let display = DisplayTestInstance::new(
1521                            None,
1522                            None,
1523                            t.as_ref(),
1524                            &self.styles.list_styles,
1525                        );
1526                        writeln!(writer, "             {}", display)?;
1527                    }
1528
1529                    let remaining = not_seen
1530                        .total_not_seen
1531                        .saturating_sub(not_seen.not_seen.len());
1532                    if remaining > 0 {
1533                        writeln!(
1534                            writer,
1535                            "             ... and {} more {}",
1536                            remaining.style(self.styles.count),
1537                            plural::tests_str(self.mode, remaining),
1538                        )?;
1539                    }
1540                }
1541
1542                // Print out warnings at the end, if any.
1543                write_final_warnings(self.mode, run_stats.final_stats(), &self.styles, writer)?;
1544            }
1545        }
1546
1547        Ok(())
1548    }
1549
1550    fn write_skip_line(
1551        &self,
1552        stress_index: Option<StressIndex>,
1553        test_instance: TestInstanceId<'a>,
1554        writer: &mut dyn WriteStr,
1555    ) -> io::Result<()> {
1556        write!(writer, "{:>12} ", "SKIP".style(self.styles.skip))?;
1557        // same spacing   [   0.034s]
1558        writeln!(
1559            writer,
1560            "[         ] {}",
1561            self.display_test_instance(stress_index, TestInstanceCounter::Padded, test_instance)
1562        )?;
1563
1564        Ok(())
1565    }
1566
1567    fn write_setup_script_status_line(
1568        &self,
1569        stress_index: Option<StressIndex>,
1570        script_id: &ScriptId,
1571        command: &str,
1572        args: &[String],
1573        status: &SetupScriptExecuteStatus<LiveSpec>,
1574        writer: &mut dyn WriteStr,
1575    ) -> io::Result<()> {
1576        match status.result {
1577            ExecutionResultDescription::Pass => {
1578                write!(writer, "{:>12} ", "SETUP PASS".style(self.styles.pass))?;
1579            }
1580            ExecutionResultDescription::Leak { result } => match result {
1581                LeakTimeoutResult::Pass => {
1582                    write!(writer, "{:>12} ", "SETUP LEAK".style(self.styles.skip))?;
1583                }
1584                LeakTimeoutResult::Fail => {
1585                    write!(writer, "{:>12} ", "SETUP LKFAIL".style(self.styles.fail))?;
1586                }
1587            },
1588            ref other => {
1589                let status_str = short_status_str(other);
1590                write!(
1591                    writer,
1592                    "{:>12} ",
1593                    format!("SETUP {status_str}").style(self.styles.fail),
1594                )?;
1595            }
1596        }
1597
1598        writeln!(
1599            writer,
1600            "{}{}",
1601            DisplayBracketedDuration(status.time_taken),
1602            self.display_script_instance(stress_index, script_id.clone(), command, args)
1603        )?;
1604
1605        Ok(())
1606    }
1607
1608    fn write_status_line(
1609        &self,
1610        stress_index: Option<StressIndex>,
1611        counter: TestInstanceCounter,
1612        test_instance: TestInstanceId<'a>,
1613        describe: ExecutionDescription<'_, LiveSpec>,
1614        writer: &mut dyn WriteStr,
1615    ) -> io::Result<()> {
1616        self.write_status_line_impl(
1617            stress_index,
1618            counter,
1619            test_instance,
1620            describe,
1621            StatusLineKind::Intermediate,
1622            writer,
1623        )
1624    }
1625
1626    fn write_final_status_line(
1627        &self,
1628        stress_index: Option<StressIndex>,
1629        counter: TestInstanceCounter,
1630        test_instance: TestInstanceId<'a>,
1631        describe: ExecutionDescription<'_, LiveSpec>,
1632        writer: &mut dyn WriteStr,
1633    ) -> io::Result<()> {
1634        self.write_status_line_impl(
1635            stress_index,
1636            counter,
1637            test_instance,
1638            describe,
1639            StatusLineKind::Final,
1640            writer,
1641        )
1642    }
1643
1644    fn write_status_line_impl(
1645        &self,
1646        stress_index: Option<StressIndex>,
1647        counter: TestInstanceCounter,
1648        test_instance: TestInstanceId<'a>,
1649        describe: ExecutionDescription<'_, LiveSpec>,
1650        kind: StatusLineKind,
1651        writer: &mut dyn WriteStr,
1652    ) -> io::Result<()> {
1653        let last_status = describe.last_status();
1654
1655        // Write the status prefix (e.g., "PASS", "FAIL", "FLAKY 2/3").
1656        self.write_status_line_prefix(describe, kind, writer)?;
1657
1658        // Write the duration and test instance.
1659        writeln!(
1660            writer,
1661            "{}{}",
1662            DisplayBracketedDuration(last_status.time_taken),
1663            self.display_test_instance(stress_index, counter, test_instance),
1664        )?;
1665
1666        // For Windows aborts, print out the exception code on a separate line.
1667        if let ExecutionResultDescription::Fail {
1668            failure: FailureDescription::Abort { ref abort },
1669            leaked: _,
1670        } = last_status.result
1671        {
1672            write_windows_abort_line(abort, &self.styles, writer)?;
1673        }
1674
1675        // For flaky tests configured with flaky-result = "fail", print a
1676        // supplementary line in intermediate output explaining why the passing
1677        // test is actually a failure.
1678        if kind == StatusLineKind::Intermediate
1679            && let ExecutionDescription::Flaky {
1680                result: FlakyResult::Fail,
1681                ..
1682            } = describe
1683        {
1684            writeln!(
1685                writer,
1686                "{:>12} test configured to {} if flaky",
1687                "-",
1688                "fail".style(self.styles.fail),
1689            )?;
1690        }
1691
1692        Ok(())
1693    }
1694
1695    fn write_status_line_prefix(
1696        &self,
1697        describe: ExecutionDescription<'_, LiveSpec>,
1698        kind: StatusLineKind,
1699        writer: &mut dyn WriteStr,
1700    ) -> io::Result<()> {
1701        let last_status = describe.last_status();
1702        match describe {
1703            ExecutionDescription::Success { .. } => {
1704                // Exhaustive match on (is_slow, result) to catch missing cases
1705                // at compile time. For intermediate status lines, is_slow is
1706                // ignored (shown via separate SLOW lines during execution).
1707                match (kind, last_status.is_slow, &last_status.result) {
1708                    // Final + slow variants.
1709                    (StatusLineKind::Final, true, ExecutionResultDescription::Pass) => {
1710                        write!(writer, "{:>12} ", "SLOW".style(self.styles.skip))?;
1711                    }
1712                    (
1713                        StatusLineKind::Final,
1714                        true,
1715                        ExecutionResultDescription::Leak {
1716                            result: LeakTimeoutResult::Pass,
1717                        },
1718                    ) => {
1719                        write!(writer, "{:>12} ", "SLOW + LEAK".style(self.styles.skip))?;
1720                    }
1721                    (
1722                        StatusLineKind::Final,
1723                        true,
1724                        ExecutionResultDescription::Timeout {
1725                            result: SlowTimeoutResult::Pass,
1726                        },
1727                    ) => {
1728                        write!(writer, "{:>12} ", "SLOW+TMPASS".style(self.styles.skip))?;
1729                    }
1730                    // Non-slow variants (or intermediate where is_slow is ignored).
1731                    (_, _, ExecutionResultDescription::Pass) => {
1732                        write!(writer, "{:>12} ", "PASS".style(self.styles.pass))?;
1733                    }
1734                    (
1735                        _,
1736                        _,
1737                        ExecutionResultDescription::Leak {
1738                            result: LeakTimeoutResult::Pass,
1739                        },
1740                    ) => {
1741                        write!(writer, "{:>12} ", "LEAK".style(self.styles.skip))?;
1742                    }
1743                    (
1744                        _,
1745                        _,
1746                        ExecutionResultDescription::Timeout {
1747                            result: SlowTimeoutResult::Pass,
1748                        },
1749                    ) => {
1750                        write!(writer, "{:>12} ", "TIMEOUT-PASS".style(self.styles.skip))?;
1751                    }
1752                    // These are failure cases and cannot appear in Success.
1753                    (
1754                        _,
1755                        _,
1756                        ExecutionResultDescription::Leak {
1757                            result: LeakTimeoutResult::Fail,
1758                        },
1759                    )
1760                    | (
1761                        _,
1762                        _,
1763                        ExecutionResultDescription::Timeout {
1764                            result: SlowTimeoutResult::Fail,
1765                        },
1766                    )
1767                    | (_, _, ExecutionResultDescription::Fail { .. })
1768                    | (_, _, ExecutionResultDescription::ExecFail) => {
1769                        unreachable!(
1770                            "success description cannot have failure result: {:?}",
1771                            last_status.result
1772                        )
1773                    }
1774                }
1775            }
1776            ExecutionDescription::Flaky {
1777                result: FlakyResult::Pass,
1778                ..
1779            } => {
1780                // Use the skip color to also represent a flaky test.
1781                let status = match kind {
1782                    StatusLineKind::Intermediate => {
1783                        format!("TRY {} PASS", last_status.retry_data.attempt)
1784                    }
1785                    StatusLineKind::Final => {
1786                        format!(
1787                            "FLAKY {}/{}",
1788                            last_status.retry_data.attempt, last_status.retry_data.total_attempts
1789                        )
1790                    }
1791                };
1792                write!(writer, "{:>12} ", status.style(self.styles.skip))?;
1793            }
1794            ExecutionDescription::Flaky {
1795                result: FlakyResult::Fail,
1796                ..
1797            } => {
1798                // Use the fail color for flaky tests configured as failures.
1799                let status = match kind {
1800                    StatusLineKind::Intermediate => {
1801                        format!("TRY {} PASS", last_status.retry_data.attempt)
1802                    }
1803                    StatusLineKind::Final => {
1804                        format!(
1805                            "FLKY-FL {}/{}",
1806                            last_status.retry_data.attempt, last_status.retry_data.total_attempts
1807                        )
1808                    }
1809                };
1810                write!(writer, "{:>12} ", status.style(self.styles.fail))?;
1811            }
1812            ExecutionDescription::Failure { .. } => {
1813                if last_status.retry_data.attempt == 1 {
1814                    write!(
1815                        writer,
1816                        "{:>12} ",
1817                        status_str(&last_status.result).style(self.styles.fail)
1818                    )?;
1819                } else {
1820                    let status_str = short_status_str(&last_status.result);
1821                    write!(
1822                        writer,
1823                        "{:>12} ",
1824                        format!("TRY {} {}", last_status.retry_data.attempt, status_str)
1825                            .style(self.styles.fail)
1826                    )?;
1827                }
1828            }
1829        }
1830        Ok(())
1831    }
1832
1833    fn display_test_instance(
1834        &self,
1835        stress_index: Option<StressIndex>,
1836        counter: TestInstanceCounter,
1837        instance: TestInstanceId<'a>,
1838    ) -> DisplayTestInstance<'_> {
1839        let counter_index = match (counter, self.counter_width) {
1840            (TestInstanceCounter::Counter { current, total }, Some(_)) => {
1841                Some(DisplayCounterIndex::new_counter(current, total))
1842            }
1843            (TestInstanceCounter::Padded, Some(counter_width)) => Some(
1844                DisplayCounterIndex::new_padded(self.theme_characters.hbar_char(), counter_width),
1845            ),
1846            (TestInstanceCounter::None, _) | (_, None) => None,
1847        };
1848
1849        DisplayTestInstance::new(
1850            stress_index,
1851            counter_index,
1852            instance,
1853            &self.styles.list_styles,
1854        )
1855    }
1856
1857    fn write_command_line(
1858        &self,
1859        command_line: &[String],
1860        writer: &mut dyn WriteStr,
1861    ) -> io::Result<()> {
1862        // Indent under START (13 spaces + "command").
1863        writeln!(
1864            writer,
1865            "{:>20}: {}",
1866            "command".style(self.styles.count),
1867            shell_words::join(command_line),
1868        )
1869    }
1870
1871    fn display_script_instance(
1872        &self,
1873        stress_index: Option<StressIndex>,
1874        script_id: ScriptId,
1875        command: &str,
1876        args: &[String],
1877    ) -> DisplayScriptInstance {
1878        DisplayScriptInstance::new(
1879            stress_index,
1880            script_id,
1881            command,
1882            args,
1883            self.styles.script_id,
1884            self.styles.count,
1885        )
1886    }
1887
1888    fn write_info_response(
1889        &self,
1890        index: usize,
1891        total: usize,
1892        response: &InfoResponse<'_>,
1893        writer: &mut dyn WriteStr,
1894    ) -> io::Result<()> {
1895        if index > 0 {
1896            // Show a shorter hbar than the hbar surrounding the info started
1897            // and finished lines.
1898            writeln!(writer, "{}", self.theme_characters.hbar(8))?;
1899        }
1900
1901        // "status: " is 8 characters. Pad "{}/{}:" such that it also gets to
1902        // the 8 characters.
1903        //
1904        // The width to be printed out is index width + total width + 1 for '/'
1905        // + 1 for ':' + 1 for the space after that.
1906        let count_width = decimal_char_width(index + 1) + decimal_char_width(total) + 3;
1907        let padding = 8usize.saturating_sub(count_width);
1908
1909        write!(
1910            writer,
1911            "\n* {}/{}: {:padding$}",
1912            // index is 0-based, so add 1 to make it 1-based.
1913            (index + 1).style(self.styles.count),
1914            total.style(self.styles.count),
1915            "",
1916        )?;
1917
1918        // Indent everything a bit to make it clear that this is a
1919        // response.
1920        let mut writer = indented(writer).with_str("  ").skip_initial();
1921
1922        match response {
1923            InfoResponse::SetupScript(SetupScriptInfoResponse {
1924                stress_index,
1925                script_id,
1926                program,
1927                args,
1928                state,
1929                output,
1930            }) => {
1931                // Write the setup script name.
1932                writeln!(
1933                    writer,
1934                    "{}",
1935                    self.display_script_instance(*stress_index, script_id.clone(), program, args)
1936                )?;
1937
1938                // Write the state of the script.
1939                self.write_unit_state(
1940                    UnitKind::Script,
1941                    "",
1942                    state,
1943                    output.has_errors(),
1944                    &mut writer,
1945                )?;
1946
1947                // Write the output of the script.
1948                if state.has_valid_output() {
1949                    self.unit_output.write_child_execution_output(
1950                        &self.styles,
1951                        &self.output_spec_for_info(UnitKind::Script),
1952                        output,
1953                        &mut writer,
1954                    )?;
1955                }
1956            }
1957            InfoResponse::Test(TestInfoResponse {
1958                stress_index,
1959                test_instance,
1960                retry_data,
1961                state,
1962                output,
1963            }) => {
1964                // Write the test name.
1965                writeln!(
1966                    writer,
1967                    "{}",
1968                    self.display_test_instance(
1969                        *stress_index,
1970                        TestInstanceCounter::None,
1971                        *test_instance
1972                    )
1973                )?;
1974
1975                // We want to show an attached attempt string either if this is
1976                // a DelayBeforeNextAttempt message or if this is a retry. (This
1977                // is a bit abstraction-breaking, but what good UI isn't?)
1978                let show_attempt_str = (retry_data.attempt > 1 && retry_data.total_attempts > 1)
1979                    || matches!(state, UnitState::DelayBeforeNextAttempt { .. });
1980                let attempt_str = if show_attempt_str {
1981                    format!(
1982                        "(attempt {}/{}) ",
1983                        retry_data.attempt, retry_data.total_attempts
1984                    )
1985                } else {
1986                    String::new()
1987                };
1988
1989                // Write the state of the test.
1990                self.write_unit_state(
1991                    UnitKind::Test,
1992                    &attempt_str,
1993                    state,
1994                    output.has_errors(),
1995                    &mut writer,
1996                )?;
1997
1998                // Write the output of the test.
1999                if state.has_valid_output() {
2000                    self.unit_output.write_child_execution_output(
2001                        &self.styles,
2002                        &self.output_spec_for_info(UnitKind::Test),
2003                        output,
2004                        &mut writer,
2005                    )?;
2006                }
2007            }
2008        }
2009
2010        writer.write_str_flush()?;
2011        let inner_writer = writer.into_inner();
2012
2013        // Add a newline at the end to visually separate the responses.
2014        writeln!(inner_writer)?;
2015
2016        Ok(())
2017    }
2018
2019    fn write_unit_state(
2020        &self,
2021        kind: UnitKind,
2022        attempt_str: &str,
2023        state: &UnitState,
2024        output_has_errors: bool,
2025        writer: &mut dyn WriteStr,
2026    ) -> io::Result<()> {
2027        let status_str = "status".style(self.styles.count);
2028        match state {
2029            UnitState::Running {
2030                pid,
2031                time_taken,
2032                slow_after,
2033            } => {
2034                let running_style = if output_has_errors {
2035                    self.styles.fail
2036                } else if slow_after.is_some() {
2037                    self.styles.skip
2038                } else {
2039                    self.styles.pass
2040                };
2041                write!(
2042                    writer,
2043                    "{status_str}: {attempt_str}{} {} for {:.3?}s as PID {}",
2044                    DisplayUnitKind::new(self.mode, kind),
2045                    "running".style(running_style),
2046                    time_taken.as_secs_f64(),
2047                    pid.style(self.styles.count),
2048                )?;
2049                if let Some(slow_after) = slow_after {
2050                    write!(
2051                        writer,
2052                        " (marked slow after {:.3?}s)",
2053                        slow_after.as_secs_f64()
2054                    )?;
2055                }
2056                writeln!(writer)?;
2057            }
2058            UnitState::Exiting {
2059                pid,
2060                time_taken,
2061                slow_after,
2062                tentative_result,
2063                waiting_duration,
2064                remaining,
2065            } => {
2066                write!(
2067                    writer,
2068                    "{status_str}: {attempt_str}{} ",
2069                    DisplayUnitKind::new(self.mode, kind)
2070                )?;
2071
2072                self.write_info_execution_result(
2073                    tentative_result.as_ref(),
2074                    slow_after.is_some(),
2075                    writer,
2076                )?;
2077                write!(writer, " after {:.3?}s", time_taken.as_secs_f64())?;
2078                if let Some(slow_after) = slow_after {
2079                    write!(
2080                        writer,
2081                        " (marked slow after {:.3?}s)",
2082                        slow_after.as_secs_f64()
2083                    )?;
2084                }
2085                writeln!(writer)?;
2086
2087                // Don't need to print the waiting duration for leak detection
2088                // if it's relatively small.
2089                if *waiting_duration >= Duration::from_secs(1) {
2090                    writeln!(
2091                        writer,
2092                        "{}:   spent {:.3?}s waiting for {} PID {} to shut down, \
2093                         will mark as leaky after another {:.3?}s",
2094                        "note".style(self.styles.count),
2095                        waiting_duration.as_secs_f64(),
2096                        DisplayUnitKind::new(self.mode, kind),
2097                        pid.style(self.styles.count),
2098                        remaining.as_secs_f64(),
2099                    )?;
2100                }
2101            }
2102            UnitState::Terminating(state) => {
2103                self.write_terminating_state(kind, attempt_str, state, writer)?;
2104            }
2105            UnitState::Exited {
2106                result,
2107                time_taken,
2108                slow_after,
2109            } => {
2110                write!(
2111                    writer,
2112                    "{status_str}: {attempt_str}{} ",
2113                    DisplayUnitKind::new(self.mode, kind)
2114                )?;
2115                self.write_info_execution_result(Some(result), slow_after.is_some(), writer)?;
2116                write!(writer, " after {:.3?}s", time_taken.as_secs_f64())?;
2117                if let Some(slow_after) = slow_after {
2118                    write!(
2119                        writer,
2120                        " (marked slow after {:.3?}s)",
2121                        slow_after.as_secs_f64()
2122                    )?;
2123                }
2124                writeln!(writer)?;
2125            }
2126            UnitState::DelayBeforeNextAttempt {
2127                previous_result,
2128                previous_slow,
2129                waiting_duration,
2130                remaining,
2131            } => {
2132                write!(
2133                    writer,
2134                    "{status_str}: {attempt_str}{} ",
2135                    DisplayUnitKind::new(self.mode, kind)
2136                )?;
2137                self.write_info_execution_result(Some(previous_result), *previous_slow, writer)?;
2138                writeln!(
2139                    writer,
2140                    ", currently {} before next attempt",
2141                    "waiting".style(self.styles.count)
2142                )?;
2143                writeln!(
2144                    writer,
2145                    "{}:   waited {:.3?}s so far, will wait another {:.3?}s before retrying {}",
2146                    "note".style(self.styles.count),
2147                    waiting_duration.as_secs_f64(),
2148                    remaining.as_secs_f64(),
2149                    DisplayUnitKind::new(self.mode, kind),
2150                )?;
2151            }
2152        }
2153
2154        Ok(())
2155    }
2156
2157    fn write_terminating_state(
2158        &self,
2159        kind: UnitKind,
2160        attempt_str: &str,
2161        state: &UnitTerminatingState,
2162        writer: &mut dyn WriteStr,
2163    ) -> io::Result<()> {
2164        let UnitTerminatingState {
2165            pid,
2166            time_taken,
2167            reason,
2168            method,
2169            waiting_duration,
2170            remaining,
2171        } = state;
2172
2173        writeln!(
2174            writer,
2175            "{}: {attempt_str}{} {} PID {} due to {} ({} ran for {:.3?}s)",
2176            "status".style(self.styles.count),
2177            "terminating".style(self.styles.fail),
2178            DisplayUnitKind::new(self.mode, kind),
2179            pid.style(self.styles.count),
2180            reason.style(self.styles.count),
2181            DisplayUnitKind::new(self.mode, kind),
2182            time_taken.as_secs_f64(),
2183        )?;
2184
2185        match method {
2186            #[cfg(unix)]
2187            UnitTerminateMethod::Signal(signal) => {
2188                writeln!(
2189                    writer,
2190                    "{}:   sent {} to process group; spent {:.3?}s waiting for {} to exit, \
2191                     will SIGKILL after another {:.3?}s",
2192                    "note".style(self.styles.count),
2193                    signal,
2194                    waiting_duration.as_secs_f64(),
2195                    DisplayUnitKind::new(self.mode, kind),
2196                    remaining.as_secs_f64(),
2197                )?;
2198            }
2199            #[cfg(windows)]
2200            UnitTerminateMethod::JobObject => {
2201                writeln!(
2202                    writer,
2203                    // Job objects are like SIGKILL -- they terminate
2204                    // immediately. No need to show the waiting duration or
2205                    // remaining time.
2206                    "{}:   instructed job object to terminate",
2207                    "note".style(self.styles.count),
2208                )?;
2209            }
2210            #[cfg(windows)]
2211            UnitTerminateMethod::Wait => {
2212                writeln!(
2213                    writer,
2214                    "{}:   waiting for {} to exit on its own; spent {:.3?}s, will terminate \
2215                     job object after another {:.3?}s",
2216                    "note".style(self.styles.count),
2217                    DisplayUnitKind::new(self.mode, kind),
2218                    waiting_duration.as_secs_f64(),
2219                    remaining.as_secs_f64(),
2220                )?;
2221            }
2222            #[cfg(test)]
2223            UnitTerminateMethod::Fake => {
2224                // This is only used in tests.
2225                writeln!(
2226                    writer,
2227                    "{}:   fake termination method; spent {:.3?}s waiting for {} to exit, \
2228                     will kill after another {:.3?}s",
2229                    "note".style(self.styles.count),
2230                    waiting_duration.as_secs_f64(),
2231                    DisplayUnitKind::new(self.mode, kind),
2232                    remaining.as_secs_f64(),
2233                )?;
2234            }
2235        }
2236
2237        Ok(())
2238    }
2239
2240    // TODO: this should be unified with write_exit_status above -- we need a
2241    // general, short description of what's happened to both an in-progress and
2242    // a final unit.
2243    fn write_info_execution_result(
2244        &self,
2245        result: Option<&ExecutionResultDescription>,
2246        is_slow: bool,
2247        writer: &mut dyn WriteStr,
2248    ) -> io::Result<()> {
2249        match result {
2250            Some(ExecutionResultDescription::Pass) => {
2251                let style = if is_slow {
2252                    self.styles.skip
2253                } else {
2254                    self.styles.pass
2255                };
2256
2257                write!(writer, "{}", "passed".style(style))
2258            }
2259            Some(ExecutionResultDescription::Leak {
2260                result: LeakTimeoutResult::Pass,
2261            }) => write!(
2262                writer,
2263                "{}",
2264                "passed with leaked handles".style(self.styles.skip)
2265            ),
2266            Some(ExecutionResultDescription::Leak {
2267                result: LeakTimeoutResult::Fail,
2268            }) => write!(
2269                writer,
2270                "{}: exited with code 0, but leaked handles",
2271                "failed".style(self.styles.fail),
2272            ),
2273            Some(ExecutionResultDescription::Timeout {
2274                result: SlowTimeoutResult::Pass,
2275            }) => {
2276                write!(writer, "{}", "passed with timeout".style(self.styles.skip))
2277            }
2278            Some(ExecutionResultDescription::Timeout {
2279                result: SlowTimeoutResult::Fail,
2280            }) => {
2281                write!(writer, "{}", "timed out".style(self.styles.fail))
2282            }
2283            Some(ExecutionResultDescription::Fail {
2284                failure: FailureDescription::Abort { abort },
2285                leaked,
2286            }) => {
2287                // The errors are shown in the output.
2288                write!(writer, "{}", "aborted".style(self.styles.fail))?;
2289                // AbortDescription is platform-independent and contains display
2290                // info. Note that Windows descriptions are handled separately,
2291                // in write_windows_abort_suffix.
2292                if let AbortDescription::UnixSignal { signal, name } = abort {
2293                    write!(writer, " with signal {}", signal.style(self.styles.count))?;
2294                    if let Some(s) = name {
2295                        write!(writer, ": SIG{s}")?;
2296                    }
2297                }
2298                if *leaked {
2299                    write!(writer, " (leaked handles)")?;
2300                }
2301                Ok(())
2302            }
2303            Some(ExecutionResultDescription::Fail {
2304                failure: FailureDescription::ExitCode { code },
2305                leaked,
2306            }) => {
2307                write!(
2308                    writer,
2309                    "{} with exit code {}",
2310                    "failed".style(self.styles.fail),
2311                    code.style(self.styles.count),
2312                )?;
2313                if *leaked {
2314                    write!(writer, " (leaked handles)")?;
2315                }
2316                Ok(())
2317            }
2318            Some(ExecutionResultDescription::ExecFail) => {
2319                write!(writer, "{}", "failed to execute".style(self.styles.fail))
2320            }
2321            None => {
2322                write!(
2323                    writer,
2324                    "{} with unknown status",
2325                    "failed".style(self.styles.fail)
2326                )
2327            }
2328        }
2329    }
2330
2331    fn write_setup_script_execute_status(
2332        &self,
2333        run_status: &SetupScriptExecuteStatus<LiveSpec>,
2334        writer: &mut dyn WriteStr,
2335    ) -> io::Result<()> {
2336        let spec = self.output_spec_for_finished(&run_status.result, false);
2337        self.unit_output.write_child_execution_output(
2338            &self.styles,
2339            &spec,
2340            &run_status.output,
2341            writer,
2342        )?;
2343
2344        if show_finished_status_info_line(&run_status.result) {
2345            write!(
2346                writer,
2347                // Align with output.
2348                "    (script ",
2349            )?;
2350            self.write_info_execution_result(Some(&run_status.result), run_status.is_slow, writer)?;
2351            writeln!(writer, ")\n")?;
2352        }
2353
2354        Ok(())
2355    }
2356
2357    fn write_test_execute_status(
2358        &self,
2359        run_status: &ExecuteStatus<LiveSpec>,
2360        is_retry: bool,
2361        writer: &mut dyn WriteStr,
2362    ) -> io::Result<()> {
2363        // Styling is based on run_status.result, which is the individual
2364        // attempt's result. For flaky-failed tests, this is called on the
2365        // last (successful) attempt, so pass styling (green headers, no
2366        // error extraction) is correct — the output content has no panics.
2367        let spec = self.output_spec_for_finished(&run_status.result, is_retry);
2368        self.unit_output.write_child_execution_output(
2369            &self.styles,
2370            &spec,
2371            &run_status.output,
2372            writer,
2373        )?;
2374
2375        if show_finished_status_info_line(&run_status.result) {
2376            write!(
2377                writer,
2378                // Align with output.
2379                "    (test ",
2380            )?;
2381            self.write_info_execution_result(Some(&run_status.result), run_status.is_slow, writer)?;
2382            writeln!(writer, ")\n")?;
2383        }
2384
2385        Ok(())
2386    }
2387
2388    fn output_spec_for_finished(
2389        &self,
2390        result: &ExecutionResultDescription,
2391        is_retry: bool,
2392    ) -> ChildOutputSpec {
2393        let header_style = if is_retry {
2394            self.styles.retry
2395        } else {
2396            match result {
2397                ExecutionResultDescription::Pass => self.styles.pass,
2398                ExecutionResultDescription::Leak {
2399                    result: LeakTimeoutResult::Pass,
2400                } => self.styles.skip,
2401                ExecutionResultDescription::Leak {
2402                    result: LeakTimeoutResult::Fail,
2403                } => self.styles.fail,
2404                ExecutionResultDescription::Timeout {
2405                    result: SlowTimeoutResult::Pass,
2406                } => self.styles.skip,
2407                ExecutionResultDescription::Timeout {
2408                    result: SlowTimeoutResult::Fail,
2409                } => self.styles.fail,
2410                ExecutionResultDescription::Fail { .. } => self.styles.fail,
2411                ExecutionResultDescription::ExecFail => self.styles.fail,
2412            }
2413        };
2414
2415        // Adding an hbar at the end gives the text a bit of visual weight that
2416        // makes it look more balanced. Align it with the end of the header to
2417        // provide a visual transition from status lines (PASS/FAIL etc) to
2418        // indented output.
2419        //
2420        // With indentation, the output looks like:
2421        //
2422        //         FAIL [ .... ]
2423        //   stdout ───
2424        //     <test stdout>
2425        //   stderr ───
2426        //     <test stderr>
2427        //
2428        // Without indentation:
2429        //
2430        //         FAIL [ .... ]
2431        // ── stdout ──
2432        // <test stdout>
2433        // ── stderr ──
2434        // <test stderr>
2435        let (six_char_start, six_char_end, eight_char_start, eight_char_end, output_indent) =
2436            if self.no_output_indent {
2437                (
2438                    self.theme_characters.hbar(2),
2439                    self.theme_characters.hbar(2),
2440                    self.theme_characters.hbar(1),
2441                    self.theme_characters.hbar(1),
2442                    "",
2443                )
2444            } else {
2445                (
2446                    " ".to_owned(),
2447                    self.theme_characters.hbar(3),
2448                    " ".to_owned(),
2449                    self.theme_characters.hbar(1),
2450                    "    ",
2451                )
2452            };
2453
2454        let stdout_header = format!(
2455            "{} {} {}",
2456            six_char_start.style(header_style),
2457            "stdout".style(header_style),
2458            six_char_end.style(header_style),
2459        );
2460        let stderr_header = format!(
2461            "{} {} {}",
2462            six_char_start.style(header_style),
2463            "stderr".style(header_style),
2464            six_char_end.style(header_style),
2465        );
2466        let combined_header = format!(
2467            "{} {} {}",
2468            six_char_start.style(header_style),
2469            "output".style(header_style),
2470            six_char_end.style(header_style),
2471        );
2472        let exec_fail_header = format!(
2473            "{} {} {}",
2474            eight_char_start.style(header_style),
2475            "execfail".style(header_style),
2476            eight_char_end.style(header_style),
2477        );
2478
2479        ChildOutputSpec {
2480            kind: UnitKind::Test,
2481            stdout_header,
2482            stderr_header,
2483            combined_header,
2484            exec_fail_header,
2485            output_indent,
2486        }
2487    }
2488
2489    // Info response queries are more compact and so have a somewhat different
2490    // output format. But at some point we should consider using the same format
2491    // for both regular test output and info responses.
2492    fn output_spec_for_info(&self, kind: UnitKind) -> ChildOutputSpec {
2493        let stdout_header = format!("{}:", "stdout".style(self.styles.count));
2494        let stderr_header = format!("{}:", "stderr".style(self.styles.count));
2495        let combined_header = format!("{}:", "output".style(self.styles.count));
2496        let exec_fail_header = format!("{}:", "errors".style(self.styles.count));
2497
2498        ChildOutputSpec {
2499            kind,
2500            stdout_header,
2501            stderr_header,
2502            combined_header,
2503            exec_fail_header,
2504            output_indent: "  ",
2505        }
2506    }
2507}
2508
2509#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
2510enum TestInstanceCounter {
2511    Counter { current: usize, total: usize },
2512    Padded,
2513    None,
2514}
2515
2516/// Whether a status line is an intermediate line (during execution) or a final
2517/// line (in the summary).
2518#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2519enum StatusLineKind {
2520    /// Intermediate status line shown during test execution.
2521    Intermediate,
2522    /// Final status line shown in the summary.
2523    Final,
2524}
2525
2526const LIBTEST_PANIC_EXIT_CODE: i32 = 101;
2527
2528// Whether to show a status line for finished units (after STDOUT:/STDERR:).
2529// This does not apply to info responses which have their own logic.
2530fn show_finished_status_info_line(result: &ExecutionResultDescription) -> bool {
2531    // Don't show the status line if the exit code is the default from cargo test panicking.
2532    match result {
2533        ExecutionResultDescription::Pass => false,
2534        ExecutionResultDescription::Leak {
2535            result: LeakTimeoutResult::Pass,
2536        } => {
2537            // Show the leaked-handles message.
2538            true
2539        }
2540        ExecutionResultDescription::Leak {
2541            result: LeakTimeoutResult::Fail,
2542        } => {
2543            // This is a confusing state without the message at the end.
2544            true
2545        }
2546        ExecutionResultDescription::Fail {
2547            failure: FailureDescription::ExitCode { code },
2548            leaked,
2549        } => {
2550            // Don't show the status line if the exit code is the default from
2551            // cargo test panicking, and if there were no leaked handles.
2552            *code != LIBTEST_PANIC_EXIT_CODE && !leaked
2553        }
2554        ExecutionResultDescription::Fail {
2555            failure: FailureDescription::Abort { .. },
2556            leaked: _,
2557        } => {
2558            // Showing a line at the end aids in clarity.
2559            true
2560        }
2561        ExecutionResultDescription::ExecFail => {
2562            // This is already shown as an error so there's no reason to show it
2563            // again.
2564            false
2565        }
2566        ExecutionResultDescription::Timeout { .. } => {
2567            // Show this to be clear what happened.
2568            true
2569        }
2570    }
2571}
2572
2573fn status_str(result: &ExecutionResultDescription) -> Cow<'static, str> {
2574    // Max 12 characters here.
2575    match result {
2576        ExecutionResultDescription::Fail {
2577            failure:
2578                FailureDescription::Abort {
2579                    abort: AbortDescription::UnixSignal { signal, name },
2580                },
2581            leaked: _,
2582        } => match name {
2583            Some(s) => format!("SIG{s}").into(),
2584            None => format!("ABORT SIG {signal}").into(),
2585        },
2586        ExecutionResultDescription::Fail {
2587            failure:
2588                FailureDescription::Abort {
2589                    abort: AbortDescription::WindowsNtStatus { .. },
2590                }
2591                | FailureDescription::Abort {
2592                    abort: AbortDescription::WindowsJobObject,
2593                },
2594            leaked: _,
2595        } => {
2596            // Going to print out the full error message on the following line -- just "ABORT" will
2597            // do for now.
2598            "ABORT".into()
2599        }
2600        ExecutionResultDescription::Fail {
2601            failure: FailureDescription::ExitCode { .. },
2602            leaked: true,
2603        } => "FAIL + LEAK".into(),
2604        ExecutionResultDescription::Fail {
2605            failure: FailureDescription::ExitCode { .. },
2606            leaked: false,
2607        } => "FAIL".into(),
2608        ExecutionResultDescription::ExecFail => "XFAIL".into(),
2609        ExecutionResultDescription::Pass => "PASS".into(),
2610        ExecutionResultDescription::Leak {
2611            result: LeakTimeoutResult::Pass,
2612        } => "LEAK".into(),
2613        ExecutionResultDescription::Leak {
2614            result: LeakTimeoutResult::Fail,
2615        } => "LEAK-FAIL".into(),
2616        ExecutionResultDescription::Timeout {
2617            result: SlowTimeoutResult::Pass,
2618        } => "TIMEOUT-PASS".into(),
2619        ExecutionResultDescription::Timeout {
2620            result: SlowTimeoutResult::Fail,
2621        } => "TIMEOUT".into(),
2622    }
2623}
2624
2625fn short_status_str(result: &ExecutionResultDescription) -> Cow<'static, str> {
2626    // Use shorter strings for this (max 6 characters).
2627    match result {
2628        ExecutionResultDescription::Fail {
2629            failure:
2630                FailureDescription::Abort {
2631                    abort: AbortDescription::UnixSignal { signal, name },
2632                },
2633            leaked: _,
2634        } => match name {
2635            Some(s) => s.to_string().into(),
2636            None => format!("SIG {signal}").into(),
2637        },
2638        ExecutionResultDescription::Fail {
2639            failure:
2640                FailureDescription::Abort {
2641                    abort: AbortDescription::WindowsNtStatus { .. },
2642                }
2643                | FailureDescription::Abort {
2644                    abort: AbortDescription::WindowsJobObject,
2645                },
2646            leaked: _,
2647        } => {
2648            // Going to print out the full error message on the following line -- just "ABORT" will
2649            // do for now.
2650            "ABORT".into()
2651        }
2652        ExecutionResultDescription::Fail {
2653            failure: FailureDescription::ExitCode { .. },
2654            leaked: true,
2655        } => "FL+LK".into(),
2656        ExecutionResultDescription::Fail {
2657            failure: FailureDescription::ExitCode { .. },
2658            leaked: false,
2659        } => "FAIL".into(),
2660        ExecutionResultDescription::ExecFail => "XFAIL".into(),
2661        ExecutionResultDescription::Pass => "PASS".into(),
2662        ExecutionResultDescription::Leak {
2663            result: LeakTimeoutResult::Pass,
2664        } => "LEAK".into(),
2665        ExecutionResultDescription::Leak {
2666            result: LeakTimeoutResult::Fail,
2667        } => "LKFAIL".into(),
2668        ExecutionResultDescription::Timeout {
2669            result: SlowTimeoutResult::Pass,
2670        } => "TMPASS".into(),
2671        ExecutionResultDescription::Timeout {
2672            result: SlowTimeoutResult::Fail,
2673        } => "TMT".into(),
2674    }
2675}
2676
2677/// Writes a supplementary line for Windows abort statuses.
2678///
2679/// For Unix signals, this is a no-op since the signal info is displayed inline.
2680fn write_windows_abort_line(
2681    status: &AbortDescription,
2682    styles: &Styles,
2683    writer: &mut dyn WriteStr,
2684) -> io::Result<()> {
2685    match status {
2686        AbortDescription::UnixSignal { .. } => {
2687            // Unix signal info is displayed inline, no separate line needed.
2688            Ok(())
2689        }
2690        AbortDescription::WindowsNtStatus { code, message } => {
2691            // For subsequent lines, use an indented displayer with {:>12}
2692            // (ensuring that message lines are aligned).
2693            const INDENT: &str = "           - ";
2694            let mut indented = indented(writer).with_str(INDENT).skip_initial();
2695            // Format code as 10 characters ("0x" + 8 hex digits) for uniformity.
2696            let code_str = format!("{:#010x}", code.style(styles.count));
2697            let status_str = match message {
2698                Some(msg) => format!("{code_str}: {msg}"),
2699                None => code_str,
2700            };
2701            writeln!(
2702                indented,
2703                "{:>12} {} {}",
2704                "-",
2705                "with code".style(styles.fail),
2706                status_str,
2707            )?;
2708            indented.write_str_flush()
2709        }
2710        AbortDescription::WindowsJobObject => {
2711            writeln!(
2712                writer,
2713                "{:>12} {} via {}",
2714                "-",
2715                "terminated".style(styles.fail),
2716                "job object".style(styles.count),
2717            )
2718        }
2719    }
2720}
2721
2722#[cfg(test)]
2723mod tests {
2724    use super::*;
2725    use crate::{
2726        errors::{ChildError, ChildFdError, ChildStartError, ErrorList},
2727        reporter::{
2728            ShowProgress,
2729            events::{
2730                ChildExecutionOutputDescription, ExecutionResult, FailureStatus,
2731                UnitTerminateReason,
2732            },
2733            test_helpers::global_slot_assignment,
2734        },
2735        test_output::{ChildExecutionOutput, ChildOutput, ChildSplitOutput},
2736    };
2737    use bytes::Bytes;
2738    use chrono::Local;
2739    use nextest_metadata::{RustBinaryId, TestCaseName};
2740    use quick_junit::ReportUuid;
2741    use smol_str::SmolStr;
2742    use std::{num::NonZero, sync::Arc};
2743    use test_case::test_case;
2744
2745    /// Creates a test reporter with default settings and calls the given function with it.
2746    ///
2747    /// Returns the output written to the reporter.
2748    fn with_reporter<'a, F>(f: F, out: &'a mut String)
2749    where
2750        F: FnOnce(DisplayReporter<'a>),
2751    {
2752        with_reporter_impl(f, out, false)
2753    }
2754
2755    /// Creates a test reporter with verbose mode enabled.
2756    fn with_verbose_reporter<'a, F>(f: F, out: &'a mut String)
2757    where
2758        F: FnOnce(DisplayReporter<'a>),
2759    {
2760        with_reporter_impl(f, out, true)
2761    }
2762
2763    fn with_reporter_impl<'a, F>(f: F, out: &'a mut String, verbose: bool)
2764    where
2765        F: FnOnce(DisplayReporter<'a>),
2766    {
2767        let builder = DisplayReporterBuilder {
2768            mode: NextestRunMode::Test,
2769            default_filter: CompiledDefaultFilter::for_default_config(),
2770            display_config: DisplayConfig {
2771                show_progress: ShowProgress::Counter,
2772                no_capture: true,
2773                status_level: Some(StatusLevel::Fail),
2774                final_status_level: Some(FinalStatusLevel::Fail),
2775                profile_status_level: StatusLevel::Fail,
2776                profile_final_status_level: FinalStatusLevel::Fail,
2777            },
2778            run_count: 5000,
2779            success_output: Some(TestOutputDisplay::Immediate),
2780            failure_output: Some(TestOutputDisplay::Immediate),
2781            should_colorize: false,
2782            verbose,
2783            no_output_indent: false,
2784            max_progress_running: MaxProgressRunning::default(),
2785            show_term_progress: ShowTerminalProgress::No,
2786            displayer_kind: DisplayerKind::Live,
2787            redactor: Redactor::noop(),
2788        };
2789
2790        let output = ReporterOutput::Writer {
2791            writer: out,
2792            use_unicode: true,
2793        };
2794        let reporter = builder.build(output);
2795        f(reporter);
2796    }
2797
2798    /// Creates a test reporter with a specific status level and capture
2799    /// enabled (so the status level is not overridden).
2800    fn with_reporter_at_status_level<'a, F>(f: F, out: &'a mut String, status_level: StatusLevel)
2801    where
2802        F: FnOnce(DisplayReporter<'a>),
2803    {
2804        let builder = DisplayReporterBuilder {
2805            mode: NextestRunMode::Test,
2806            default_filter: CompiledDefaultFilter::for_default_config(),
2807            display_config: DisplayConfig {
2808                show_progress: ShowProgress::Counter,
2809                no_capture: false,
2810                status_level: Some(status_level),
2811                final_status_level: Some(FinalStatusLevel::Fail),
2812                profile_status_level: StatusLevel::Fail,
2813                profile_final_status_level: FinalStatusLevel::Fail,
2814            },
2815            run_count: 5000,
2816            success_output: Some(TestOutputDisplay::Immediate),
2817            failure_output: Some(TestOutputDisplay::Immediate),
2818            should_colorize: false,
2819            verbose: false,
2820            no_output_indent: false,
2821            max_progress_running: MaxProgressRunning::default(),
2822            show_term_progress: ShowTerminalProgress::No,
2823            displayer_kind: DisplayerKind::Live,
2824            redactor: Redactor::noop(),
2825        };
2826
2827        let output = ReporterOutput::Writer {
2828            writer: out,
2829            use_unicode: true,
2830        };
2831        let reporter = builder.build(output);
2832        f(reporter);
2833    }
2834
2835    fn make_split_output(
2836        result: Option<ExecutionResult>,
2837        stdout: &str,
2838        stderr: &str,
2839    ) -> ChildExecutionOutputDescription<LiveSpec> {
2840        ChildExecutionOutput::Output {
2841            result,
2842            output: ChildOutput::Split(ChildSplitOutput {
2843                stdout: Some(Bytes::from(stdout.to_owned()).into()),
2844                stderr: Some(Bytes::from(stderr.to_owned()).into()),
2845            }),
2846            errors: None,
2847        }
2848        .into()
2849    }
2850
2851    fn make_split_output_with_errors(
2852        result: Option<ExecutionResult>,
2853        stdout: &str,
2854        stderr: &str,
2855        errors: Vec<ChildError>,
2856    ) -> ChildExecutionOutputDescription<LiveSpec> {
2857        ChildExecutionOutput::Output {
2858            result,
2859            output: ChildOutput::Split(ChildSplitOutput {
2860                stdout: Some(Bytes::from(stdout.to_owned()).into()),
2861                stderr: Some(Bytes::from(stderr.to_owned()).into()),
2862            }),
2863            errors: ErrorList::new("testing split output", errors),
2864        }
2865        .into()
2866    }
2867
2868    fn make_combined_output_with_errors(
2869        result: Option<ExecutionResult>,
2870        output: &str,
2871        errors: Vec<ChildError>,
2872    ) -> ChildExecutionOutputDescription<LiveSpec> {
2873        ChildExecutionOutput::Output {
2874            result,
2875            output: ChildOutput::Combined {
2876                output: Bytes::from(output.to_owned()).into(),
2877            },
2878            errors: ErrorList::new("testing split output", errors),
2879        }
2880        .into()
2881    }
2882
2883    /// Helper to build a passing `FinalOutput`.
2884    fn make_pass_output() -> FinalOutput {
2885        let status = ExecuteStatus {
2886            retry_data: RetryData {
2887                attempt: 1,
2888                total_attempts: 1,
2889            },
2890            output: make_split_output(Some(ExecutionResult::Pass), "", ""),
2891            result: ExecutionResultDescription::Pass,
2892            start_time: Local::now().into(),
2893            time_taken: Duration::from_secs(1),
2894            is_slow: false,
2895            delay_before_start: Duration::ZERO,
2896            error_summary: None,
2897            output_error_slice: None,
2898        };
2899        FinalOutput::Executed {
2900            run_statuses: ExecutionStatuses::new(vec![status], FlakyResult::default()),
2901            display_output: false,
2902        }
2903    }
2904
2905    /// Helper to build a failing `FinalOutput`.
2906    fn make_fail_output() -> FinalOutput {
2907        let result = ExecutionResult::Fail {
2908            failure_status: FailureStatus::ExitCode(1),
2909            leaked: false,
2910        };
2911        let status = ExecuteStatus {
2912            retry_data: RetryData {
2913                attempt: 1,
2914                total_attempts: 1,
2915            },
2916            output: make_split_output(Some(result), "", ""),
2917            result: ExecutionResultDescription::from(result),
2918            start_time: Local::now().into(),
2919            time_taken: Duration::from_secs(1),
2920            is_slow: false,
2921            delay_before_start: Duration::ZERO,
2922            error_summary: None,
2923            output_error_slice: None,
2924        };
2925        FinalOutput::Executed {
2926            run_statuses: ExecutionStatuses::new(vec![status], FlakyResult::default()),
2927            display_output: false,
2928        }
2929    }
2930
2931    /// Helper to build a skipped `FinalOutput`.
2932    fn make_skip_output() -> FinalOutput {
2933        FinalOutput::Skipped(MismatchReason::Ignored)
2934    }
2935
2936    /// Extract `(binary_id, test_name)` pairs from sorted entries for assertion.
2937    fn extract_ids<'a>(entries: &[FinalOutputEntry<'a>]) -> Vec<(&'a str, &'a str)> {
2938        entries
2939            .iter()
2940            .map(|e| (e.instance.binary_id.as_str(), e.instance.test_name.as_str()))
2941            .collect()
2942    }
2943
2944    #[test]
2945    fn final_status_line() {
2946        let binary_id = RustBinaryId::new("my-binary-id");
2947        let test_name = TestCaseName::new("test1");
2948        let test_instance = TestInstanceId {
2949            binary_id: &binary_id,
2950            test_name: &test_name,
2951        };
2952
2953        let fail_result_internal = ExecutionResult::Fail {
2954            failure_status: FailureStatus::ExitCode(1),
2955            leaked: false,
2956        };
2957        let fail_result = ExecutionResultDescription::from(fail_result_internal);
2958
2959        let fail_status = ExecuteStatus {
2960            retry_data: RetryData {
2961                attempt: 1,
2962                total_attempts: 2,
2963            },
2964            // output is not relevant here.
2965            output: make_split_output(Some(fail_result_internal), "", ""),
2966            result: fail_result.clone(),
2967            start_time: Local::now().into(),
2968            time_taken: Duration::from_secs(1),
2969            is_slow: false,
2970            delay_before_start: Duration::ZERO,
2971            error_summary: None,
2972            output_error_slice: None,
2973        };
2974        let fail_describe = ExecutionDescription::Failure {
2975            first_status: &fail_status,
2976            last_status: &fail_status,
2977            retries: &[],
2978        };
2979
2980        let flaky_status = ExecuteStatus {
2981            retry_data: RetryData {
2982                attempt: 2,
2983                total_attempts: 2,
2984            },
2985            // output is not relevant here.
2986            output: make_split_output(Some(fail_result_internal), "", ""),
2987            result: ExecutionResultDescription::Pass,
2988            start_time: Local::now().into(),
2989            time_taken: Duration::from_secs(2),
2990            is_slow: false,
2991            delay_before_start: Duration::ZERO,
2992            error_summary: None,
2993            output_error_slice: None,
2994        };
2995
2996        // Make an `ExecutionStatuses` with a failure and a success, indicating flakiness.
2997        let statuses =
2998            ExecutionStatuses::new(vec![fail_status.clone(), flaky_status], FlakyResult::Pass);
2999        let flaky_describe = statuses.describe();
3000
3001        let mut out = String::new();
3002
3003        with_reporter(
3004            |mut reporter| {
3005                // TODO: write a bunch more outputs here.
3006                reporter
3007                    .inner
3008                    .write_final_status_line(
3009                        None,
3010                        TestInstanceCounter::None,
3011                        test_instance,
3012                        fail_describe,
3013                        reporter.output.writer_mut().unwrap(),
3014                    )
3015                    .unwrap();
3016
3017                reporter
3018                    .inner
3019                    .write_final_status_line(
3020                        Some(StressIndex {
3021                            current: 1,
3022                            total: None,
3023                        }),
3024                        TestInstanceCounter::Padded,
3025                        test_instance,
3026                        flaky_describe,
3027                        reporter.output.writer_mut().unwrap(),
3028                    )
3029                    .unwrap();
3030
3031                reporter
3032                    .inner
3033                    .write_final_status_line(
3034                        Some(StressIndex {
3035                            current: 2,
3036                            total: Some(NonZero::new(3).unwrap()),
3037                        }),
3038                        TestInstanceCounter::Counter {
3039                            current: 20,
3040                            total: 5000,
3041                        },
3042                        test_instance,
3043                        flaky_describe,
3044                        reporter.output.writer_mut().unwrap(),
3045                    )
3046                    .unwrap();
3047            },
3048            &mut out,
3049        );
3050
3051        insta::assert_snapshot!("final_status_output", out,);
3052    }
3053
3054    #[test]
3055    fn status_line_all_variants() {
3056        let binary_id = RustBinaryId::new("my-binary-id");
3057        let test_name = TestCaseName::new("test_name");
3058        let test_instance = TestInstanceId {
3059            binary_id: &binary_id,
3060            test_name: &test_name,
3061        };
3062
3063        // --- Success result types ---
3064        let pass_result_internal = ExecutionResult::Pass;
3065        let pass_result = ExecutionResultDescription::from(pass_result_internal);
3066
3067        let leak_pass_result_internal = ExecutionResult::Leak {
3068            result: LeakTimeoutResult::Pass,
3069        };
3070        let leak_pass_result = ExecutionResultDescription::from(leak_pass_result_internal);
3071
3072        let timeout_pass_result_internal = ExecutionResult::Timeout {
3073            result: SlowTimeoutResult::Pass,
3074        };
3075        let timeout_pass_result = ExecutionResultDescription::from(timeout_pass_result_internal);
3076
3077        // --- Failure result types ---
3078        let fail_result_internal = ExecutionResult::Fail {
3079            failure_status: FailureStatus::ExitCode(1),
3080            leaked: false,
3081        };
3082        let fail_result = ExecutionResultDescription::from(fail_result_internal);
3083
3084        let fail_leak_result_internal = ExecutionResult::Fail {
3085            failure_status: FailureStatus::ExitCode(1),
3086            leaked: true,
3087        };
3088        let fail_leak_result = ExecutionResultDescription::from(fail_leak_result_internal);
3089
3090        let exec_fail_result_internal = ExecutionResult::ExecFail;
3091        let exec_fail_result = ExecutionResultDescription::from(exec_fail_result_internal);
3092
3093        let leak_fail_result_internal = ExecutionResult::Leak {
3094            result: LeakTimeoutResult::Fail,
3095        };
3096        let leak_fail_result = ExecutionResultDescription::from(leak_fail_result_internal);
3097
3098        let timeout_fail_result_internal = ExecutionResult::Timeout {
3099            result: SlowTimeoutResult::Fail,
3100        };
3101        let timeout_fail_result = ExecutionResultDescription::from(timeout_fail_result_internal);
3102
3103        // Construct abort results directly as ExecutionResultDescription (platform-independent).
3104        let abort_unix_result = ExecutionResultDescription::Fail {
3105            failure: FailureDescription::Abort {
3106                abort: AbortDescription::UnixSignal {
3107                    signal: 11,
3108                    name: Some("SEGV".into()),
3109                },
3110            },
3111            leaked: false,
3112        };
3113        let abort_windows_result = ExecutionResultDescription::Fail {
3114            failure: FailureDescription::Abort {
3115                abort: AbortDescription::WindowsNtStatus {
3116                    // STATUS_ACCESS_VIOLATION = 0xC0000005
3117                    code: 0xC0000005_u32 as i32,
3118                    message: Some("Access violation".into()),
3119                },
3120            },
3121            leaked: false,
3122        };
3123
3124        // --- Success statuses (is_slow = false) ---
3125        let pass_status = ExecuteStatus {
3126            retry_data: RetryData {
3127                attempt: 1,
3128                total_attempts: 1,
3129            },
3130            output: make_split_output(Some(pass_result_internal), "", ""),
3131            result: pass_result.clone(),
3132            start_time: Local::now().into(),
3133            time_taken: Duration::from_secs(1),
3134            is_slow: false,
3135            delay_before_start: Duration::ZERO,
3136            error_summary: None,
3137            output_error_slice: None,
3138        };
3139
3140        let leak_pass_status = ExecuteStatus {
3141            retry_data: RetryData {
3142                attempt: 1,
3143                total_attempts: 1,
3144            },
3145            output: make_split_output(Some(leak_pass_result_internal), "", ""),
3146            result: leak_pass_result.clone(),
3147            start_time: Local::now().into(),
3148            time_taken: Duration::from_secs(2),
3149            is_slow: false,
3150            delay_before_start: Duration::ZERO,
3151            error_summary: None,
3152            output_error_slice: None,
3153        };
3154
3155        let timeout_pass_status = ExecuteStatus {
3156            retry_data: RetryData {
3157                attempt: 1,
3158                total_attempts: 1,
3159            },
3160            output: make_split_output(Some(timeout_pass_result_internal), "", ""),
3161            result: timeout_pass_result.clone(),
3162            start_time: Local::now().into(),
3163            time_taken: Duration::from_secs(240),
3164            is_slow: false,
3165            delay_before_start: Duration::ZERO,
3166            error_summary: None,
3167            output_error_slice: None,
3168        };
3169
3170        // --- Success statuses (is_slow = true) ---
3171        let pass_slow_status = ExecuteStatus {
3172            retry_data: RetryData {
3173                attempt: 1,
3174                total_attempts: 1,
3175            },
3176            output: make_split_output(Some(pass_result_internal), "", ""),
3177            result: pass_result.clone(),
3178            start_time: Local::now().into(),
3179            time_taken: Duration::from_secs(30),
3180            is_slow: true,
3181            delay_before_start: Duration::ZERO,
3182            error_summary: None,
3183            output_error_slice: None,
3184        };
3185
3186        let leak_pass_slow_status = ExecuteStatus {
3187            retry_data: RetryData {
3188                attempt: 1,
3189                total_attempts: 1,
3190            },
3191            output: make_split_output(Some(leak_pass_result_internal), "", ""),
3192            result: leak_pass_result.clone(),
3193            start_time: Local::now().into(),
3194            time_taken: Duration::from_secs(30),
3195            is_slow: true,
3196            delay_before_start: Duration::ZERO,
3197            error_summary: None,
3198            output_error_slice: None,
3199        };
3200
3201        let timeout_pass_slow_status = ExecuteStatus {
3202            retry_data: RetryData {
3203                attempt: 1,
3204                total_attempts: 1,
3205            },
3206            output: make_split_output(Some(timeout_pass_result_internal), "", ""),
3207            result: timeout_pass_result.clone(),
3208            start_time: Local::now().into(),
3209            time_taken: Duration::from_secs(300),
3210            is_slow: true,
3211            delay_before_start: Duration::ZERO,
3212            error_summary: None,
3213            output_error_slice: None,
3214        };
3215
3216        // --- Flaky statuses ---
3217        let flaky_first_status = ExecuteStatus {
3218            retry_data: RetryData {
3219                attempt: 1,
3220                total_attempts: 2,
3221            },
3222            output: make_split_output(Some(fail_result_internal), "", ""),
3223            result: fail_result.clone(),
3224            start_time: Local::now().into(),
3225            time_taken: Duration::from_secs(1),
3226            is_slow: false,
3227            delay_before_start: Duration::ZERO,
3228            error_summary: None,
3229            output_error_slice: None,
3230        };
3231        let flaky_last_status = ExecuteStatus {
3232            retry_data: RetryData {
3233                attempt: 2,
3234                total_attempts: 2,
3235            },
3236            output: make_split_output(Some(pass_result_internal), "", ""),
3237            result: pass_result.clone(),
3238            start_time: Local::now().into(),
3239            time_taken: Duration::from_secs(1),
3240            is_slow: false,
3241            delay_before_start: Duration::ZERO,
3242            error_summary: None,
3243            output_error_slice: None,
3244        };
3245
3246        // --- First-attempt failure statuses ---
3247        let fail_status = ExecuteStatus {
3248            retry_data: RetryData {
3249                attempt: 1,
3250                total_attempts: 1,
3251            },
3252            output: make_split_output(Some(fail_result_internal), "", ""),
3253            result: fail_result.clone(),
3254            start_time: Local::now().into(),
3255            time_taken: Duration::from_secs(1),
3256            is_slow: false,
3257            delay_before_start: Duration::ZERO,
3258            error_summary: None,
3259            output_error_slice: None,
3260        };
3261
3262        let fail_leak_status = ExecuteStatus {
3263            retry_data: RetryData {
3264                attempt: 1,
3265                total_attempts: 1,
3266            },
3267            output: make_split_output(Some(fail_leak_result_internal), "", ""),
3268            result: fail_leak_result.clone(),
3269            start_time: Local::now().into(),
3270            time_taken: Duration::from_secs(1),
3271            is_slow: false,
3272            delay_before_start: Duration::ZERO,
3273            error_summary: None,
3274            output_error_slice: None,
3275        };
3276
3277        let exec_fail_status = ExecuteStatus {
3278            retry_data: RetryData {
3279                attempt: 1,
3280                total_attempts: 1,
3281            },
3282            output: make_split_output(Some(exec_fail_result_internal), "", ""),
3283            result: exec_fail_result.clone(),
3284            start_time: Local::now().into(),
3285            time_taken: Duration::from_secs(1),
3286            is_slow: false,
3287            delay_before_start: Duration::ZERO,
3288            error_summary: None,
3289            output_error_slice: None,
3290        };
3291
3292        let leak_fail_status = ExecuteStatus {
3293            retry_data: RetryData {
3294                attempt: 1,
3295                total_attempts: 1,
3296            },
3297            output: make_split_output(Some(leak_fail_result_internal), "", ""),
3298            result: leak_fail_result.clone(),
3299            start_time: Local::now().into(),
3300            time_taken: Duration::from_secs(1),
3301            is_slow: false,
3302            delay_before_start: Duration::ZERO,
3303            error_summary: None,
3304            output_error_slice: None,
3305        };
3306
3307        let timeout_fail_status = ExecuteStatus {
3308            retry_data: RetryData {
3309                attempt: 1,
3310                total_attempts: 1,
3311            },
3312            output: make_split_output(Some(timeout_fail_result_internal), "", ""),
3313            result: timeout_fail_result.clone(),
3314            start_time: Local::now().into(),
3315            time_taken: Duration::from_secs(60),
3316            is_slow: false,
3317            delay_before_start: Duration::ZERO,
3318            error_summary: None,
3319            output_error_slice: None,
3320        };
3321
3322        let abort_unix_status = ExecuteStatus {
3323            retry_data: RetryData {
3324                attempt: 1,
3325                total_attempts: 1,
3326            },
3327            output: make_split_output(None, "", ""),
3328            result: abort_unix_result.clone(),
3329            start_time: Local::now().into(),
3330            time_taken: Duration::from_secs(1),
3331            is_slow: false,
3332            delay_before_start: Duration::ZERO,
3333            error_summary: None,
3334            output_error_slice: None,
3335        };
3336
3337        let abort_windows_status = ExecuteStatus {
3338            retry_data: RetryData {
3339                attempt: 1,
3340                total_attempts: 1,
3341            },
3342            output: make_split_output(None, "", ""),
3343            result: abort_windows_result.clone(),
3344            start_time: Local::now().into(),
3345            time_taken: Duration::from_secs(1),
3346            is_slow: false,
3347            delay_before_start: Duration::ZERO,
3348            error_summary: None,
3349            output_error_slice: None,
3350        };
3351
3352        // --- Retry failure statuses ---
3353        let fail_retry_status = ExecuteStatus {
3354            retry_data: RetryData {
3355                attempt: 2,
3356                total_attempts: 2,
3357            },
3358            output: make_split_output(Some(fail_result_internal), "", ""),
3359            result: fail_result.clone(),
3360            start_time: Local::now().into(),
3361            time_taken: Duration::from_secs(1),
3362            is_slow: false,
3363            delay_before_start: Duration::ZERO,
3364            error_summary: None,
3365            output_error_slice: None,
3366        };
3367
3368        let fail_leak_retry_status = ExecuteStatus {
3369            retry_data: RetryData {
3370                attempt: 2,
3371                total_attempts: 2,
3372            },
3373            output: make_split_output(Some(fail_leak_result_internal), "", ""),
3374            result: fail_leak_result.clone(),
3375            start_time: Local::now().into(),
3376            time_taken: Duration::from_secs(1),
3377            is_slow: false,
3378            delay_before_start: Duration::ZERO,
3379            error_summary: None,
3380            output_error_slice: None,
3381        };
3382
3383        let leak_fail_retry_status = ExecuteStatus {
3384            retry_data: RetryData {
3385                attempt: 2,
3386                total_attempts: 2,
3387            },
3388            output: make_split_output(Some(leak_fail_result_internal), "", ""),
3389            result: leak_fail_result.clone(),
3390            start_time: Local::now().into(),
3391            time_taken: Duration::from_secs(1),
3392            is_slow: false,
3393            delay_before_start: Duration::ZERO,
3394            error_summary: None,
3395            output_error_slice: None,
3396        };
3397
3398        let timeout_fail_retry_status = ExecuteStatus {
3399            retry_data: RetryData {
3400                attempt: 2,
3401                total_attempts: 2,
3402            },
3403            output: make_split_output(Some(timeout_fail_result_internal), "", ""),
3404            result: timeout_fail_result.clone(),
3405            start_time: Local::now().into(),
3406            time_taken: Duration::from_secs(60),
3407            is_slow: false,
3408            delay_before_start: Duration::ZERO,
3409            error_summary: None,
3410            output_error_slice: None,
3411        };
3412
3413        // --- Build descriptions ---
3414        let pass_describe = ExecutionDescription::Success {
3415            single_status: &pass_status,
3416        };
3417        let leak_pass_describe = ExecutionDescription::Success {
3418            single_status: &leak_pass_status,
3419        };
3420        let timeout_pass_describe = ExecutionDescription::Success {
3421            single_status: &timeout_pass_status,
3422        };
3423        let pass_slow_describe = ExecutionDescription::Success {
3424            single_status: &pass_slow_status,
3425        };
3426        let leak_pass_slow_describe = ExecutionDescription::Success {
3427            single_status: &leak_pass_slow_status,
3428        };
3429        let timeout_pass_slow_describe = ExecutionDescription::Success {
3430            single_status: &timeout_pass_slow_status,
3431        };
3432        let flaky_describe = ExecutionDescription::Flaky {
3433            last_status: &flaky_last_status,
3434            prior_statuses: std::slice::from_ref(&flaky_first_status),
3435            result: FlakyResult::Pass,
3436        };
3437        let flaky_fail_describe = ExecutionDescription::Flaky {
3438            last_status: &flaky_last_status,
3439            prior_statuses: std::slice::from_ref(&flaky_first_status),
3440            result: FlakyResult::Fail,
3441        };
3442        let fail_describe = ExecutionDescription::Failure {
3443            first_status: &fail_status,
3444            last_status: &fail_status,
3445            retries: &[],
3446        };
3447        let fail_leak_describe = ExecutionDescription::Failure {
3448            first_status: &fail_leak_status,
3449            last_status: &fail_leak_status,
3450            retries: &[],
3451        };
3452        let exec_fail_describe = ExecutionDescription::Failure {
3453            first_status: &exec_fail_status,
3454            last_status: &exec_fail_status,
3455            retries: &[],
3456        };
3457        let leak_fail_describe = ExecutionDescription::Failure {
3458            first_status: &leak_fail_status,
3459            last_status: &leak_fail_status,
3460            retries: &[],
3461        };
3462        let timeout_fail_describe = ExecutionDescription::Failure {
3463            first_status: &timeout_fail_status,
3464            last_status: &timeout_fail_status,
3465            retries: &[],
3466        };
3467        let abort_unix_describe = ExecutionDescription::Failure {
3468            first_status: &abort_unix_status,
3469            last_status: &abort_unix_status,
3470            retries: &[],
3471        };
3472        let abort_windows_describe = ExecutionDescription::Failure {
3473            first_status: &abort_windows_status,
3474            last_status: &abort_windows_status,
3475            retries: &[],
3476        };
3477        let fail_retry_describe = ExecutionDescription::Failure {
3478            first_status: &fail_status,
3479            last_status: &fail_retry_status,
3480            retries: std::slice::from_ref(&fail_retry_status),
3481        };
3482        let fail_leak_retry_describe = ExecutionDescription::Failure {
3483            first_status: &fail_leak_status,
3484            last_status: &fail_leak_retry_status,
3485            retries: std::slice::from_ref(&fail_leak_retry_status),
3486        };
3487        let leak_fail_retry_describe = ExecutionDescription::Failure {
3488            first_status: &leak_fail_status,
3489            last_status: &leak_fail_retry_status,
3490            retries: std::slice::from_ref(&leak_fail_retry_status),
3491        };
3492        let timeout_fail_retry_describe = ExecutionDescription::Failure {
3493            first_status: &timeout_fail_status,
3494            last_status: &timeout_fail_retry_status,
3495            retries: std::slice::from_ref(&timeout_fail_retry_status),
3496        };
3497
3498        // Collect all test cases: (label, description).
3499        // The label helps identify each case in the snapshot.
3500        let test_cases: Vec<(&str, ExecutionDescription<'_, LiveSpec>)> = vec![
3501            // Success variants (is_slow = false).
3502            ("pass", pass_describe),
3503            ("leak pass", leak_pass_describe),
3504            ("timeout pass", timeout_pass_describe),
3505            // Success variants (is_slow = true) - only different for Final.
3506            ("pass slow", pass_slow_describe),
3507            ("leak pass slow", leak_pass_slow_describe),
3508            ("timeout pass slow", timeout_pass_slow_describe),
3509            // Flaky variants.
3510            ("flaky", flaky_describe),
3511            ("flaky fail", flaky_fail_describe),
3512            // First-attempt failure variants.
3513            ("fail", fail_describe),
3514            ("fail leak", fail_leak_describe),
3515            ("exec fail", exec_fail_describe),
3516            ("leak fail", leak_fail_describe),
3517            ("timeout fail", timeout_fail_describe),
3518            ("abort unix", abort_unix_describe),
3519            ("abort windows", abort_windows_describe),
3520            // Retry failure variants.
3521            ("fail retry", fail_retry_describe),
3522            ("fail leak retry", fail_leak_retry_describe),
3523            ("leak fail retry", leak_fail_retry_describe),
3524            ("timeout fail retry", timeout_fail_retry_describe),
3525        ];
3526
3527        let mut out = String::new();
3528        let mut counter = 0usize;
3529
3530        with_reporter(
3531            |mut reporter| {
3532                let writer = reporter.output.writer_mut().unwrap();
3533
3534                // Loop over both StatusLineKind variants.
3535                for (kind_name, kind) in [
3536                    ("intermediate", StatusLineKind::Intermediate),
3537                    ("final", StatusLineKind::Final),
3538                ] {
3539                    writeln!(writer, "=== {kind_name} ===").unwrap();
3540
3541                    for (label, describe) in &test_cases {
3542                        counter += 1;
3543                        let test_counter = TestInstanceCounter::Counter {
3544                            current: counter,
3545                            total: 100,
3546                        };
3547
3548                        // Write label as a comment for clarity in snapshot.
3549                        writeln!(writer, "# {label}: ").unwrap();
3550
3551                        reporter
3552                            .inner
3553                            .write_status_line_impl(
3554                                None,
3555                                test_counter,
3556                                test_instance,
3557                                *describe,
3558                                kind,
3559                                writer,
3560                            )
3561                            .unwrap();
3562                    }
3563                }
3564            },
3565            &mut out,
3566        );
3567
3568        insta::assert_snapshot!("status_line_all_variants", out);
3569    }
3570
3571    #[test]
3572    fn test_summary_line() {
3573        let run_id = ReportUuid::nil();
3574        let mut out = String::new();
3575
3576        with_reporter(
3577            |mut reporter| {
3578                // Test single run with all passing tests
3579                let run_stats_success = RunStats {
3580                    initial_run_count: 5,
3581                    finished_count: 5,
3582                    setup_scripts_initial_count: 0,
3583                    setup_scripts_finished_count: 0,
3584                    setup_scripts_passed: 0,
3585                    setup_scripts_failed: 0,
3586                    setup_scripts_exec_failed: 0,
3587                    setup_scripts_timed_out: 0,
3588                    passed: 5,
3589                    passed_slow: 0,
3590                    passed_timed_out: 0,
3591                    flaky: 0,
3592                    failed: 0,
3593                    failed_slow: 0,
3594                    failed_timed_out: 0,
3595                    leaky: 0,
3596                    leaky_failed: 0,
3597                    exec_failed: 0,
3598                    skipped: 0,
3599                    cancel_reason: None,
3600                };
3601
3602                reporter
3603                    .write_event(&TestEvent {
3604                        timestamp: Local::now().into(),
3605                        elapsed: Duration::ZERO,
3606                        kind: TestEventKind::RunFinished {
3607                            run_id,
3608                            start_time: Local::now().into(),
3609                            elapsed: Duration::from_secs(2),
3610                            run_stats: RunFinishedStats::Single(run_stats_success),
3611                            outstanding_not_seen: None,
3612                        },
3613                    })
3614                    .unwrap();
3615
3616                // Test single run with mixed results
3617                let run_stats_mixed = RunStats {
3618                    initial_run_count: 10,
3619                    finished_count: 8,
3620                    setup_scripts_initial_count: 1,
3621                    setup_scripts_finished_count: 1,
3622                    setup_scripts_passed: 1,
3623                    setup_scripts_failed: 0,
3624                    setup_scripts_exec_failed: 0,
3625                    setup_scripts_timed_out: 0,
3626                    passed: 5,
3627                    passed_slow: 1,
3628                    passed_timed_out: 2,
3629                    flaky: 1,
3630                    failed: 2,
3631                    failed_slow: 0,
3632                    failed_timed_out: 1,
3633                    leaky: 1,
3634                    leaky_failed: 0,
3635                    exec_failed: 1,
3636                    skipped: 2,
3637                    cancel_reason: Some(CancelReason::Signal),
3638                };
3639
3640                reporter
3641                    .write_event(&TestEvent {
3642                        timestamp: Local::now().into(),
3643                        elapsed: Duration::ZERO,
3644                        kind: TestEventKind::RunFinished {
3645                            run_id,
3646                            start_time: Local::now().into(),
3647                            elapsed: Duration::from_millis(15750),
3648                            run_stats: RunFinishedStats::Single(run_stats_mixed),
3649                            outstanding_not_seen: None,
3650                        },
3651                    })
3652                    .unwrap();
3653
3654                // Test stress run with success
3655                let stress_stats_success = StressRunStats {
3656                    completed: StressIndex {
3657                        current: 25,
3658                        total: Some(NonZero::new(50).unwrap()),
3659                    },
3660                    success_count: 25,
3661                    failed_count: 0,
3662                    last_final_stats: FinalRunStats::Success,
3663                };
3664
3665                reporter
3666                    .write_event(&TestEvent {
3667                        timestamp: Local::now().into(),
3668                        elapsed: Duration::ZERO,
3669                        kind: TestEventKind::RunFinished {
3670                            run_id,
3671                            start_time: Local::now().into(),
3672                            elapsed: Duration::from_secs(120),
3673                            run_stats: RunFinishedStats::Stress(stress_stats_success),
3674                            outstanding_not_seen: None,
3675                        },
3676                    })
3677                    .unwrap();
3678
3679                // Test stress run with failures and cancellation
3680                let stress_stats_failed = StressRunStats {
3681                    completed: StressIndex {
3682                        current: 15,
3683                        total: None, // Unlimited iterations
3684                    },
3685                    success_count: 12,
3686                    failed_count: 3,
3687                    last_final_stats: FinalRunStats::Cancelled {
3688                        reason: Some(CancelReason::Interrupt),
3689                        kind: RunStatsFailureKind::SetupScript,
3690                    },
3691                };
3692
3693                reporter
3694                    .write_event(&TestEvent {
3695                        timestamp: Local::now().into(),
3696                        elapsed: Duration::ZERO,
3697                        kind: TestEventKind::RunFinished {
3698                            run_id,
3699                            start_time: Local::now().into(),
3700                            elapsed: Duration::from_millis(45250),
3701                            run_stats: RunFinishedStats::Stress(stress_stats_failed),
3702                            outstanding_not_seen: None,
3703                        },
3704                    })
3705                    .unwrap();
3706
3707                // Test no tests run case
3708                let run_stats_empty = RunStats {
3709                    initial_run_count: 0,
3710                    finished_count: 0,
3711                    setup_scripts_initial_count: 0,
3712                    setup_scripts_finished_count: 0,
3713                    setup_scripts_passed: 0,
3714                    setup_scripts_failed: 0,
3715                    setup_scripts_exec_failed: 0,
3716                    setup_scripts_timed_out: 0,
3717                    passed: 0,
3718                    passed_slow: 0,
3719                    passed_timed_out: 0,
3720                    flaky: 0,
3721                    failed: 0,
3722                    failed_slow: 0,
3723                    failed_timed_out: 0,
3724                    leaky: 0,
3725                    leaky_failed: 0,
3726                    exec_failed: 0,
3727                    skipped: 0,
3728                    cancel_reason: None,
3729                };
3730
3731                reporter
3732                    .write_event(&TestEvent {
3733                        timestamp: Local::now().into(),
3734                        elapsed: Duration::ZERO,
3735                        kind: TestEventKind::RunFinished {
3736                            run_id,
3737                            start_time: Local::now().into(),
3738                            elapsed: Duration::from_millis(100),
3739                            run_stats: RunFinishedStats::Single(run_stats_empty),
3740                            outstanding_not_seen: None,
3741                        },
3742                    })
3743                    .unwrap();
3744            },
3745            &mut out,
3746        );
3747
3748        insta::assert_snapshot!("summary_line_output", out,);
3749    }
3750
3751    // ---
3752
3753    /// Send an information response to the reporter and return the output.
3754    #[test]
3755    fn test_info_response() {
3756        let args = vec!["arg1".to_string(), "arg2".to_string()];
3757        let binary_id = RustBinaryId::new("my-binary-id");
3758        let test_name1 = TestCaseName::new("test1");
3759        let test_name2 = TestCaseName::new("test2");
3760        let test_name3 = TestCaseName::new("test3");
3761        let test_name4 = TestCaseName::new("test4");
3762        let test_name5 = TestCaseName::new("test5");
3763
3764        let mut out = String::new();
3765
3766        with_reporter(
3767            |mut reporter| {
3768                // Info started event.
3769                reporter
3770                    .write_event(&TestEvent {
3771                        timestamp: Local::now().into(),
3772                        elapsed: Duration::ZERO,
3773                        kind: TestEventKind::InfoStarted {
3774                            total: 30,
3775                            run_stats: RunStats {
3776                                initial_run_count: 40,
3777                                finished_count: 20,
3778                                setup_scripts_initial_count: 1,
3779                                setup_scripts_finished_count: 1,
3780                                setup_scripts_passed: 1,
3781                                setup_scripts_failed: 0,
3782                                setup_scripts_exec_failed: 0,
3783                                setup_scripts_timed_out: 0,
3784                                passed: 17,
3785                                passed_slow: 4,
3786                                passed_timed_out: 3,
3787                                flaky: 2,
3788                                failed: 2,
3789                                failed_slow: 1,
3790                                failed_timed_out: 1,
3791                                leaky: 1,
3792                                leaky_failed: 2,
3793                                exec_failed: 1,
3794                                skipped: 5,
3795                                cancel_reason: None,
3796                            },
3797                        },
3798                    })
3799                    .unwrap();
3800
3801                // A basic setup script.
3802                reporter
3803                    .write_event(&TestEvent {
3804                        timestamp: Local::now().into(),
3805                        elapsed: Duration::ZERO,
3806                        kind: TestEventKind::InfoResponse {
3807                            index: 0,
3808                            total: 21,
3809                            // Technically, you won't get setup script and test responses in the
3810                            // same response, but it's easiest to test in this manner.
3811                            response: InfoResponse::SetupScript(SetupScriptInfoResponse {
3812                                stress_index: None,
3813                                script_id: ScriptId::new(SmolStr::new("setup")).unwrap(),
3814                                program: "setup".to_owned(),
3815                                args: args.clone(),
3816                                state: UnitState::Running {
3817                                    pid: 4567,
3818                                    time_taken: Duration::from_millis(1234),
3819                                    slow_after: None,
3820                                },
3821                                output: make_split_output(
3822                                    None,
3823                                    "script stdout 1",
3824                                    "script stderr 1",
3825                                ),
3826                            }),
3827                        },
3828                    })
3829                    .unwrap();
3830
3831                // A setup script with a slow warning, combined output, and an
3832                // execution failure.
3833                reporter
3834                    .write_event(&TestEvent {
3835                        timestamp: Local::now().into(),
3836                        elapsed: Duration::ZERO,
3837                        kind: TestEventKind::InfoResponse {
3838                            index: 1,
3839                            total: 21,
3840                            response: InfoResponse::SetupScript(SetupScriptInfoResponse {
3841                                stress_index: None,
3842                                script_id: ScriptId::new(SmolStr::new("setup-slow")).unwrap(),
3843                                program: "setup-slow".to_owned(),
3844                                args: args.clone(),
3845                                state: UnitState::Running {
3846                                    pid: 4568,
3847                                    time_taken: Duration::from_millis(1234),
3848                                    slow_after: Some(Duration::from_millis(1000)),
3849                                },
3850                                output: make_combined_output_with_errors(
3851                                    None,
3852                                    "script output 2\n",
3853                                    vec![ChildError::Fd(ChildFdError::ReadStdout(Arc::new(
3854                                        std::io::Error::other("read stdout error"),
3855                                    )))],
3856                                ),
3857                            }),
3858                        },
3859                    })
3860                    .unwrap();
3861
3862                // A setup script that's terminating and has multiple errors.
3863                reporter
3864                    .write_event(&TestEvent {
3865                        timestamp: Local::now().into(),
3866                        elapsed: Duration::ZERO,
3867                        kind: TestEventKind::InfoResponse {
3868                            index: 2,
3869                            total: 21,
3870                            response: InfoResponse::SetupScript(SetupScriptInfoResponse {
3871                                stress_index: None,
3872                                script_id: ScriptId::new(SmolStr::new("setup-terminating"))
3873                                    .unwrap(),
3874                                program: "setup-terminating".to_owned(),
3875                                args: args.clone(),
3876                                state: UnitState::Terminating(UnitTerminatingState {
3877                                    pid: 5094,
3878                                    time_taken: Duration::from_millis(1234),
3879                                    reason: UnitTerminateReason::Signal,
3880                                    method: UnitTerminateMethod::Fake,
3881                                    waiting_duration: Duration::from_millis(6789),
3882                                    remaining: Duration::from_millis(9786),
3883                                }),
3884                                output: make_split_output_with_errors(
3885                                    None,
3886                                    "script output 3\n",
3887                                    "script stderr 3\n",
3888                                    vec![
3889                                        ChildError::Fd(ChildFdError::ReadStdout(Arc::new(
3890                                            std::io::Error::other("read stdout error"),
3891                                        ))),
3892                                        ChildError::Fd(ChildFdError::ReadStderr(Arc::new(
3893                                            std::io::Error::other("read stderr error"),
3894                                        ))),
3895                                    ],
3896                                ),
3897                            }),
3898                        },
3899                    })
3900                    .unwrap();
3901
3902                // A setup script that's about to exit along with a start error
3903                // (this is not a real situation but we're just testing out
3904                // various cases).
3905                reporter
3906                    .write_event(&TestEvent {
3907                        timestamp: Local::now().into(),
3908                        elapsed: Duration::ZERO,
3909                        kind: TestEventKind::InfoResponse {
3910                            index: 3,
3911                            total: 21,
3912                            response: InfoResponse::SetupScript(SetupScriptInfoResponse {
3913                                stress_index: Some(StressIndex {
3914                                    current: 0,
3915                                    total: None,
3916                                }),
3917                                script_id: ScriptId::new(SmolStr::new("setup-exiting")).unwrap(),
3918                                program: "setup-exiting".to_owned(),
3919                                args: args.clone(),
3920                                state: UnitState::Exiting {
3921                                    pid: 9987,
3922                                    time_taken: Duration::from_millis(1234),
3923                                    slow_after: Some(Duration::from_millis(1000)),
3924                                    // Even if exit_status is 0, the presence of
3925                                    // exec-fail errors should be considered
3926                                    // part of the output.
3927                                    tentative_result: Some(ExecutionResultDescription::ExecFail),
3928                                    waiting_duration: Duration::from_millis(10467),
3929                                    remaining: Duration::from_millis(335),
3930                                },
3931                                output: ChildExecutionOutput::StartError(ChildStartError::Spawn(
3932                                    Arc::new(std::io::Error::other("exec error")),
3933                                ))
3934                                .into(),
3935                            }),
3936                        },
3937                    })
3938                    .unwrap();
3939
3940                // A setup script that has exited.
3941                reporter
3942                    .write_event(&TestEvent {
3943                        timestamp: Local::now().into(),
3944                        elapsed: Duration::ZERO,
3945                        kind: TestEventKind::InfoResponse {
3946                            index: 4,
3947                            total: 21,
3948                            response: InfoResponse::SetupScript(SetupScriptInfoResponse {
3949                                stress_index: Some(StressIndex {
3950                                    current: 1,
3951                                    total: Some(NonZero::new(3).unwrap()),
3952                                }),
3953                                script_id: ScriptId::new(SmolStr::new("setup-exited")).unwrap(),
3954                                program: "setup-exited".to_owned(),
3955                                args: args.clone(),
3956                                state: UnitState::Exited {
3957                                    result: ExecutionResultDescription::Fail {
3958                                        failure: FailureDescription::ExitCode { code: 1 },
3959                                        leaked: true,
3960                                    },
3961                                    time_taken: Duration::from_millis(9999),
3962                                    slow_after: Some(Duration::from_millis(3000)),
3963                                },
3964                                output: ChildExecutionOutput::StartError(ChildStartError::Spawn(
3965                                    Arc::new(std::io::Error::other("exec error")),
3966                                ))
3967                                .into(),
3968                            }),
3969                        },
3970                    })
3971                    .unwrap();
3972
3973                // A test is running.
3974                reporter
3975                    .write_event(&TestEvent {
3976                        timestamp: Local::now().into(),
3977                        elapsed: Duration::ZERO,
3978                        kind: TestEventKind::InfoResponse {
3979                            index: 5,
3980                            total: 21,
3981                            response: InfoResponse::Test(TestInfoResponse {
3982                                stress_index: None,
3983                                test_instance: TestInstanceId {
3984                                    binary_id: &binary_id,
3985                                    test_name: &test_name1,
3986                                },
3987                                retry_data: RetryData {
3988                                    attempt: 1,
3989                                    total_attempts: 1,
3990                                },
3991                                state: UnitState::Running {
3992                                    pid: 12345,
3993                                    time_taken: Duration::from_millis(400),
3994                                    slow_after: None,
3995                                },
3996                                output: make_split_output(None, "abc", "def"),
3997                            }),
3998                        },
3999                    })
4000                    .unwrap();
4001
4002                // A test is being terminated due to a timeout.
4003                reporter
4004                    .write_event(&TestEvent {
4005                        timestamp: Local::now().into(),
4006                        elapsed: Duration::ZERO,
4007                        kind: TestEventKind::InfoResponse {
4008                            index: 6,
4009                            total: 21,
4010                            response: InfoResponse::Test(TestInfoResponse {
4011                                stress_index: Some(StressIndex {
4012                                    current: 0,
4013                                    total: None,
4014                                }),
4015                                test_instance: TestInstanceId {
4016                                    binary_id: &binary_id,
4017                                    test_name: &test_name2,
4018                                },
4019                                retry_data: RetryData {
4020                                    attempt: 2,
4021                                    total_attempts: 3,
4022                                },
4023                                state: UnitState::Terminating(UnitTerminatingState {
4024                                    pid: 12346,
4025                                    time_taken: Duration::from_millis(99999),
4026                                    reason: UnitTerminateReason::Timeout,
4027                                    method: UnitTerminateMethod::Fake,
4028                                    waiting_duration: Duration::from_millis(6789),
4029                                    remaining: Duration::from_millis(9786),
4030                                }),
4031                                output: make_split_output(None, "abc", "def"),
4032                            }),
4033                        },
4034                    })
4035                    .unwrap();
4036
4037                // A test is exiting.
4038                reporter
4039                    .write_event(&TestEvent {
4040                        timestamp: Local::now().into(),
4041                        elapsed: Duration::ZERO,
4042                        kind: TestEventKind::InfoResponse {
4043                            index: 7,
4044                            total: 21,
4045                            response: InfoResponse::Test(TestInfoResponse {
4046                                stress_index: None,
4047                                test_instance: TestInstanceId {
4048                                    binary_id: &binary_id,
4049                                    test_name: &test_name3,
4050                                },
4051                                retry_data: RetryData {
4052                                    attempt: 2,
4053                                    total_attempts: 3,
4054                                },
4055                                state: UnitState::Exiting {
4056                                    pid: 99999,
4057                                    time_taken: Duration::from_millis(99999),
4058                                    slow_after: Some(Duration::from_millis(33333)),
4059                                    tentative_result: None,
4060                                    waiting_duration: Duration::from_millis(1),
4061                                    remaining: Duration::from_millis(999),
4062                                },
4063                                output: make_split_output(None, "abc", "def"),
4064                            }),
4065                        },
4066                    })
4067                    .unwrap();
4068
4069                // A test has exited.
4070                reporter
4071                    .write_event(&TestEvent {
4072                        timestamp: Local::now().into(),
4073                        elapsed: Duration::ZERO,
4074                        kind: TestEventKind::InfoResponse {
4075                            index: 8,
4076                            total: 21,
4077                            response: InfoResponse::Test(TestInfoResponse {
4078                                stress_index: Some(StressIndex {
4079                                    current: 1,
4080                                    total: Some(NonZero::new(3).unwrap()),
4081                                }),
4082                                test_instance: TestInstanceId {
4083                                    binary_id: &binary_id,
4084                                    test_name: &test_name4,
4085                                },
4086                                retry_data: RetryData {
4087                                    attempt: 1,
4088                                    total_attempts: 5,
4089                                },
4090                                state: UnitState::Exited {
4091                                    result: ExecutionResultDescription::Pass,
4092                                    time_taken: Duration::from_millis(99999),
4093                                    slow_after: Some(Duration::from_millis(33333)),
4094                                },
4095                                output: make_combined_output_with_errors(
4096                                    Some(ExecutionResult::Pass),
4097                                    "abc\ndef\nghi\n",
4098                                    vec![ChildError::Fd(ChildFdError::Wait(Arc::new(
4099                                        std::io::Error::other("error waiting"),
4100                                    )))],
4101                                ),
4102                            }),
4103                        },
4104                    })
4105                    .unwrap();
4106
4107                // Delay before next attempt.
4108                reporter
4109                    .write_event(&TestEvent {
4110                        timestamp: Local::now().into(),
4111                        elapsed: Duration::ZERO,
4112                        kind: TestEventKind::InfoResponse {
4113                            index: 9,
4114                            total: 21,
4115                            response: InfoResponse::Test(TestInfoResponse {
4116                                stress_index: None,
4117                                test_instance: TestInstanceId {
4118                                    binary_id: &binary_id,
4119                                    test_name: &test_name4,
4120                                },
4121                                retry_data: RetryData {
4122                                    // Note that even though attempt is 1, we
4123                                    // still show it in the UI in this special
4124                                    // case.
4125                                    attempt: 1,
4126                                    total_attempts: 5,
4127                                },
4128                                state: UnitState::DelayBeforeNextAttempt {
4129                                    previous_result: ExecutionResultDescription::ExecFail,
4130                                    previous_slow: true,
4131                                    waiting_duration: Duration::from_millis(1234),
4132                                    remaining: Duration::from_millis(5678),
4133                                },
4134                                // In reality, the output isn't available at this point,
4135                                // and it shouldn't be shown.
4136                                output: make_combined_output_with_errors(
4137                                    Some(ExecutionResult::Pass),
4138                                    "*** THIS OUTPUT SHOULD BE IGNORED",
4139                                    vec![ChildError::Fd(ChildFdError::Wait(Arc::new(
4140                                        std::io::Error::other(
4141                                            "*** THIS ERROR SHOULD ALSO BE IGNORED",
4142                                        ),
4143                                    )))],
4144                                ),
4145                            }),
4146                        },
4147                    })
4148                    .unwrap();
4149
4150                // A test that was aborted by a signal and leaked handles.
4151                reporter
4152                    .write_event(&TestEvent {
4153                        timestamp: Local::now().into(),
4154                        elapsed: Duration::ZERO,
4155                        kind: TestEventKind::InfoResponse {
4156                            index: 10,
4157                            total: 21,
4158                            response: InfoResponse::Test(TestInfoResponse {
4159                                stress_index: None,
4160                                test_instance: TestInstanceId {
4161                                    binary_id: &binary_id,
4162                                    test_name: &test_name5,
4163                                },
4164                                retry_data: RetryData {
4165                                    attempt: 1,
4166                                    total_attempts: 1,
4167                                },
4168                                state: UnitState::Exited {
4169                                    result: ExecutionResultDescription::Fail {
4170                                        failure: FailureDescription::Abort {
4171                                            abort: AbortDescription::UnixSignal {
4172                                                signal: 11,
4173                                                name: Some("SEGV".into()),
4174                                            },
4175                                        },
4176                                        leaked: true,
4177                                    },
4178                                    time_taken: Duration::from_millis(5678),
4179                                    slow_after: None,
4180                                },
4181                                output: make_split_output(None, "segfault output", ""),
4182                            }),
4183                        },
4184                    })
4185                    .unwrap();
4186
4187                reporter
4188                    .write_event(&TestEvent {
4189                        timestamp: Local::now().into(),
4190                        elapsed: Duration::ZERO,
4191                        kind: TestEventKind::InfoFinished { missing: 2 },
4192                    })
4193                    .unwrap();
4194            },
4195            &mut out,
4196        );
4197
4198        insta::assert_snapshot!("info_response_output", out,);
4199    }
4200
4201    #[test]
4202    fn verbose_command_line() {
4203        let binary_id = RustBinaryId::new("my-binary-id");
4204        let test_name = TestCaseName::new("test_name");
4205        let test_with_spaces = TestCaseName::new("test_with_spaces");
4206        let test_special_chars = TestCaseName::new("test_special_chars");
4207        let test_retry = TestCaseName::new("test_retry");
4208        let mut out = String::new();
4209
4210        with_verbose_reporter(
4211            |mut reporter| {
4212                let current_stats = RunStats {
4213                    initial_run_count: 10,
4214                    finished_count: 0,
4215                    ..Default::default()
4216                };
4217
4218                // Test a simple command.
4219                reporter
4220                    .write_event(&TestEvent {
4221                        timestamp: Local::now().into(),
4222                        elapsed: Duration::ZERO,
4223                        kind: TestEventKind::TestStarted {
4224                            stress_index: None,
4225                            test_instance: TestInstanceId {
4226                                binary_id: &binary_id,
4227                                test_name: &test_name,
4228                            },
4229                            slot_assignment: global_slot_assignment(0),
4230                            current_stats,
4231                            running: 1,
4232                            command_line: vec![
4233                                "/path/to/binary".to_string(),
4234                                "--exact".to_string(),
4235                                "test_name".to_string(),
4236                            ],
4237                        },
4238                    })
4239                    .unwrap();
4240
4241                // Test a command with arguments that need quoting.
4242                reporter
4243                    .write_event(&TestEvent {
4244                        timestamp: Local::now().into(),
4245                        elapsed: Duration::ZERO,
4246                        kind: TestEventKind::TestStarted {
4247                            stress_index: None,
4248                            test_instance: TestInstanceId {
4249                                binary_id: &binary_id,
4250                                test_name: &test_with_spaces,
4251                            },
4252                            slot_assignment: global_slot_assignment(1),
4253                            current_stats,
4254                            running: 2,
4255                            command_line: vec![
4256                                "/path/to/binary".to_string(),
4257                                "--exact".to_string(),
4258                                "test with spaces".to_string(),
4259                                "--flag=value".to_string(),
4260                            ],
4261                        },
4262                    })
4263                    .unwrap();
4264
4265                // Test a command with special characters.
4266                reporter
4267                    .write_event(&TestEvent {
4268                        timestamp: Local::now().into(),
4269                        elapsed: Duration::ZERO,
4270                        kind: TestEventKind::TestStarted {
4271                            stress_index: None,
4272                            test_instance: TestInstanceId {
4273                                binary_id: &binary_id,
4274                                test_name: &test_special_chars,
4275                            },
4276                            slot_assignment: global_slot_assignment(2),
4277                            current_stats,
4278                            running: 3,
4279                            command_line: vec![
4280                                "/path/to/binary".to_string(),
4281                                "test\"with\"quotes".to_string(),
4282                                "test'with'single".to_string(),
4283                            ],
4284                        },
4285                    })
4286                    .unwrap();
4287
4288                // Test a retry (attempt 2) - should show "TRY 2 START".
4289                reporter
4290                    .write_event(&TestEvent {
4291                        timestamp: Local::now().into(),
4292                        elapsed: Duration::ZERO,
4293                        kind: TestEventKind::TestRetryStarted {
4294                            stress_index: None,
4295                            test_instance: TestInstanceId {
4296                                binary_id: &binary_id,
4297                                test_name: &test_retry,
4298                            },
4299                            slot_assignment: global_slot_assignment(0),
4300                            retry_data: RetryData {
4301                                attempt: 2,
4302                                total_attempts: 3,
4303                            },
4304                            running: 1,
4305                            command_line: vec![
4306                                "/path/to/binary".to_string(),
4307                                "--exact".to_string(),
4308                                "test_retry".to_string(),
4309                            ],
4310                        },
4311                    })
4312                    .unwrap();
4313
4314                // Test a retry (attempt 3) - should show "TRY 3 START".
4315                reporter
4316                    .write_event(&TestEvent {
4317                        timestamp: Local::now().into(),
4318                        elapsed: Duration::ZERO,
4319                        kind: TestEventKind::TestRetryStarted {
4320                            stress_index: None,
4321                            test_instance: TestInstanceId {
4322                                binary_id: &binary_id,
4323                                test_name: &test_retry,
4324                            },
4325                            slot_assignment: global_slot_assignment(0),
4326                            retry_data: RetryData {
4327                                attempt: 3,
4328                                total_attempts: 3,
4329                            },
4330                            running: 1,
4331                            command_line: vec![
4332                                "/path/to/binary".to_string(),
4333                                "--exact".to_string(),
4334                                "test_retry".to_string(),
4335                            ],
4336                        },
4337                    })
4338                    .unwrap();
4339            },
4340            &mut out,
4341        );
4342
4343        insta::assert_snapshot!("verbose_command_line", out);
4344    }
4345
4346    #[test]
4347    fn no_capture_settings() {
4348        // Ensure that output settings are ignored with no-capture.
4349        let mut out = String::new();
4350
4351        with_reporter(
4352            |reporter| {
4353                assert!(reporter.inner.no_capture, "no_capture is true");
4354                let overrides = reporter.inner.unit_output.overrides();
4355                assert_eq!(
4356                    overrides.force_failure_output,
4357                    Some(TestOutputDisplay::Never),
4358                    "failure output is never, overriding other settings"
4359                );
4360                assert_eq!(
4361                    overrides.force_success_output,
4362                    Some(TestOutputDisplay::Never),
4363                    "success output is never, overriding other settings"
4364                );
4365                assert_eq!(
4366                    reporter.inner.status_levels.status_level,
4367                    StatusLevel::Pass,
4368                    "status level is pass, overriding other settings"
4369                );
4370            },
4371            &mut out,
4372        );
4373    }
4374
4375    /// Writes the canonical set of TestSlow events to the reporter.
4376    ///
4377    /// Covers all interesting combinations:
4378    /// - `!will_terminate`: attempt 1/1, attempt 1/3, attempt 2/3, attempt 3/3
4379    /// - `will_terminate` (single attempt): attempt 1/1
4380    /// - `will_terminate` (non-last): attempt 1/3, attempt 2/3
4381    /// - `will_terminate` (last): attempt 3/3
4382    fn write_test_slow_events<'a>(
4383        reporter: &mut DisplayReporter<'a>,
4384        binary_id: &'a RustBinaryId,
4385        test_name: &'a TestCaseName,
4386    ) {
4387        // First attempt, single attempt total.
4388        reporter
4389            .write_event(&TestEvent {
4390                timestamp: Local::now().into(),
4391                elapsed: Duration::ZERO,
4392                kind: TestEventKind::TestSlow {
4393                    stress_index: None,
4394                    test_instance: TestInstanceId {
4395                        binary_id,
4396                        test_name,
4397                    },
4398                    retry_data: RetryData {
4399                        attempt: 1,
4400                        total_attempts: 1,
4401                    },
4402                    elapsed: Duration::from_secs(60),
4403                    will_terminate: false,
4404                },
4405            })
4406            .unwrap();
4407
4408        // First attempt, multiple attempts total.
4409        reporter
4410            .write_event(&TestEvent {
4411                timestamp: Local::now().into(),
4412                elapsed: Duration::ZERO,
4413                kind: TestEventKind::TestSlow {
4414                    stress_index: None,
4415                    test_instance: TestInstanceId {
4416                        binary_id,
4417                        test_name,
4418                    },
4419                    retry_data: RetryData {
4420                        attempt: 1,
4421                        total_attempts: 3,
4422                    },
4423                    elapsed: Duration::from_secs(60),
4424                    will_terminate: false,
4425                },
4426            })
4427            .unwrap();
4428
4429        // Second attempt.
4430        reporter
4431            .write_event(&TestEvent {
4432                timestamp: Local::now().into(),
4433                elapsed: Duration::ZERO,
4434                kind: TestEventKind::TestSlow {
4435                    stress_index: None,
4436                    test_instance: TestInstanceId {
4437                        binary_id,
4438                        test_name,
4439                    },
4440                    retry_data: RetryData {
4441                        attempt: 2,
4442                        total_attempts: 3,
4443                    },
4444                    elapsed: Duration::from_secs(60),
4445                    will_terminate: false,
4446                },
4447            })
4448            .unwrap();
4449
4450        // Third attempt.
4451        reporter
4452            .write_event(&TestEvent {
4453                timestamp: Local::now().into(),
4454                elapsed: Duration::ZERO,
4455                kind: TestEventKind::TestSlow {
4456                    stress_index: None,
4457                    test_instance: TestInstanceId {
4458                        binary_id,
4459                        test_name,
4460                    },
4461                    retry_data: RetryData {
4462                        attempt: 3,
4463                        total_attempts: 3,
4464                    },
4465                    elapsed: Duration::from_secs(60),
4466                    will_terminate: false,
4467                },
4468            })
4469            .unwrap();
4470
4471        // will_terminate on single attempt (required_status_level is Fail).
4472        // This exercises the `total_attempts > 1` guard in the TRY N
4473        // prefix logic: at Slow and above, the output should be
4474        // "TERMINATING" (not "TRY 1 TRMNTG") because there's only one
4475        // attempt.
4476        reporter
4477            .write_event(&TestEvent {
4478                timestamp: Local::now().into(),
4479                elapsed: Duration::ZERO,
4480                kind: TestEventKind::TestSlow {
4481                    stress_index: None,
4482                    test_instance: TestInstanceId {
4483                        binary_id,
4484                        test_name,
4485                    },
4486                    retry_data: RetryData {
4487                        attempt: 1,
4488                        total_attempts: 1,
4489                    },
4490                    elapsed: Duration::from_secs(120),
4491                    will_terminate: true,
4492                },
4493            })
4494            .unwrap();
4495
4496        // will_terminate on first attempt with retries (non-last, so
4497        // required_status_level is Retry).
4498        reporter
4499            .write_event(&TestEvent {
4500                timestamp: Local::now().into(),
4501                elapsed: Duration::ZERO,
4502                kind: TestEventKind::TestSlow {
4503                    stress_index: None,
4504                    test_instance: TestInstanceId {
4505                        binary_id,
4506                        test_name,
4507                    },
4508                    retry_data: RetryData {
4509                        attempt: 1,
4510                        total_attempts: 3,
4511                    },
4512                    elapsed: Duration::from_secs(120),
4513                    will_terminate: true,
4514                },
4515            })
4516            .unwrap();
4517
4518        // will_terminate on non-last retry (required_status_level is Retry).
4519        reporter
4520            .write_event(&TestEvent {
4521                timestamp: Local::now().into(),
4522                elapsed: Duration::ZERO,
4523                kind: TestEventKind::TestSlow {
4524                    stress_index: None,
4525                    test_instance: TestInstanceId {
4526                        binary_id,
4527                        test_name,
4528                    },
4529                    retry_data: RetryData {
4530                        attempt: 2,
4531                        total_attempts: 3,
4532                    },
4533                    elapsed: Duration::from_secs(120),
4534                    will_terminate: true,
4535                },
4536            })
4537            .unwrap();
4538
4539        // will_terminate on last attempt (required_status_level is Fail).
4540        reporter
4541            .write_event(&TestEvent {
4542                timestamp: Local::now().into(),
4543                elapsed: Duration::ZERO,
4544                kind: TestEventKind::TestSlow {
4545                    stress_index: None,
4546                    test_instance: TestInstanceId {
4547                        binary_id,
4548                        test_name,
4549                    },
4550                    retry_data: RetryData {
4551                        attempt: 3,
4552                        total_attempts: 3,
4553                    },
4554                    elapsed: Duration::from_secs(120),
4555                    will_terminate: true,
4556                },
4557            })
4558            .unwrap();
4559    }
4560
4561    /// Writes the canonical set of SetupScriptSlow events to the reporter.
4562    ///
4563    /// Setup scripts don't have retries, so the combinations are simpler:
4564    /// - `!will_terminate`: displayed at Slow and above.
4565    /// - `will_terminate`: displayed at Fail and above.
4566    fn write_setup_script_slow_events(reporter: &mut DisplayReporter<'_>) {
4567        // Slow but not terminating.
4568        reporter
4569            .write_event(&TestEvent {
4570                timestamp: Local::now().into(),
4571                elapsed: Duration::ZERO,
4572                kind: TestEventKind::SetupScriptSlow {
4573                    stress_index: None,
4574                    script_id: ScriptId::new(SmolStr::new("my-script")).unwrap(),
4575                    program: "my-program".to_owned(),
4576                    args: vec!["--arg1".to_owned()],
4577                    elapsed: Duration::from_secs(60),
4578                    will_terminate: false,
4579                },
4580            })
4581            .unwrap();
4582
4583        // Slow and about to be terminated.
4584        reporter
4585            .write_event(&TestEvent {
4586                timestamp: Local::now().into(),
4587                elapsed: Duration::ZERO,
4588                kind: TestEventKind::SetupScriptSlow {
4589                    stress_index: None,
4590                    script_id: ScriptId::new(SmolStr::new("my-script")).unwrap(),
4591                    program: "my-program".to_owned(),
4592                    args: vec!["--arg1".to_owned()],
4593                    elapsed: Duration::from_secs(120),
4594                    will_terminate: true,
4595                },
4596            })
4597            .unwrap();
4598    }
4599
4600    /// Tests that TestSlow and SetupScriptSlow events are displayed correctly
4601    /// at each status level. Each level produces a different subset of events.
4602    ///
4603    /// The hierarchy for slow events is:
4604    /// - StatusLevel::None: nothing displayed
4605    /// - StatusLevel::Fail: will_terminate on last/single attempt (Fail-level)
4606    ///   for tests, will_terminate for setup scripts
4607    /// - StatusLevel::Retry: will_terminate events (both last and non-last)
4608    ///   for tests, same as Fail for setup scripts (no retries)
4609    /// - StatusLevel::Slow and above: all events
4610    #[test_case(StatusLevel::None; "none")]
4611    #[test_case(StatusLevel::Fail; "fail")]
4612    #[test_case(StatusLevel::Retry; "retry")]
4613    #[test_case(StatusLevel::Slow; "slow")]
4614    #[test_case(StatusLevel::Pass; "pass")]
4615    fn test_slow_status_levels(status_level: StatusLevel) {
4616        let binary_id = RustBinaryId::new("my-binary-id");
4617        let test_name = TestCaseName::new("test_name");
4618        let mut out = String::new();
4619
4620        with_reporter_at_status_level(
4621            |mut reporter| {
4622                write_test_slow_events(&mut reporter, &binary_id, &test_name);
4623                write_setup_script_slow_events(&mut reporter);
4624            },
4625            &mut out,
4626            status_level,
4627        );
4628
4629        // The test_case label (e.g. "none", "fail") is used by insta as
4630        // the snapshot suffix via the function name.
4631        let label = match status_level {
4632            StatusLevel::None => "none",
4633            StatusLevel::Fail => "fail",
4634            StatusLevel::Retry => "retry",
4635            StatusLevel::Slow => "slow",
4636            StatusLevel::Pass => "pass",
4637            _ => unreachable!("test only covers these levels"),
4638        };
4639        insta::assert_snapshot!(format!("test_slow_status_level_{label}"), out);
4640    }
4641
4642    #[test]
4643    fn sort_final_outputs_counter_flag() {
4644        // Sorting the same entries with and without the counter flag should
4645        // produce different orders when counter order and instance order
4646        // diverge. The fourth entry shares a counter value with the third,
4647        // exercising the instance tiebreaker within the same counter.
4648        let binary_a = RustBinaryId::new("aaa");
4649        let binary_b = RustBinaryId::new("bbb");
4650        let test_x = TestCaseName::new("test_x");
4651        let test_y = TestCaseName::new("test_y");
4652
4653        let mut entries = vec![
4654            FinalOutputEntry {
4655                stress_index: None,
4656                counter: TestInstanceCounter::Counter {
4657                    current: 99,
4658                    total: 100,
4659                },
4660                instance: TestInstanceId {
4661                    binary_id: &binary_b,
4662                    test_name: &test_y,
4663                },
4664                output: make_pass_output(),
4665            },
4666            FinalOutputEntry {
4667                stress_index: None,
4668                counter: TestInstanceCounter::Counter {
4669                    current: 1,
4670                    total: 100,
4671                },
4672                instance: TestInstanceId {
4673                    binary_id: &binary_a,
4674                    test_name: &test_y,
4675                },
4676                output: make_pass_output(),
4677            },
4678            FinalOutputEntry {
4679                stress_index: None,
4680                counter: TestInstanceCounter::Counter {
4681                    current: 50,
4682                    total: 100,
4683                },
4684                instance: TestInstanceId {
4685                    binary_id: &binary_a,
4686                    test_name: &test_x,
4687                },
4688                output: make_pass_output(),
4689            },
4690            FinalOutputEntry {
4691                stress_index: None,
4692                counter: TestInstanceCounter::Counter {
4693                    current: 50,
4694                    total: 100,
4695                },
4696                instance: TestInstanceId {
4697                    binary_id: &binary_b,
4698                    test_name: &test_x,
4699                },
4700                output: make_pass_output(),
4701            },
4702        ];
4703
4704        // Without the counter being shown, sort purely by instance.
4705        sort_final_outputs(&mut entries, false);
4706        assert_eq!(
4707            extract_ids(&entries),
4708            vec![
4709                ("aaa", "test_x"),
4710                ("aaa", "test_y"),
4711                ("bbb", "test_x"),
4712                ("bbb", "test_y"),
4713            ],
4714            "without counter, sort is purely by instance"
4715        );
4716
4717        // With the counter being shown, sort by counter first (1, 50, 50, 99),
4718        // then by instance within the same counter value.
4719        sort_final_outputs(&mut entries, true);
4720        assert_eq!(
4721            extract_ids(&entries),
4722            vec![
4723                ("aaa", "test_y"),
4724                ("aaa", "test_x"),
4725                ("bbb", "test_x"),
4726                ("bbb", "test_y"),
4727            ],
4728            "with counter, sort by counter first, then by instance as tiebreaker"
4729        );
4730    }
4731
4732    #[test]
4733    fn sort_final_outputs_mixed_status_levels() {
4734        // Status level is the primary sort key regardless of counter setting.
4735        // Reverse ordering: skip (highest level) first, pass, fail (lowest
4736        // level) last.
4737        let binary_a = RustBinaryId::new("aaa");
4738        let binary_b = RustBinaryId::new("bbb");
4739        let binary_c = RustBinaryId::new("ccc");
4740        let test_1 = TestCaseName::new("test_1");
4741
4742        let mut entries = vec![
4743            FinalOutputEntry {
4744                stress_index: None,
4745                counter: TestInstanceCounter::Counter {
4746                    current: 1,
4747                    total: 100,
4748                },
4749                instance: TestInstanceId {
4750                    binary_id: &binary_a,
4751                    test_name: &test_1,
4752                },
4753                output: make_fail_output(),
4754            },
4755            FinalOutputEntry {
4756                stress_index: None,
4757                counter: TestInstanceCounter::Counter {
4758                    current: 2,
4759                    total: 100,
4760                },
4761                instance: TestInstanceId {
4762                    binary_id: &binary_b,
4763                    test_name: &test_1,
4764                },
4765                output: make_skip_output(),
4766            },
4767            FinalOutputEntry {
4768                stress_index: None,
4769                counter: TestInstanceCounter::Counter {
4770                    current: 3,
4771                    total: 100,
4772                },
4773                instance: TestInstanceId {
4774                    binary_id: &binary_c,
4775                    test_name: &test_1,
4776                },
4777                output: make_pass_output(),
4778            },
4779        ];
4780
4781        // Pass first, then Skip, then Fail last.
4782        sort_final_outputs(&mut entries, false);
4783        assert_eq!(
4784            extract_ids(&entries),
4785            vec![("ccc", "test_1"), ("bbb", "test_1"), ("aaa", "test_1")],
4786            "pass first, then skip, then fail (reversed status level)"
4787        );
4788
4789        // Shuffle and re-sort with the counter. This results in the same order
4790        // since the status level is more important than the counter.
4791        entries.swap(0, 2);
4792        sort_final_outputs(&mut entries, true);
4793        assert_eq!(
4794            extract_ids(&entries),
4795            vec![("ccc", "test_1"), ("bbb", "test_1"), ("aaa", "test_1")],
4796            "with counter, status level still dominates"
4797        );
4798    }
4799
4800    #[test]
4801    fn sort_final_outputs_stress_indexes() {
4802        // Stress index is the secondary sort key after status level.
4803        let binary_a = RustBinaryId::new("aaa");
4804        let test_1 = TestCaseName::new("test_1");
4805        let test_2 = TestCaseName::new("test_2");
4806
4807        let mut entries = vec![
4808            FinalOutputEntry {
4809                stress_index: Some(StressIndex {
4810                    current: 2,
4811                    total: None,
4812                }),
4813                counter: TestInstanceCounter::Counter {
4814                    current: 1,
4815                    total: 100,
4816                },
4817                instance: TestInstanceId {
4818                    binary_id: &binary_a,
4819                    test_name: &test_1,
4820                },
4821                output: make_pass_output(),
4822            },
4823            FinalOutputEntry {
4824                stress_index: Some(StressIndex {
4825                    current: 0,
4826                    total: None,
4827                }),
4828                counter: TestInstanceCounter::Counter {
4829                    current: 3,
4830                    total: 100,
4831                },
4832                instance: TestInstanceId {
4833                    binary_id: &binary_a,
4834                    test_name: &test_2,
4835                },
4836                output: make_pass_output(),
4837            },
4838            FinalOutputEntry {
4839                stress_index: Some(StressIndex {
4840                    current: 0,
4841                    total: None,
4842                }),
4843                counter: TestInstanceCounter::Counter {
4844                    current: 2,
4845                    total: 100,
4846                },
4847                instance: TestInstanceId {
4848                    binary_id: &binary_a,
4849                    test_name: &test_1,
4850                },
4851                output: make_pass_output(),
4852            },
4853        ];
4854
4855        sort_final_outputs(&mut entries, false);
4856        assert_eq!(
4857            extract_ids(&entries),
4858            vec![("aaa", "test_1"), ("aaa", "test_2"), ("aaa", "test_1")],
4859            "stress index 0 entries come before stress index 2"
4860        );
4861        // Verify the stress indexes are in order.
4862        let stress_indexes: Vec<_> = entries
4863            .iter()
4864            .map(|e| e.stress_index.unwrap().current)
4865            .collect();
4866        assert_eq!(
4867            stress_indexes,
4868            vec![0, 0, 2],
4869            "stress indexes are sorted correctly"
4870        );
4871    }
4872}
4873
4874#[cfg(all(windows, test))]
4875mod windows_tests {
4876    use super::*;
4877    use crate::reporter::events::AbortDescription;
4878    use windows_sys::Win32::{
4879        Foundation::{STATUS_CONTROL_C_EXIT, STATUS_CONTROL_STACK_VIOLATION},
4880        Globalization::SetThreadUILanguage,
4881    };
4882
4883    #[test]
4884    fn test_write_windows_abort_line() {
4885        unsafe {
4886            // Set the thread UI language to US English for consistent output.
4887            SetThreadUILanguage(0x0409);
4888        }
4889
4890        insta::assert_snapshot!(
4891            "ctrl_c_code",
4892            to_abort_line(AbortStatus::WindowsNtStatus(STATUS_CONTROL_C_EXIT))
4893        );
4894        insta::assert_snapshot!(
4895            "stack_violation_code",
4896            to_abort_line(AbortStatus::WindowsNtStatus(STATUS_CONTROL_STACK_VIOLATION)),
4897        );
4898        insta::assert_snapshot!("job_object", to_abort_line(AbortStatus::JobObject));
4899    }
4900
4901    #[track_caller]
4902    fn to_abort_line(status: AbortStatus) -> String {
4903        let mut buf = String::new();
4904        let description = AbortDescription::from(status);
4905        write_windows_abort_line(&description, &Styles::default(), &mut buf).unwrap();
4906        buf
4907    }
4908}