nextest_runner/reporter/displayer/
imp.rs

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