Skip to main content

nextest_runner/reporter/displayer/
imp.rs

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