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