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