Skip to main content

nextest_runner/reporter/displayer/
progress.rs

1// Copyright (c) The nextest Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use crate::{
5    helpers::{
6        DisplayTestInstance, plural,
7        progress::{PROGRESS_REFRESH_RATE_HZ, progress_bar_style, term_progress_percent},
8    },
9    list::TestInstanceId,
10    reporter::{
11        displayer::formatters::DisplayBracketedHhMmSs,
12        events::*,
13        helpers::{Styles, print_lines_in_chunks},
14    },
15    run_mode::NextestRunMode,
16};
17use anstyle_progress::TermProgress;
18use indicatif::{ProgressBar, ProgressDrawTarget};
19use nextest_metadata::{RustBinaryId, TestCaseName};
20use owo_colors::OwoColorize;
21use std::{
22    cmp::{max, min},
23    env, fmt,
24    str::FromStr,
25    time::{Duration, Instant},
26};
27use swrite::{SWrite, swrite};
28
29/// The maximum number of running tests to display with
30/// `--show-progress=running` or `only`.
31#[derive(Clone, Copy, Debug, Eq, PartialEq)]
32pub enum MaxProgressRunning {
33    /// Show a specific maximum number of running tests.
34    /// If 0, running tests (including the overflow summary) aren't displayed.
35    Count(usize),
36
37    /// Show all running tests (no limit).
38    Infinite,
39}
40
41#[cfg(feature = "config-schema")]
42impl schemars::JsonSchema for MaxProgressRunning {
43    fn schema_name() -> std::borrow::Cow<'static, str> {
44        "MaxProgressRunning".into()
45    }
46
47    fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
48        schemars::json_schema!({
49            "title": "MaxProgressRunning",
50            "description":
51                "Maximum number of running tests to display: a non-negative integer, \
52                 or \"infinite\" for no limit.",
53            "oneOf": [
54                { "type": "integer", "minimum": 0 },
55                { "type": "string", "enum": ["infinite"] }
56            ]
57        })
58    }
59}
60
61impl MaxProgressRunning {
62    /// The default value (8 tests).
63    pub const DEFAULT_VALUE: Self = Self::Count(8);
64}
65
66impl Default for MaxProgressRunning {
67    fn default() -> Self {
68        Self::DEFAULT_VALUE
69    }
70}
71
72impl FromStr for MaxProgressRunning {
73    type Err = String;
74
75    fn from_str(s: &str) -> Result<Self, Self::Err> {
76        if s.eq_ignore_ascii_case("infinite") {
77            return Ok(Self::Infinite);
78        }
79
80        match s.parse::<usize>() {
81            Err(e) => Err(format!("Error: {e} parsing {s}")),
82            Ok(n) => Ok(Self::Count(n)),
83        }
84    }
85}
86
87impl fmt::Display for MaxProgressRunning {
88    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
89        match self {
90            Self::Infinite => write!(f, "infinite"),
91            Self::Count(n) => write!(f, "{n}"),
92        }
93    }
94}
95
96/// How to show progress.
97///
98/// In the `Auto` variant, the progress display is chosen based on the
99/// environment: a progress bar in interactive terminals, a counter otherwise.
100#[derive(Clone, Copy, Debug, PartialEq, Eq)]
101pub enum ShowProgress {
102    /// Automatically decide based on environment.
103    ///
104    /// When `suppress_success` is true and a progress bar is shown,
105    /// successful test output is suppressed (status level defaults to
106    /// `Slow`, final status level defaults to `None`). In non-interactive
107    /// contexts, output behaves identically to `suppress_success: false`:
108    /// all test results are displayed normally.
109    Auto {
110        /// Whether to hide successful test output when a progress bar is
111        /// shown.
112        suppress_success: bool,
113    },
114
115    /// No progress display.
116    None,
117
118    /// Show a counter on each line.
119    Counter,
120
121    /// Show a progress bar and the running tests.
122    Running,
123}
124
125impl Default for ShowProgress {
126    fn default() -> Self {
127        ShowProgress::Auto {
128            suppress_success: false,
129        }
130    }
131}
132
133#[derive(Debug)]
134pub(super) enum RunningTestStatus {
135    Running,
136    Slow,
137    Delay(Duration),
138    Retry,
139}
140
141#[derive(Debug)]
142pub(super) struct RunningTest {
143    binary_id: RustBinaryId,
144    test_name: TestCaseName,
145    status: RunningTestStatus,
146    start_time: Instant,
147    paused_for: Duration,
148}
149
150impl RunningTest {
151    fn message(&self, now: Instant, width: usize, styles: &Styles) -> String {
152        let mut elapsed = (now - self.start_time).saturating_sub(self.paused_for);
153        let status = match self.status {
154            RunningTestStatus::Running => "     ".to_owned(),
155            RunningTestStatus::Slow => " SLOW".style(styles.skip).to_string(),
156            RunningTestStatus::Delay(d) => {
157                // The elapsed might be greater than the delay duration in case
158                // we ticked past the delay duration without receiving a
159                // notification that the test retry started.
160                elapsed = d.saturating_sub(elapsed);
161                "DELAY".style(styles.retry).to_string()
162            }
163            RunningTestStatus::Retry => "RETRY".style(styles.retry).to_string(),
164        };
165        let max_width = width.saturating_sub(25);
166        let test = DisplayTestInstance::new(
167            None,
168            None,
169            TestInstanceId {
170                binary_id: &self.binary_id,
171
172                test_name: &self.test_name,
173            },
174            &styles.list_styles,
175        )
176        .with_max_width(max_width);
177        format!(
178            "       {} {}{}",
179            status,
180            DisplayBracketedHhMmSs(elapsed),
181            test
182        )
183    }
184}
185
186#[derive(Debug)]
187pub(super) struct ProgressBarState {
188    bar: ProgressBar,
189    mode: NextestRunMode,
190    stats: RunStats,
191    running: usize,
192    max_progress_running: MaxProgressRunning,
193    // Keep track of the maximum number of lines used. This allows to adapt the
194    // size of the 'viewport' to what we are using, and not just to the maximum
195    // number of tests that can be run in parallel
196    max_running_displayed: usize,
197    // None when the running tests are not displayed
198    running_tests: Option<Vec<RunningTest>>,
199    buffer: String,
200    // Size in bytes for chunking println calls. Configurable via the
201    // undocumented __NEXTEST_PROGRESS_PRINTLN_CHUNK_SIZE env var.
202    println_chunk_size: usize,
203    // Reasons for hiding the progress bar. We show the progress bar if none of
204    // these are set and hide it if any of them are set.
205    //
206    // indicatif cannot handle this kind of "stacked" state management, so it
207    // falls on us to do so.
208    //
209    // The current draw target is a pure function of these three booleans: if
210    // any of them are set, the draw target is hidden, otherwise it's stderr. If
211    // this changes, we'll need to track those other inputs.
212    hidden_no_capture: bool,
213    hidden_run_paused: bool,
214    hidden_info_response: bool,
215}
216
217impl ProgressBarState {
218    pub(super) fn new(
219        mode: NextestRunMode,
220        run_count: usize,
221        progress_chars: &str,
222        max_progress_running: MaxProgressRunning,
223    ) -> Self {
224        let bar = ProgressBar::new(run_count as u64);
225        let run_count_width = format!("{run_count}").len();
226        let suffix = format!("{{pos:>{run_count_width}}}/{{len:{run_count_width}}}: {{msg}}");
227        bar.set_style(progress_bar_style(progress_chars, &suffix));
228
229        let running_tests =
230            (!matches!(max_progress_running, MaxProgressRunning::Count(0))).then(Vec::new);
231
232        // The println chunk size defaults to a value chosen by experimentation,
233        // locally and over SSH. This controls how often the progress bar
234        // refreshes during large output bursts.
235        let println_chunk_size = env::var("__NEXTEST_PROGRESS_PRINTLN_CHUNK_SIZE")
236            .ok()
237            .and_then(|s| s.parse::<usize>().ok())
238            .unwrap_or(4096);
239
240        Self {
241            bar,
242            mode,
243            stats: RunStats::default(),
244            running: 0,
245            max_progress_running,
246            max_running_displayed: 0,
247            running_tests,
248            buffer: String::new(),
249            println_chunk_size,
250            hidden_no_capture: false,
251            hidden_run_paused: false,
252            hidden_info_response: false,
253        }
254    }
255
256    pub(super) fn tick(&mut self, styles: &Styles) {
257        self.update_message(styles);
258        self.print_and_clear_buffer();
259    }
260
261    fn print_and_clear_buffer(&mut self) {
262        self.print_and_force_redraw();
263        self.buffer.clear();
264    }
265
266    /// Prints the contents of the buffer, and always forces a redraw.
267    fn print_and_force_redraw(&self) {
268        if self.buffer.is_empty() {
269            // Force a redraw as part of our contract. See the documentation for
270            // `PROGRESS_REFRESH_RATE_HZ`.
271            self.bar.force_draw();
272            return;
273        }
274
275        // println below also forces a redraw, so we don't need to call
276        // force_draw in this case.
277
278        // ProgressBar::println is only called if there's something in the
279        // buffer, for two reasons:
280        //
281        // 1. If passed in nothing at all, it prints an empty line.
282        // 2. It forces a full redraw.
283        //
284        // But if self.buffer is too large, we can overwhelm the terminal with
285        // large amounts of non-progress-bar output, causing the progress bar to
286        // flicker in and out. To avoid those issues, we chunk the output to
287        // maintain progress bar visibility by redrawing it regularly.
288        print_lines_in_chunks(&self.buffer, self.println_chunk_size, |chunk| {
289            self.bar.println(chunk);
290        });
291    }
292
293    fn update_message(&mut self, styles: &Styles) {
294        let mut msg = self.progress_bar_msg(styles);
295        msg += "     ";
296
297        if let Some(running_tests) = &self.running_tests {
298            let (_, width) = console::Term::stderr().size();
299            let width = max(width as usize, 40);
300            let now = Instant::now();
301            let mut count = match self.max_progress_running {
302                MaxProgressRunning::Count(count) => min(running_tests.len(), count),
303                MaxProgressRunning::Infinite => running_tests.len(),
304            };
305            for running_test in &running_tests[..count] {
306                msg.push('\n');
307                msg.push_str(&running_test.message(now, width, styles));
308            }
309            if count < running_tests.len() {
310                let overflow_count = running_tests.len() - count;
311                swrite!(
312                    msg,
313                    "\n             ... and {} more {} running",
314                    overflow_count.style(styles.count),
315                    plural::tests_str(self.mode, overflow_count),
316                );
317                count += 1;
318            }
319            self.max_running_displayed = max(self.max_running_displayed, count);
320            msg.push_str(&"\n".to_string().repeat(self.max_running_displayed - count));
321        }
322        self.bar.set_message(msg);
323    }
324
325    fn progress_bar_msg(&self, styles: &Styles) -> String {
326        progress_bar_msg(&self.stats, self.running, styles)
327    }
328
329    pub(super) fn update_progress_bar(&mut self, event: &TestEvent<'_>, styles: &Styles) {
330        let before_should_hide = self.should_hide();
331
332        match &event.kind {
333            TestEventKind::StressSubRunStarted { .. } => {
334                self.bar.reset();
335            }
336            TestEventKind::StressSubRunFinished { .. } => {
337                // Clear all test bars to remove empty lines of output between
338                // sub-runs.
339                self.bar.finish_and_clear();
340            }
341            // Hide the progress bar if either stderr or stdout are being passed through.
342            TestEventKind::SetupScriptStarted { no_capture, .. } if *no_capture => {
343                self.hidden_no_capture = true;
344            }
345            // Restore the progress bar if it was hidden.
346            TestEventKind::SetupScriptFinished { no_capture, .. } if *no_capture => {
347                self.hidden_no_capture = false;
348            }
349            TestEventKind::TestStarted {
350                current_stats,
351                running,
352                test_instance,
353                ..
354            } => {
355                self.running = *running;
356                self.stats = *current_stats;
357
358                self.bar.set_prefix(progress_bar_prefix(
359                    current_stats,
360                    current_stats.cancel_reason,
361                    styles,
362                ));
363                self.bar.set_length(current_stats.initial_run_count as u64);
364                self.bar.set_position(current_stats.finished_count as u64);
365
366                if let Some(running_tests) = &mut self.running_tests {
367                    running_tests.push(RunningTest {
368                        binary_id: test_instance.binary_id.clone(),
369                        test_name: test_instance.test_name.to_owned(),
370                        status: RunningTestStatus::Running,
371                        start_time: Instant::now(),
372                        paused_for: Duration::ZERO,
373                    });
374                }
375            }
376            TestEventKind::TestFinished {
377                current_stats,
378                running,
379                test_instance,
380                ..
381            } => {
382                self.running = *running;
383                self.stats = *current_stats;
384                self.remove_test(test_instance);
385
386                self.bar.set_prefix(progress_bar_prefix(
387                    current_stats,
388                    current_stats.cancel_reason,
389                    styles,
390                ));
391                self.bar.set_length(current_stats.initial_run_count as u64);
392                self.bar.set_position(current_stats.finished_count as u64);
393            }
394            TestEventKind::TestAttemptFailedWillRetry {
395                test_instance,
396                delay_before_next_attempt,
397                ..
398            } => {
399                self.remove_test(test_instance);
400                if let Some(running_tests) = &mut self.running_tests {
401                    running_tests.push(RunningTest {
402                        binary_id: test_instance.binary_id.clone(),
403                        test_name: test_instance.test_name.to_owned(),
404                        status: RunningTestStatus::Delay(*delay_before_next_attempt),
405                        start_time: Instant::now(),
406                        paused_for: Duration::ZERO,
407                    });
408                }
409            }
410            TestEventKind::TestRetryStarted { test_instance, .. } => {
411                self.remove_test(test_instance);
412                if let Some(running_tests) = &mut self.running_tests {
413                    running_tests.push(RunningTest {
414                        binary_id: test_instance.binary_id.clone(),
415                        test_name: test_instance.test_name.to_owned(),
416                        status: RunningTestStatus::Retry,
417                        start_time: Instant::now(),
418                        paused_for: Duration::ZERO,
419                    });
420                }
421            }
422            TestEventKind::TestSlow { test_instance, .. } => {
423                if let Some(running_tests) = &mut self.running_tests {
424                    running_tests
425                        .iter_mut()
426                        .find(|rt| {
427                            &rt.binary_id == test_instance.binary_id
428                                && &rt.test_name == test_instance.test_name
429                        })
430                        .expect("a slow test to be already running")
431                        .status = RunningTestStatus::Slow;
432                }
433            }
434            TestEventKind::InfoStarted { .. } => {
435                // While info is being displayed, hide the progress bar to avoid
436                // it interrupting the info display.
437                self.hidden_info_response = true;
438            }
439            TestEventKind::InfoFinished { .. } => {
440                // Restore the progress bar if it was hidden.
441                self.hidden_info_response = false;
442            }
443            TestEventKind::RunPaused { .. } => {
444                // Pausing the run should hide the progress bar since we'll exit
445                // to the terminal immediately after.
446                self.hidden_run_paused = true;
447            }
448            TestEventKind::RunContinued { .. } => {
449                // Continuing the run should show the progress bar since we'll
450                // continue to output to it.
451                self.hidden_run_paused = false;
452                let current_global_elapsed = self.bar.elapsed();
453                self.bar.set_elapsed(event.elapsed);
454
455                if let Some(running_tests) = &mut self.running_tests {
456                    let delta = current_global_elapsed.saturating_sub(event.elapsed);
457                    for running_test in running_tests {
458                        running_test.paused_for += delta;
459                    }
460                }
461            }
462            TestEventKind::RunBeginCancel {
463                current_stats,
464                running,
465                ..
466            }
467            | TestEventKind::RunBeginKill {
468                current_stats,
469                running,
470                ..
471            } => {
472                self.running = *running;
473                self.stats = *current_stats;
474                self.bar.set_prefix(progress_bar_cancel_prefix(
475                    current_stats.cancel_reason,
476                    styles,
477                ));
478            }
479            _ => {}
480        }
481
482        let after_should_hide = self.should_hide();
483
484        match (before_should_hide, after_should_hide) {
485            (false, true) => self.bar.set_draw_target(Self::hidden_target()),
486            (true, false) => self.bar.set_draw_target(Self::stderr_target()),
487            _ => {}
488        }
489    }
490
491    fn remove_test(&mut self, test_instance: &TestInstanceId) {
492        if let Some(running_tests) = &mut self.running_tests {
493            running_tests.remove(
494                running_tests
495                    .iter()
496                    .position(|e| {
497                        &e.binary_id == test_instance.binary_id
498                            && &e.test_name == test_instance.test_name
499                    })
500                    .expect("finished test to have started"),
501            );
502        }
503    }
504
505    pub(super) fn write_buf(&mut self, buf: &str) {
506        self.buffer.push_str(buf);
507    }
508
509    #[inline]
510    pub(super) fn finish_and_clear(&self) {
511        self.print_and_force_redraw();
512        self.bar.finish_and_clear();
513    }
514
515    fn stderr_target() -> ProgressDrawTarget {
516        ProgressDrawTarget::stderr_with_hz(PROGRESS_REFRESH_RATE_HZ)
517    }
518
519    fn hidden_target() -> ProgressDrawTarget {
520        ProgressDrawTarget::hidden()
521    }
522
523    fn should_hide(&self) -> bool {
524        self.hidden_no_capture || self.hidden_run_paused || self.hidden_info_response
525    }
526}
527
528pub(super) fn terminal_progress_value(event: &TestEvent<'_>) -> TermProgress {
529    match &event.kind {
530        TestEventKind::RunStarted { .. }
531        | TestEventKind::StressSubRunStarted { .. }
532        | TestEventKind::StressSubRunFinished { .. }
533        | TestEventKind::SetupScriptStarted { .. }
534        | TestEventKind::SetupScriptSlow { .. }
535        | TestEventKind::SetupScriptFinished { .. } => TermProgress::none(),
536        TestEventKind::TestStarted { current_stats, .. }
537        | TestEventKind::TestFinished { current_stats, .. } => {
538            if current_stats.has_failures() || current_stats.cancel_reason.is_some() {
539                term_progress_errored(current_stats)
540            } else {
541                term_progress_running(current_stats)
542            }
543        }
544        TestEventKind::TestSlow { .. }
545        | TestEventKind::TestAttemptFailedWillRetry { .. }
546        | TestEventKind::TestRetryStarted { .. }
547        | TestEventKind::TestSkipped { .. }
548        | TestEventKind::InfoStarted { .. }
549        | TestEventKind::InfoResponse { .. }
550        | TestEventKind::InfoFinished { .. }
551        | TestEventKind::InputEnter { .. } => TermProgress::none(),
552        TestEventKind::RunBeginCancel { current_stats, .. }
553        | TestEventKind::RunBeginKill { current_stats, .. } => term_progress_errored(current_stats),
554        TestEventKind::RunPaused { .. }
555        | TestEventKind::RunContinued { .. }
556        | TestEventKind::RunFinished { .. } => {
557            // Reset the terminal state to nothing, since nextest is giving up
558            // control of the terminal at this point. (We don't use the paused
559            // terminal state here because the user might run other programs
560            // with their own progress bars while nextest is paused.)
561            TermProgress::remove()
562        }
563    }
564}
565
566fn term_progress_running(current_stats: &RunStats) -> TermProgress {
567    let percent = term_progress_percent(
568        current_stats.finished_count,
569        current_stats.initial_run_count,
570    );
571    TermProgress::start().percent(percent)
572}
573
574fn term_progress_errored(current_stats: &RunStats) -> TermProgress {
575    let percent = term_progress_percent(
576        current_stats.finished_count,
577        current_stats.initial_run_count,
578    );
579    TermProgress::error().percent(percent)
580}
581
582/// Returns a summary of current progress.
583pub(super) fn progress_str(
584    elapsed: Duration,
585    current_stats: &RunStats,
586    running: usize,
587    styles: &Styles,
588) -> String {
589    // First, show the prefix.
590    let mut s = progress_bar_prefix(current_stats, current_stats.cancel_reason, styles);
591
592    // Then, the time elapsed, test counts, and message.
593    swrite!(
594        s,
595        " {}{}/{}: {}",
596        DisplayBracketedHhMmSs(elapsed),
597        current_stats.finished_count,
598        current_stats.initial_run_count,
599        progress_bar_msg(current_stats, running, styles)
600    );
601
602    s
603}
604
605pub(super) fn write_summary_str(run_stats: &RunStats, styles: &Styles, out: &mut String) {
606    // Written in this style to ensure new fields are accounted for.
607    let &RunStats {
608        initial_run_count: _,
609        finished_count: _,
610        setup_scripts_initial_count: _,
611        setup_scripts_finished_count: _,
612        setup_scripts_passed: _,
613        setup_scripts_failed: _,
614        setup_scripts_exec_failed: _,
615        setup_scripts_timed_out: _,
616        passed,
617        passed_slow,
618        passed_timed_out: _,
619        flaky,
620        failed,
621        failed_slow: _,
622        failed_timed_out,
623        leaky,
624        leaky_failed,
625        exec_failed,
626        skipped,
627        cancel_reason: _,
628    } = run_stats;
629
630    swrite!(
631        out,
632        "{} {}",
633        passed.style(styles.count),
634        "passed".style(styles.pass)
635    );
636
637    if passed_slow > 0 || flaky > 0 || leaky > 0 {
638        let mut text = Vec::with_capacity(3);
639        if passed_slow > 0 {
640            text.push(format!(
641                "{} {}",
642                passed_slow.style(styles.count),
643                "slow".style(styles.skip),
644            ));
645        }
646        if flaky > 0 {
647            text.push(format!(
648                "{} {}",
649                flaky.style(styles.count),
650                "flaky".style(styles.skip),
651            ));
652        }
653        if leaky > 0 {
654            text.push(format!(
655                "{} {}",
656                leaky.style(styles.count),
657                "leaky".style(styles.skip),
658            ));
659        }
660        swrite!(out, " ({})", text.join(", "));
661    }
662    swrite!(out, ", ");
663
664    if failed > 0 {
665        swrite!(
666            out,
667            "{} {}",
668            failed.style(styles.count),
669            "failed".style(styles.fail),
670        );
671        if leaky_failed > 0 {
672            swrite!(
673                out,
674                " ({} due to being {})",
675                leaky_failed.style(styles.count),
676                "leaky".style(styles.fail),
677            );
678        }
679        swrite!(out, ", ");
680    }
681
682    if exec_failed > 0 {
683        swrite!(
684            out,
685            "{} {}, ",
686            exec_failed.style(styles.count),
687            "exec failed".style(styles.fail),
688        );
689    }
690
691    if failed_timed_out > 0 {
692        swrite!(
693            out,
694            "{} {}, ",
695            failed_timed_out.style(styles.count),
696            "timed out".style(styles.fail),
697        );
698    }
699
700    swrite!(
701        out,
702        "{} {}",
703        skipped.style(styles.count),
704        "skipped".style(styles.skip),
705    );
706}
707
708fn progress_bar_cancel_prefix(reason: Option<CancelReason>, styles: &Styles) -> String {
709    let status = match reason {
710        Some(CancelReason::SetupScriptFailure)
711        | Some(CancelReason::TestFailure)
712        | Some(CancelReason::ReportError)
713        | Some(CancelReason::GlobalTimeout)
714        | Some(CancelReason::TestFailureImmediate)
715        | Some(CancelReason::Signal)
716        | Some(CancelReason::Interrupt)
717        | None => "Cancelling",
718        Some(CancelReason::SecondSignal) => "Killing",
719    };
720    format!("{:>12}", status.style(styles.fail))
721}
722
723fn progress_bar_prefix(
724    run_stats: &RunStats,
725    cancel_reason: Option<CancelReason>,
726    styles: &Styles,
727) -> String {
728    if let Some(reason) = cancel_reason {
729        return progress_bar_cancel_prefix(Some(reason), styles);
730    }
731
732    let style = if run_stats.has_failures() {
733        styles.fail
734    } else {
735        styles.pass
736    };
737
738    format!("{:>12}", "Running".style(style))
739}
740
741pub(super) fn progress_bar_msg(
742    current_stats: &RunStats,
743    running: usize,
744    styles: &Styles,
745) -> String {
746    let mut s = format!("{} running, ", running.style(styles.count));
747    write_summary_str(current_stats, styles, &mut s);
748    s
749}
750
751#[cfg(test)]
752mod tests {
753    use super::*;
754    use crate::{
755        config::elements::{FlakyResult, JunitFlakyFailStatus},
756        output_spec::LiveSpec,
757        reporter::{TestOutputDisplay, test_helpers::global_slot_assignment},
758        test_output::{ChildExecutionOutput, ChildOutput, ChildSplitOutput},
759    };
760    use bytes::Bytes;
761    use chrono::Local;
762
763    #[test]
764    fn terminal_progress_value_escape_codes() {
765        let binary_id = RustBinaryId::new("test-binary");
766        let test_name = TestCaseName::new("test_name");
767
768        let normal = TestEvent {
769            timestamp: Local::now().fixed_offset(),
770            elapsed: Duration::ZERO,
771            kind: TestEventKind::TestStarted {
772                stress_index: None,
773                test_instance: TestInstanceId {
774                    binary_id: &binary_id,
775                    test_name: &test_name,
776                },
777                slot_assignment: global_slot_assignment(0),
778                current_stats: RunStats {
779                    initial_run_count: 10,
780                    finished_count: 3,
781                    ..RunStats::default()
782                },
783                running: 1,
784                command_line: vec![],
785            },
786        };
787        assert_eq!(
788            terminal_progress_value(&normal).to_string(),
789            "\x1b]9;4;1;30\x1b\\"
790        );
791
792        let failing = TestEvent {
793            timestamp: Local::now().fixed_offset(),
794            elapsed: Duration::ZERO,
795            kind: TestEventKind::TestStarted {
796                stress_index: None,
797                test_instance: TestInstanceId {
798                    binary_id: &binary_id,
799                    test_name: &test_name,
800                },
801                slot_assignment: global_slot_assignment(0),
802                current_stats: RunStats {
803                    initial_run_count: 10,
804                    finished_count: 3,
805                    failed: 1,
806                    ..RunStats::default()
807                },
808                running: 1,
809                command_line: vec![],
810            },
811        };
812        assert_eq!(
813            terminal_progress_value(&failing).to_string(),
814            "\x1b]9;4;2;30\x1b\\"
815        );
816
817        let cancelling = TestEvent {
818            timestamp: Local::now().fixed_offset(),
819            elapsed: Duration::ZERO,
820            kind: TestEventKind::RunBeginCancel {
821                setup_scripts_running: 0,
822                current_stats: RunStats {
823                    initial_run_count: 4,
824                    finished_count: 1,
825                    cancel_reason: Some(CancelReason::Signal),
826                    ..RunStats::default()
827                },
828                running: 3,
829            },
830        };
831        assert_eq!(
832            terminal_progress_value(&cancelling).to_string(),
833            "\x1b]9;4;2;25\x1b\\"
834        );
835
836        let paused = TestEvent {
837            timestamp: Local::now().fixed_offset(),
838            elapsed: Duration::ZERO,
839            kind: TestEventKind::RunPaused {
840                setup_scripts_running: 0,
841                running: 2,
842            },
843        };
844        assert_eq!(
845            terminal_progress_value(&paused).to_string(),
846            "\x1b]9;4;0;\x1b\\"
847        );
848    }
849
850    #[test]
851    fn test_progress_bar_prefix() {
852        let mut styles = Styles::default();
853        styles.colorize();
854
855        for (name, stats) in run_stats_test_failure_examples() {
856            let prefix = progress_bar_prefix(&stats, Some(CancelReason::TestFailure), &styles);
857            assert_eq!(
858                prefix,
859                "  Cancelling".style(styles.fail).to_string(),
860                "{name} matches"
861            );
862        }
863        for (name, stats) in run_stats_setup_script_failure_examples() {
864            let prefix =
865                progress_bar_prefix(&stats, Some(CancelReason::SetupScriptFailure), &styles);
866            assert_eq!(
867                prefix,
868                "  Cancelling".style(styles.fail).to_string(),
869                "{name} matches"
870            );
871        }
872
873        let prefix = progress_bar_prefix(&RunStats::default(), Some(CancelReason::Signal), &styles);
874        assert_eq!(prefix, "  Cancelling".style(styles.fail).to_string());
875
876        let prefix = progress_bar_prefix(&RunStats::default(), None, &styles);
877        assert_eq!(prefix, "     Running".style(styles.pass).to_string());
878
879        for (name, stats) in run_stats_test_failure_examples() {
880            let prefix = progress_bar_prefix(&stats, None, &styles);
881            assert_eq!(
882                prefix,
883                "     Running".style(styles.fail).to_string(),
884                "{name} matches"
885            );
886        }
887        for (name, stats) in run_stats_setup_script_failure_examples() {
888            let prefix = progress_bar_prefix(&stats, None, &styles);
889            assert_eq!(
890                prefix,
891                "     Running".style(styles.fail).to_string(),
892                "{name} matches"
893            );
894        }
895    }
896
897    #[test]
898    fn progress_str_snapshots() {
899        let mut styles = Styles::default();
900        styles.colorize();
901
902        // This elapsed time is arbitrary but reasonably large.
903        let elapsed = Duration::from_secs(123456);
904        let running = 10;
905
906        for (name, stats) in run_stats_test_failure_examples() {
907            let s = progress_str(elapsed, &stats, running, &styles);
908            insta::assert_snapshot!(format!("{name}_with_cancel_reason"), s);
909
910            let mut stats = stats;
911            stats.cancel_reason = None;
912            let s = progress_str(elapsed, &stats, running, &styles);
913            insta::assert_snapshot!(format!("{name}_without_cancel_reason"), s);
914        }
915
916        for (name, stats) in run_stats_setup_script_failure_examples() {
917            let s = progress_str(elapsed, &stats, running, &styles);
918            insta::assert_snapshot!(format!("{name}_with_cancel_reason"), s);
919
920            let mut stats = stats;
921            stats.cancel_reason = None;
922            let s = progress_str(elapsed, &stats, running, &styles);
923            insta::assert_snapshot!(format!("{name}_without_cancel_reason"), s);
924        }
925    }
926
927    #[test]
928    fn running_test_snapshots() {
929        let styles = Styles::default();
930        let now = Instant::now();
931
932        for (name, running_test) in running_test_examples(now) {
933            let msg = running_test.message(now, 80, &styles);
934            insta::assert_snapshot!(name, msg);
935        }
936    }
937
938    #[test]
939    fn running_test_message_multi_hour() {
940        let styles = Styles::default();
941        let start_time = Instant::now();
942        let now = start_time + Duration::from_secs(5 * 3600 + 13 * 60 + 30);
943
944        let running_test = RunningTest {
945            binary_id: RustBinaryId::new("my-binary"),
946            test_name: TestCaseName::new("test::my_test"),
947            status: RunningTestStatus::Running,
948            start_time,
949            paused_for: Duration::ZERO,
950        };
951        let msg = running_test.message(now, 80, &styles);
952        insta::assert_snapshot!(msg, @"             [ 05:13:30] my-binary test::my_test");
953    }
954
955    fn running_test_examples(now: Instant) -> Vec<(&'static str, RunningTest)> {
956        let binary_id = RustBinaryId::new("my-binary");
957        let test_name = TestCaseName::new("test::my_test");
958        let start_time = now - Duration::from_secs(125); // 2 minutes 5 seconds ago
959
960        vec![
961            (
962                "running_status",
963                RunningTest {
964                    binary_id: binary_id.clone(),
965                    test_name: test_name.clone(),
966                    status: RunningTestStatus::Running,
967                    start_time,
968                    paused_for: Duration::ZERO,
969                },
970            ),
971            (
972                "slow_status",
973                RunningTest {
974                    binary_id: binary_id.clone(),
975                    test_name: test_name.clone(),
976                    status: RunningTestStatus::Slow,
977                    start_time,
978                    paused_for: Duration::ZERO,
979                },
980            ),
981            (
982                "delay_status",
983                RunningTest {
984                    binary_id: binary_id.clone(),
985                    test_name: test_name.clone(),
986                    status: RunningTestStatus::Delay(Duration::from_secs(130)),
987                    start_time,
988                    paused_for: Duration::ZERO,
989                },
990            ),
991            (
992                "delay_status_underflow",
993                RunningTest {
994                    binary_id: binary_id.clone(),
995                    test_name: test_name.clone(),
996                    status: RunningTestStatus::Delay(Duration::from_secs(124)),
997                    start_time,
998                    paused_for: Duration::ZERO,
999                },
1000            ),
1001            (
1002                "retry_status",
1003                RunningTest {
1004                    binary_id: binary_id.clone(),
1005                    test_name: test_name.clone(),
1006                    status: RunningTestStatus::Retry,
1007                    start_time,
1008                    paused_for: Duration::ZERO,
1009                },
1010            ),
1011            (
1012                "with_paused_duration",
1013                RunningTest {
1014                    binary_id: binary_id.clone(),
1015                    test_name: test_name.clone(),
1016                    status: RunningTestStatus::Running,
1017                    start_time,
1018                    paused_for: Duration::from_secs(30),
1019                },
1020            ),
1021        ]
1022    }
1023
1024    fn run_stats_test_failure_examples() -> Vec<(&'static str, RunStats)> {
1025        vec![
1026            (
1027                "one_failed",
1028                RunStats {
1029                    initial_run_count: 20,
1030                    finished_count: 1,
1031                    failed: 1,
1032                    cancel_reason: Some(CancelReason::TestFailure),
1033                    ..RunStats::default()
1034                },
1035            ),
1036            (
1037                "one_failed_one_passed",
1038                RunStats {
1039                    initial_run_count: 20,
1040                    finished_count: 2,
1041                    failed: 1,
1042                    passed: 1,
1043                    cancel_reason: Some(CancelReason::TestFailure),
1044                    ..RunStats::default()
1045                },
1046            ),
1047            (
1048                "one_exec_failed",
1049                RunStats {
1050                    initial_run_count: 20,
1051                    finished_count: 10,
1052                    exec_failed: 1,
1053                    cancel_reason: Some(CancelReason::TestFailure),
1054                    ..RunStats::default()
1055                },
1056            ),
1057            (
1058                "one_timed_out",
1059                RunStats {
1060                    initial_run_count: 20,
1061                    finished_count: 10,
1062                    failed_timed_out: 1,
1063                    cancel_reason: Some(CancelReason::TestFailure),
1064                    ..RunStats::default()
1065                },
1066            ),
1067        ]
1068    }
1069
1070    fn run_stats_setup_script_failure_examples() -> Vec<(&'static str, RunStats)> {
1071        vec![
1072            (
1073                "one_setup_script_failed",
1074                RunStats {
1075                    initial_run_count: 30,
1076                    setup_scripts_failed: 1,
1077                    cancel_reason: Some(CancelReason::SetupScriptFailure),
1078                    ..RunStats::default()
1079                },
1080            ),
1081            (
1082                "one_setup_script_exec_failed",
1083                RunStats {
1084                    initial_run_count: 35,
1085                    setup_scripts_exec_failed: 1,
1086                    cancel_reason: Some(CancelReason::SetupScriptFailure),
1087                    ..RunStats::default()
1088                },
1089            ),
1090            (
1091                "one_setup_script_timed_out",
1092                RunStats {
1093                    initial_run_count: 40,
1094                    setup_scripts_timed_out: 1,
1095                    cancel_reason: Some(CancelReason::SetupScriptFailure),
1096                    ..RunStats::default()
1097                },
1098            ),
1099        ]
1100    }
1101
1102    /// Test that `update_progress_bar` correctly updates `self.stats` when
1103    /// processing `TestStarted` and `TestFinished` events.
1104    ///
1105    /// This test verifies both:
1106    ///
1107    /// 1. State: `self.stats` equals the event's `current_stats` after processing.
1108    /// 2. Output: `state.progress_bar_msg()` reflects the updated stats.
1109    #[test]
1110    fn update_progress_bar_updates_stats() {
1111        let styles = Styles::default();
1112        let binary_id = RustBinaryId::new("test-binary");
1113        let test_name = TestCaseName::new("test_name");
1114
1115        // Create ProgressBarState with initial (default) stats.
1116        let mut state = ProgressBarState::new(
1117            NextestRunMode::Test,
1118            10,
1119            "=> ",
1120            MaxProgressRunning::default(),
1121        );
1122
1123        // Verify the initial state.
1124        assert_eq!(state.stats, RunStats::default());
1125        assert_eq!(state.running, 0);
1126
1127        // Create a TestStarted event.
1128        let started_stats = RunStats {
1129            initial_run_count: 10,
1130            passed: 5,
1131            ..RunStats::default()
1132        };
1133        let started_event = TestEvent {
1134            timestamp: Local::now().fixed_offset(),
1135            elapsed: Duration::ZERO,
1136            kind: TestEventKind::TestStarted {
1137                stress_index: None,
1138                test_instance: TestInstanceId {
1139                    binary_id: &binary_id,
1140                    test_name: &test_name,
1141                },
1142                slot_assignment: global_slot_assignment(0),
1143                current_stats: started_stats,
1144                running: 3,
1145                command_line: vec![],
1146            },
1147        };
1148
1149        state.update_progress_bar(&started_event, &styles);
1150
1151        // Verify the state was updated.
1152        assert_eq!(
1153            state.stats, started_stats,
1154            "stats should be updated on TestStarted"
1155        );
1156        assert_eq!(state.running, 3, "running should be updated on TestStarted");
1157
1158        // Verify that the output reflects the updated stats.
1159        let msg = state.progress_bar_msg(&styles);
1160        insta::assert_snapshot!(msg, @"3 running, 5 passed, 0 skipped");
1161
1162        // Create a TestFinished event with different stats.
1163        let finished_stats = RunStats {
1164            initial_run_count: 10,
1165            finished_count: 1,
1166            passed: 8,
1167            ..RunStats::default()
1168        };
1169        let finished_event = TestEvent {
1170            timestamp: Local::now().fixed_offset(),
1171            elapsed: Duration::ZERO,
1172            kind: TestEventKind::TestFinished {
1173                stress_index: None,
1174                test_instance: TestInstanceId {
1175                    binary_id: &binary_id,
1176                    test_name: &test_name,
1177                },
1178                success_output: TestOutputDisplay::Never,
1179                failure_output: TestOutputDisplay::Never,
1180                junit_store_success_output: false,
1181                junit_store_failure_output: false,
1182                junit_flaky_fail_status: JunitFlakyFailStatus::default(),
1183                run_statuses: ExecutionStatuses::new(
1184                    vec![ExecuteStatus {
1185                        retry_data: RetryData {
1186                            attempt: 1,
1187                            total_attempts: 1,
1188                        },
1189                        output: make_test_output(),
1190                        result: ExecutionResultDescription::Pass,
1191                        start_time: Local::now().fixed_offset(),
1192                        time_taken: Duration::from_secs(1),
1193                        is_slow: false,
1194                        delay_before_start: Duration::ZERO,
1195                        error_summary: None,
1196                        output_error_slice: None,
1197                    }],
1198                    FlakyResult::default(),
1199                ),
1200                current_stats: finished_stats,
1201                running: 2,
1202            },
1203        };
1204
1205        state.update_progress_bar(&finished_event, &styles);
1206
1207        // Verify the state was updated.
1208        assert_eq!(
1209            state.stats, finished_stats,
1210            "stats should be updated on TestFinished"
1211        );
1212        assert_eq!(
1213            state.running, 2,
1214            "running should be updated on TestFinished"
1215        );
1216
1217        // Verify that the output reflects the updated stats.
1218        let msg = state.progress_bar_msg(&styles);
1219        insta::assert_snapshot!(msg, @"2 running, 8 passed, 0 skipped");
1220
1221        // Create a RunBeginCancel event.
1222        let cancel_stats = RunStats {
1223            initial_run_count: 10,
1224            finished_count: 3,
1225            passed: 2,
1226            failed: 1,
1227            cancel_reason: Some(CancelReason::TestFailure),
1228            ..RunStats::default()
1229        };
1230        let cancel_event = TestEvent {
1231            timestamp: Local::now().fixed_offset(),
1232            elapsed: Duration::ZERO,
1233            kind: TestEventKind::RunBeginCancel {
1234                setup_scripts_running: 0,
1235                current_stats: cancel_stats,
1236                running: 4,
1237            },
1238        };
1239
1240        state.update_progress_bar(&cancel_event, &styles);
1241
1242        // Verify the state was updated.
1243        assert_eq!(
1244            state.stats, cancel_stats,
1245            "stats should be updated on RunBeginCancel"
1246        );
1247        assert_eq!(
1248            state.running, 4,
1249            "running should be updated on RunBeginCancel"
1250        );
1251
1252        // Verify that the output reflects the updated stats.
1253        let msg = state.progress_bar_msg(&styles);
1254        insta::assert_snapshot!(msg, @"4 running, 2 passed, 1 failed, 0 skipped");
1255
1256        // Create a RunBeginKill event with different stats.
1257        let kill_stats = RunStats {
1258            initial_run_count: 10,
1259            finished_count: 5,
1260            passed: 3,
1261            failed: 2,
1262            cancel_reason: Some(CancelReason::Signal),
1263            ..RunStats::default()
1264        };
1265        let kill_event = TestEvent {
1266            timestamp: Local::now().fixed_offset(),
1267            elapsed: Duration::ZERO,
1268            kind: TestEventKind::RunBeginKill {
1269                setup_scripts_running: 0,
1270                current_stats: kill_stats,
1271                running: 2,
1272            },
1273        };
1274
1275        state.update_progress_bar(&kill_event, &styles);
1276
1277        // Verify the state was updated.
1278        assert_eq!(
1279            state.stats, kill_stats,
1280            "stats should be updated on RunBeginKill"
1281        );
1282        assert_eq!(
1283            state.running, 2,
1284            "running should be updated on RunBeginKill"
1285        );
1286
1287        // Verify that the output reflects the updated stats.
1288        let msg = state.progress_bar_msg(&styles);
1289        insta::assert_snapshot!(msg, @"2 running, 3 passed, 2 failed, 0 skipped");
1290    }
1291
1292    // Helper to create minimal output for ExecuteStatus.
1293    fn make_test_output() -> ChildExecutionOutputDescription<LiveSpec> {
1294        ChildExecutionOutput::Output {
1295            result: Some(ExecutionResult::Pass),
1296            output: ChildOutput::Split(ChildSplitOutput {
1297                stdout: Some(Bytes::new().into()),
1298                stderr: Some(Bytes::new().into()),
1299            }),
1300            errors: None,
1301        }
1302        .into()
1303    }
1304}