Skip to main content

nextest_runner/reporter/displayer/
imp.rs

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