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