Skip to main content

nextest_runner/helpers/
progress.rs

1// Copyright (c) The nextest Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use crate::cargo_config::{CargoConfigs, DiscoveredConfig};
5use anstyle_progress::{TermProgress, supports_term_progress};
6use indicatif::ProgressStyle;
7use std::{
8    env,
9    io::{self, Write},
10};
11use tracing::debug;
12
13/// The refresh rate for the progress bar, set to a minimal value.
14///
15/// For progress, during each tick, two things happen:
16///
17/// - We update the message, calling self.bar.set_message.
18/// - We print any buffered output.
19///
20/// We want both of these updates to be combined into one terminal flush, so we
21/// set *this* to a minimal value (so self.bar.set_message doesn't do a redraw),
22/// and rely on ProgressBarState::print_and_force_redraw to always flush the
23/// terminal.
24pub(crate) const PROGRESS_REFRESH_RATE_HZ: u8 = 1;
25
26pub(crate) fn progress_bar_style(progress_chars: &str, suffix: &str) -> ProgressStyle {
27    // Create the template. This is a little confusing -- {{foo}} is what's
28    // passed into the ProgressBar, while {bar} is inserted by the format!()
29    // statement.
30    let template = format!("{{prefix:>12}} [{{elapsed_precise:>9}}] {{wide_bar}} {suffix}");
31    ProgressStyle::default_bar()
32        .progress_chars(progress_chars)
33        .template(&template)
34        .expect("template is known to be valid")
35}
36
37/// Whether to show OSC 9;4 terminal progress.
38#[derive(Clone, Copy, Debug, Eq, PartialEq)]
39pub enum ShowTerminalProgress {
40    /// Show terminal progress.
41    Yes,
42
43    /// Do not show terminal progress.
44    No,
45}
46
47impl ShowTerminalProgress {
48    const ENV: &str = "CARGO_TERM_PROGRESS_TERM_INTEGRATION";
49
50    /// Determines whether to show terminal progress based on Cargo configs and
51    /// whether the output is a terminal.
52    pub fn from_cargo_configs(configs: &CargoConfigs, is_terminal: bool) -> Self {
53        // See whether terminal integration is enabled in Cargo.
54        for config in configs.discovered_configs() {
55            match config {
56                DiscoveredConfig::CliOption { config, source } => {
57                    if let Some(v) = config.term.progress.term_integration {
58                        if v {
59                            debug!("enabling terminal progress reporting based on {source:?}");
60                            return Self::Yes;
61                        } else {
62                            debug!("disabling terminal progress reporting based on {source:?}");
63                            return Self::No;
64                        }
65                    }
66                }
67                DiscoveredConfig::Env => {
68                    if let Some(v) = env::var_os(Self::ENV) {
69                        if v == "true" {
70                            debug!(
71                                "enabling terminal progress reporting based on \
72                                 CARGO_TERM_PROGRESS_TERM_INTEGRATION environment variable"
73                            );
74                            return Self::Yes;
75                        } else if v == "false" {
76                            debug!(
77                                "disabling terminal progress reporting based on \
78                                 CARGO_TERM_PROGRESS_TERM_INTEGRATION environment variable"
79                            );
80                            return Self::No;
81                        } else {
82                            debug!(
83                                "invalid value for CARGO_TERM_PROGRESS_TERM_INTEGRATION \
84                                 environment variable: {v:?}, ignoring"
85                            );
86                        }
87                    }
88                }
89                DiscoveredConfig::File { config, source } => {
90                    if let Some(v) = config.term.progress.term_integration {
91                        if v {
92                            debug!("enabling terminal progress reporting based on {source:?}");
93                            return Self::Yes;
94                        } else {
95                            debug!("disabling terminal progress reporting based on {source:?}");
96                            return Self::No;
97                        }
98                    }
99                }
100            }
101        }
102
103        let show = supports_term_progress(is_terminal);
104        debug!(is_terminal, show, "autodetected terminal progress support");
105        if show { Self::Yes } else { Self::No }
106    }
107}
108
109/// OSC 9 terminal progress reporting.
110#[derive(Default)]
111pub(crate) struct TerminalProgress {
112    last_value: TermProgress,
113}
114
115impl TerminalProgress {
116    pub(crate) fn new(show: ShowTerminalProgress) -> Option<Self> {
117        match show {
118            ShowTerminalProgress::Yes => Some(Self::default()),
119            ShowTerminalProgress::No => None,
120        }
121    }
122
123    pub(crate) fn set(&mut self, value: TermProgress) {
124        self.last_value = value;
125    }
126
127    pub(crate) fn emit(&self) {
128        // Use the write! macro rather than eprint! so that we ignore errors
129        // rather than panicking. Terminal progress reporting is cosmetic and
130        // best-effort.
131        let _ = write!(io::stderr(), "{}", self.last_value);
132    }
133}
134
135pub(crate) fn term_progress_percent(done: usize, total: usize) -> u8 {
136    if total == 0 {
137        return 100;
138    }
139    ((done as f64 / total as f64) * 100.0)
140        .round()
141        .clamp(0.0, 100.0) as u8
142}
143
144#[cfg(test)]
145mod tests {
146    use super::term_progress_percent;
147
148    #[test]
149    fn term_progress_percent_boundaries() {
150        // Handle 0/0 properly.
151        assert_eq!(term_progress_percent(0, 0), 100);
152        assert_eq!(term_progress_percent(0, 10), 0);
153        assert_eq!(term_progress_percent(1, 2), 50);
154        assert_eq!(term_progress_percent(1, 4), 25);
155        assert_eq!(term_progress_percent(10, 10), 100);
156
157        // We round away from zero (12.5 -> 13, 37.5 -> 38).
158        assert_eq!(term_progress_percent(1, 8), 13);
159        assert_eq!(term_progress_percent(3, 8), 38);
160
161        // We stay within 0..=100 for the percentage.
162        assert_eq!(term_progress_percent(11, 10), 100);
163    }
164}