#
cargo/util/
progress.rs

    1//! Support for CLI progress bars.
    2
    3use std::cmp;
    4use std::time::{Duration, Instant};
    5
    6use crate::core::shell::Verbosity;
    7use crate::util::context::ProgressWhen;
    8use crate::util::{CargoResult, GlobalContext};
    9use cargo_util::is_ci;
    10use unicode_width::UnicodeWidthChar;
    11
    12/// CLI progress bar.
    13///
    14/// The `Progress` object can be in an enabled or disabled state. When
    15/// disabled, calling any of the methods to update it will not display
    16/// anything. Disabling is typically done by the user with options such as
    17/// `--quiet` or the `term.progress` config option.
    18///
    19/// There are several methods to update the progress bar and to cause it to
    20/// update its display.
    21///
    22/// The bar will be removed from the display when the `Progress` object is
    23/// dropped or [`Progress::clear`] is called.
    24///
    25/// The progress bar has built-in rate limiting to avoid updating the display
    26/// too fast. It should usually be fine to call [`Progress::tick`] as often as
    27/// needed, though be cautious if the tick rate is very high or it is
    28/// expensive to compute the progress value.
    29pub struct Progress<'gctx> {
    30    state: Option<State<'gctx>>,
    31}
    32
    33/// Indicates the style of information for displaying the amount of progress.
    34///
    35/// See also [`Progress::print_now`] for displaying progress without a bar.
    36pub enum ProgressStyle {
    37    /// Displays progress as a percentage.
    38    ///
    39    /// Example: `Fetch [=====================>   ]  88.15%`
    40    ///
    41    /// This is good for large values like number of bytes downloaded.
    42    Percentage,
    43    /// Displays progress as a ratio.
    44    ///
    45    /// Example: `Building [===>                      ] 35/222`
    46    ///
    47    /// This is good for smaller values where the exact number is useful to see.
    48    Ratio,
    49    /// Does not display an exact value of how far along it is.
    50    ///
    51    /// Example: `Fetch [===========>                     ]`
    52    ///
    53    /// This is good for situations where the exact value is an approximation,
    54    /// and thus there isn't anything accurate to display to the user.
    55    Indeterminate,
    56}
    57
    58struct Throttle {
    59    first: bool,
    60    last_update: Instant,
    61}
    62
    63struct State<'gctx> {
    64    gctx: &'gctx GlobalContext,
    65    format: Format,
    66    name: String,
    67    done: bool,
    68    throttle: Throttle,
    69    last_line: Option<String>,
    70    fixed_width: Option<usize>,
    71}
    72
    73struct Format {
    74    style: ProgressStyle,
    75    max_width: usize,
    76    max_print: usize,
    77    term_integration: TerminalIntegration,
    78    unicode: bool,
    79}
    80
    81/// Controls terminal progress integration via OSC sequences.
    82struct TerminalIntegration {
    83    enabled: bool,
    84    error: bool,
    85}
    86
    87/// A progress status value printable as an ANSI OSC 9;4 escape code.
    88#[cfg_attr(test, derive(PartialEq, Debug))]
    89enum StatusValue {
    90    /// No output.
    91    None,
    92    /// Remove progress.
    93    Remove,
    94    /// Progress value (0-100).
    95    Value(f64),
    96    /// Indeterminate state (no bar, just animation)
    97    Indeterminate,
    98    /// Progress value in an error state (0-100).
    99    Error(f64),
    100}
    101
    102enum ProgressOutput {
    103    /// Print progress without a message
    104    PrintNow,
    105    /// Progress, message and progress report
    106    TextAndReport(String, StatusValue),
    107    /// Only progress report, no message and no text progress
    108    Report(StatusValue),
    109}
    110
    111impl TerminalIntegration {
    112    #[cfg(test)]
    113    fn new(enabled: bool) -> Self {
    114        Self {
    115            enabled,
    116            error: false,
    117        }
    118    }
    119
    120    /// Creates a `TerminalIntegration` from Cargo's configuration.
    121    /// Autodetect support if not explicitly enabled or disabled.
    122    fn from_config(gctx: &GlobalContext) -> Self {
    123        let enabled = gctx
    124            .progress_config()
    125            .term_integration
    126            .unwrap_or_else(|| gctx.shell().is_err_term_integration_available());
    127
    128        Self {
    129            enabled,
    130            error: false,
    131        }
    132    }
    133
    134    fn progress_state(&self, value: StatusValue) -> StatusValue {
    135        match (self.enabled, self.error) {
    136            (true, false) => value,
    137            (true, true) => match value {
    138                StatusValue::Value(v) => StatusValue::Error(v),
    139                _ => StatusValue::Error(100.0),
    140            },
    141            (false, _) => StatusValue::None,
    142        }
    143    }
    144
    145    pub fn remove(&self) -> StatusValue {
    146        self.progress_state(StatusValue::Remove)
    147    }
    148
    149    pub fn value(&self, percent: f64) -> StatusValue {
    150        self.progress_state(StatusValue::Value(percent))
    151    }
    152
    153    pub fn indeterminate(&self) -> StatusValue {
    154        self.progress_state(StatusValue::Indeterminate)
    155    }
    156
    157    pub fn error(&mut self) {
    158        self.error = true;
    159    }
    160}
    161
    162impl std::fmt::Display for StatusValue {
    163    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    164        // From https://conemu.github.io/en/AnsiEscapeCodes.html#ConEmu_specific_OSC
    165        // ESC ] 9 ; 4 ; st ; pr ST
    166        // When st is 0: remove progress.
    167        // When st is 1: set progress value to pr (number, 0-100).
    168        // When st is 2: set error state in taskbar, pr is optional.
    169        // When st is 3: set indeterminate state, pr is ignored.
    170        // When st is 4: set paused state, pr is optional.
    171        let (state, progress) = match self {
    172            Self::None => return Ok(()), // No output
    173            Self::Remove => (0, 0.0),
    174            Self::Value(v) => (1, *v),
    175            Self::Indeterminate => (3, 0.0),
    176            Self::Error(v) => (2, *v),
    177        };
    178        write!(f, "\x1b]9;4;{state};{progress:.0}\x1b\\")
    179    }
    180}
    181
    182impl<'gctx> Progress<'gctx> {
    183    /// Creates a new progress bar.
    184    ///
    185    /// The first parameter is the text displayed to the left of the bar, such
    186    /// as "Fetching".
    187    ///
    188    /// The progress bar is not displayed until explicitly updated with one if
    189    /// its methods.
    190    ///
    191    /// The progress bar may be created in a disabled state if the user has
    192    /// disabled progress display (such as with the `--quiet` option).
    193    pub fn with_style(
    194        name: &str,
    195        style: ProgressStyle,
    196        gctx: &'gctx GlobalContext,
    197    ) -> Progress<'gctx> {
    198        // report no progress when -q (for quiet) or TERM=dumb are set
    199        // or if running on Continuous Integration service like Travis where the
    200        // output logs get mangled.
    201        let dumb = match gctx.get_env("TERM") {
    202            Ok(term) => term == "dumb",
    203            Err(_) => false,
    204        };
    205        let progress_config = gctx.progress_config();
    206        match progress_config.when {
    207            ProgressWhen::Always => return Progress::new_priv(name, style, gctx),
    208            ProgressWhen::Never => return Progress { state: None },
    209            ProgressWhen::Auto => {}
    210        }
    211        if gctx.shell().verbosity() == Verbosity::Quiet || dumb || is_ci() {
    212            return Progress { state: None };
    213        }
    214        Progress::new_priv(name, style, gctx)
    215    }
    216
    217    fn new_priv(name: &str, style: ProgressStyle, gctx: &'gctx GlobalContext) -> Progress<'gctx> {
    218        let progress_config = gctx.progress_config();
    219        let width = progress_config
    220            .width
    221            .or_else(|| gctx.shell().err_width().progress_max_width());
    222
    223        Progress {
    224            state: width.map(|n| State {
    225                gctx,
    226                format: Format {
    227                    style,
    228                    max_width: n,
    229                    // 50 gives some space for text after the progress bar,
    230                    // even on narrow (e.g. 80 char) terminals.
    231                    max_print: 50,
    232                    term_integration: TerminalIntegration::from_config(gctx),
    233                    unicode: gctx.shell().err_unicode(),
    234                },
    235                name: name.to_string(),
    236                done: false,
    237                throttle: Throttle::new(),
    238                last_line: None,
    239                fixed_width: progress_config.width,
    240            }),
    241        }
    242    }
    243
    244    /// Disables the progress bar, ensuring it won't be displayed.
    245    pub fn disable(&mut self) {
    246        self.state = None;
    247    }
    248
    249    /// Returns whether or not the progress bar is allowed to be displayed.
    250    pub fn is_enabled(&self) -> bool {
    251        self.state.is_some()
    252    }
    253
    254    /// Creates a new `Progress` with the [`ProgressStyle::Percentage`] style.
    255    ///
    256    /// See [`Progress::with_style`] for more information.
    257    pub fn new(name: &str, gctx: &'gctx GlobalContext) -> Progress<'gctx> {
    258        Self::with_style(name, ProgressStyle::Percentage, gctx)
    259    }
    260
    261    /// Updates the state of the progress bar.
    262    ///
    263    /// * `cur` should be how far along the progress is.
    264    /// * `max` is the maximum value for the progress bar.
    265    /// * `msg` is a small piece of text to display at the end of the progress
    266    ///   bar. It will be truncated with `…` if it does not fit on the terminal.
    267    ///
    268    /// This may not actually update the display if `tick` is being called too
    269    /// quickly.
    270    pub fn tick(&mut self, cur: usize, max: usize, msg: &str) -> CargoResult<()> {
    271        let Some(s) = &mut self.state else {
    272            return Ok(());
    273        };
    274
    275        // Don't update too often as it can cause excessive performance loss
    276        // just putting stuff onto the terminal. We also want to avoid
    277        // flickering by not drawing anything that goes away too quickly. As a
    278        // result we've got two branches here:
    279        //
    280        // 1. If we haven't drawn anything, we wait for a period of time to
    281        //    actually start drawing to the console. This ensures that
    282        //    short-lived operations don't flicker on the console. Currently
    283        //    there's a 500ms delay to when we first draw something.
    284        // 2. If we've drawn something, then we rate limit ourselves to only
    285        //    draw to the console every so often. Currently there's a 100ms
    286        //    delay between updates.
    287        if !s.throttle.allowed() {
    288            return Ok(());
    289        }
    290
    291        s.tick(cur, max, msg)
    292    }
    293
    294    /// Updates the state of the progress bar.
    295    ///
    296    /// This is the same as [`Progress::tick`], but ignores rate throttling
    297    /// and forces the display to be updated immediately.
    298    ///
    299    /// This may be useful for situations where you know you aren't calling
    300    /// `tick` too fast, and accurate information is more important than
    301    /// limiting the console update rate.
    302    pub fn tick_now(&mut self, cur: usize, max: usize, msg: &str) -> CargoResult<()> {
    303        match self.state {
    304            Some(ref mut s) => s.tick(cur, max, msg),
    305            None => Ok(()),
    306        }
    307    }
    308
    309    /// Returns whether or not updates are currently being throttled.
    310    ///
    311    /// This can be useful if computing the values for calling the
    312    /// [`Progress::tick`] function may require some expensive work.
    313    pub fn update_allowed(&mut self) -> bool {
    314        match &mut self.state {
    315            Some(s) => s.throttle.allowed(),
    316            None => false,
    317        }
    318    }
    319
    320    /// Displays progress without a bar.
    321    ///
    322    /// The given `msg` is the text to display after the status message.
    323    ///
    324    /// Example: `Downloading 61 crates, remaining bytes: 28.0 MB`
    325    ///
    326    /// This does not have any rate limit throttling, so be careful about
    327    /// calling it too often.
    328    pub fn print_now(&mut self, msg: &str) -> CargoResult<()> {
    329        match &mut self.state {
    330            Some(s) => s.print(ProgressOutput::PrintNow, msg),
    331            None => Ok(()),
    332        }
    333    }
    334
    335    /// Clears the progress bar from the console.
    336    pub fn clear(&mut self) {
    337        if let Some(ref mut s) = self.state {
    338            s.clear();
    339        }
    340    }
    341
    342    /// Sets the progress reporter to the error state.
    343    pub fn indicate_error(&mut self) {
    344        if let Some(s) = &mut self.state {
    345            s.format.term_integration.error()
    346        }
    347    }
    348}
    349
    350impl Throttle {
    351    fn new() -> Throttle {
    352        Throttle {
    353            first: true,
    354            last_update: Instant::now(),
    355        }
    356    }
    357
    358    fn allowed(&mut self) -> bool {
    359        if self.first {
    360            let delay = Duration::from_millis(500);
    361            if self.last_update.elapsed() < delay {
    362                return false;
    363            }
    364        } else {
    365            let interval = Duration::from_millis(100);
    366            if self.last_update.elapsed() < interval {
    367                return false;
    368            }
    369        }
    370        self.update();
    371        true
    372    }
    373
    374    fn update(&mut self) {
    375        self.first = false;
    376        self.last_update = Instant::now();
    377    }
    378}
    379
    380impl<'gctx> State<'gctx> {
    381    fn tick(&mut self, cur: usize, max: usize, msg: &str) -> CargoResult<()> {
    382        if self.done {
    383            write!(
    384                self.gctx.shell().err(),
    385                "{}",
    386                self.format.term_integration.remove()
    387            )?;
    388            return Ok(());
    389        }
    390
    391        if max > 0 && cur == max {
    392            self.done = true;
    393        }
    394
    395        // Write out a pretty header, then the progress bar itself, and then
    396        // return back to the beginning of the line for the next print.
    397        self.try_update_max_width();
    398        if let Some(pbar) = self.format.progress(cur, max) {
    399            self.print(pbar, msg)?;
    400        }
    401        Ok(())
    402    }
    403
    404    fn print(&mut self, progress: ProgressOutput, msg: &str) -> CargoResult<()> {
    405        self.throttle.update();
    406        self.try_update_max_width();
    407
    408        let (mut line, report) = match progress {
    409            ProgressOutput::PrintNow => (String::new(), None),
    410            ProgressOutput::TextAndReport(prefix, report) => (prefix, Some(report)),
    411            ProgressOutput::Report(report) => (String::new(), Some(report)),
    412        };
    413
    414        // make sure we have enough room for the header
    415        if self.format.max_width < 15 {
    416            // even if we don't have space we can still output progress report
    417            if let Some(tb) = report {
    418                write!(self.gctx.shell().err(), "{tb}\r")?;
    419            }
    420            return Ok(());
    421        }
    422
    423        self.format.render(&mut line, msg);
    424        while line.len() < self.format.max_width - 15 {
    425            line.push(' ');
    426        }
    427
    428        // Only update if the line has changed.
    429        if self.gctx.shell().is_cleared() || self.last_line.as_ref() != Some(&line) {
    430            let mut shell = self.gctx.shell();
    431            shell.set_needs_clear(false);
    432            shell.transient_status(&self.name)?;
    433            if let Some(tb) = report {
    434                write!(shell.err(), "{line}{tb}\r")?;
    435            } else {
    436                write!(shell.err(), "{line}\r")?;
    437            }
    438            self.last_line = Some(line);
    439            shell.set_needs_clear(true);
    440        }
    441
    442        Ok(())
    443    }
    444
    445    fn clear(&mut self) {
    446        // Always clear the progress report
    447        let _ = write!(
    448            self.gctx.shell().err(),
    449            "{}",
    450            self.format.term_integration.remove()
    451        );
    452        // No need to clear if the progress is not currently being displayed.
    453        if self.last_line.is_some() && !self.gctx.shell().is_cleared() {
    454            self.gctx.shell().err_erase_line();
    455            self.last_line = None;
    456        }
    457    }
    458
    459    fn try_update_max_width(&mut self) {
    460        if self.fixed_width.is_none() {
    461            if let Some(n) = self.gctx.shell().err_width().progress_max_width() {
    462                self.format.max_width = n;
    463            }
    464        }
    465    }
    466}
    467
    468impl Format {
    469    fn progress(&self, cur: usize, max: usize) -> Option<ProgressOutput> {
    470        assert!(cur <= max);
    471        // Render the percentage at the far right and then figure how long the
    472        // progress bar is
    473        let pct = (cur as f64) / (max as f64);
    474        let pct = if !pct.is_finite() { 0.0 } else { pct };
    475        let stats = match self.style {
    476            ProgressStyle::Percentage => format!(" {:6.02}%", pct * 100.0),
    477            ProgressStyle::Ratio => format!(" {cur}/{max}"),
    478            ProgressStyle::Indeterminate => String::new(),
    479        };
    480        let report = match self.style {
    481            ProgressStyle::Percentage | ProgressStyle::Ratio => {
    482                self.term_integration.value(pct * 100.0)
    483            }
    484            ProgressStyle::Indeterminate => self.term_integration.indeterminate(),
    485        };
    486
    487        let extra_len = stats.len() + 2 /* [ and ] */ + 15 /* status header */;
    488        let Some(display_width) = self.width().checked_sub(extra_len) else {
    489            if self.term_integration.enabled {
    490                return Some(ProgressOutput::Report(report));
    491            }
    492            return None;
    493        };
    494
    495        let mut string = String::with_capacity(self.max_width);
    496        string.push('[');
    497        let hashes = display_width as f64 * pct;
    498        let hashes = hashes as usize;
    499
    500        // Draw the `===>`
    501        if hashes > 0 {
    502            for _ in 0..hashes - 1 {
    503                string.push('=');
    504            }
    505            if cur == max {
    506                string.push('=');
    507            } else {
    508                string.push('>');
    509            }
    510        }
    511
    512        // Draw the empty space we have left to do
    513        for _ in 0..(display_width - hashes) {
    514            string.push(' ');
    515        }
    516        string.push(']');
    517        string.push_str(&stats);
    518
    519        Some(ProgressOutput::TextAndReport(string, report))
    520    }
    521
    522    fn render(&self, string: &mut String, msg: &str) {
    523        let mut avail_msg_len = self.max_width - string.len() - 15;
    524        let mut ellipsis_pos = 0;
    525
    526        let (ellipsis, ellipsis_width) = if self.unicode { ("…", 1) } else { ("...", 3) };
    527
    528        if avail_msg_len <= ellipsis_width {
    529            return;
    530        }
    531        for c in msg.chars() {
    532            let display_width = c.width().unwrap_or(0);
    533            if avail_msg_len >= display_width {
    534                avail_msg_len -= display_width;
    535                string.push(c);
    536                if avail_msg_len >= ellipsis_width {
    537                    ellipsis_pos = string.len();
    538                }
    539            } else {
    540                string.truncate(ellipsis_pos);
    541                string.push_str(ellipsis);
    542                break;
    543            }
    544        }
    545    }
    546
    547    #[cfg(test)]
    548    fn progress_status(&self, cur: usize, max: usize, msg: &str) -> Option<String> {
    549        let mut ret = match self.progress(cur, max)? {
    550            // Check only the variant that contains text.
    551            ProgressOutput::TextAndReport(text, _) => text,
    552            _ => return None,
    553        };
    554        self.render(&mut ret, msg);
    555        Some(ret)
    556    }
    557
    558    fn width(&self) -> usize {
    559        cmp::min(self.max_width, self.max_print)
    560    }
    561}
    562
    563impl<'gctx> Drop for State<'gctx> {
    564    fn drop(&mut self) {
    565        self.clear();
    566    }
    567}
    568
    569#[test]
    570fn test_progress_status() {
    571    let format = Format {
    572        style: ProgressStyle::Ratio,
    573        max_print: 40,
    574        max_width: 60,
    575        term_integration: TerminalIntegration::new(false),
    576        unicode: true,
    577    };
    578    assert_eq!(
    579        format.progress_status(0, 4, ""),
    580        Some("[                   ] 0/4".to_string())
    581    );
    582    assert_eq!(
    583        format.progress_status(1, 4, ""),
    584        Some("[===>               ] 1/4".to_string())
    585    );
    586    assert_eq!(
    587        format.progress_status(2, 4, ""),
    588        Some("[========>          ] 2/4".to_string())
    589    );
    590    assert_eq!(
    591        format.progress_status(3, 4, ""),
    592        Some("[=============>     ] 3/4".to_string())
    593    );
    594    assert_eq!(
    595        format.progress_status(4, 4, ""),
    596        Some("[===================] 4/4".to_string())
    597    );
    598
    599    assert_eq!(
    600        format.progress_status(3999, 4000, ""),
    601        Some("[===========> ] 3999/4000".to_string())
    602    );
    603    assert_eq!(
    604        format.progress_status(4000, 4000, ""),
    605        Some("[=============] 4000/4000".to_string())
    606    );
    607
    608    assert_eq!(
    609        format.progress_status(3, 4, ": short message"),
    610        Some("[=============>     ] 3/4: short message".to_string())
    611    );
    612    assert_eq!(
    613        format.progress_status(3, 4, ": msg thats just fit"),
    614        Some("[=============>     ] 3/4: msg thats just fit".to_string())
    615    );
    616    assert_eq!(
    617        format.progress_status(3, 4, ": msg that's just fit"),
    618        Some("[=============>     ] 3/4: msg that's just f…".to_string())
    619    );
    620
    621    // combining diacritics have width zero and thus can fit max_width.
    622    let zalgo_msg = "z̸̧̢̗͉̝̦͍̱ͧͦͨ̑̅̌ͥ́͢a̢ͬͨ̽ͯ̅̑ͥ͋̏̑ͫ̄͢͏̫̝̪̤͎̱̣͍̭̞̙̱͙͍̘̭͚l̶̡̛̥̝̰̭̹̯̯̞̪͇̱̦͙͔̘̼͇͓̈ͨ͗ͧ̓͒ͦ̀̇ͣ̈ͭ͊͛̃̑͒̿̕͜g̸̷̢̩̻̻͚̠͓̞̥͐ͩ͌̑ͥ̊̽͋͐̐͌͛̐̇̑ͨ́ͅo͙̳̣͔̰̠̜͕͕̞̦̙̭̜̯̹̬̻̓͑ͦ͋̈̉͌̃ͯ̀̂͠ͅ ̸̡͎̦̲̖̤̺̜̮̱̰̥͔̯̅̏ͬ̂ͨ̋̃̽̈́̾̔̇ͣ̚͜͜h̡ͫ̐̅̿̍̀͜҉̛͇̭̹̰̠͙̞ẽ̶̙̹̳̖͉͎̦͂̋̓ͮ̔ͬ̐̀͂̌͑̒͆̚͜͠ ͓͓̟͍̮̬̝̝̰͓͎̼̻ͦ͐̾̔͒̃̓͟͟c̮̦͍̺͈͚̯͕̄̒͐̂͊̊͗͊ͤͣ̀͘̕͝͞o̶͍͚͍̣̮͌ͦ̽̑ͩ̅ͮ̐̽̏͗́͂̅ͪ͠m̷̧͖̻͔̥̪̭͉͉̤̻͖̩̤͖̘ͦ̂͌̆̂ͦ̒͊ͯͬ͊̉̌ͬ͝͡e̵̹̣͍̜̺̤̤̯̫̹̠̮͎͙̯͚̰̼͗͐̀̒͂̉̀̚͝͞s̵̲͍͙͖̪͓͓̺̱̭̩̣͖̣ͤͤ͂̎̈͗͆ͨͪ̆̈͗͝͠";
    623    assert_eq!(
    624        format.progress_status(3, 4, zalgo_msg),
    625        Some("[=============>     ] 3/4".to_string() + zalgo_msg)
    626    );
    627
    628    // some non-ASCII ellipsize test
    629    assert_eq!(
    630        format.progress_status(3, 4, "_123456789123456e\u{301}\u{301}8\u{301}90a"),
    631        Some("[=============>     ] 3/4_123456789123456e\u{301}\u{301}8\u{301}9…".to_string())
    632    );
    633    assert_eq!(
    634        format.progress_status(3, 4, "：每個漢字佔據了兩個字元"),
    635        Some("[=============>     ] 3/4：每個漢字佔據了兩…".to_string())
    636    );
    637    assert_eq!(
    638        // handle breaking at middle of character
    639        format.progress_status(3, 4, "：-每個漢字佔據了兩個字元"),
    640        Some("[=============>     ] 3/4：-每個漢字佔據了兩…".to_string())
    641    );
    642}
    643
    644#[test]
    645fn test_progress_status_percentage() {
    646    let format = Format {
    647        style: ProgressStyle::Percentage,
    648        max_print: 40,
    649        max_width: 60,
    650        term_integration: TerminalIntegration::new(false),
    651        unicode: true,
    652    };
    653    assert_eq!(
    654        format.progress_status(0, 77, ""),
    655        Some("[               ]   0.00%".to_string())
    656    );
    657    assert_eq!(
    658        format.progress_status(1, 77, ""),
    659        Some("[               ]   1.30%".to_string())
    660    );
    661    assert_eq!(
    662        format.progress_status(76, 77, ""),
    663        Some("[=============> ]  98.70%".to_string())
    664    );
    665    assert_eq!(
    666        format.progress_status(77, 77, ""),
    667        Some("[===============] 100.00%".to_string())
    668    );
    669}
    670
    671#[test]
    672fn test_progress_status_too_short() {
    673    let format = Format {
    674        style: ProgressStyle::Percentage,
    675        max_print: 25,
    676        max_width: 25,
    677        term_integration: TerminalIntegration::new(false),
    678        unicode: true,
    679    };
    680    assert_eq!(
    681        format.progress_status(1, 1, ""),
    682        Some("[] 100.00%".to_string())
    683    );
    684
    685    let format = Format {
    686        style: ProgressStyle::Percentage,
    687        max_print: 24,
    688        max_width: 24,
    689        term_integration: TerminalIntegration::new(false),
    690        unicode: true,
    691    };
    692    assert_eq!(format.progress_status(1, 1, ""), None);
    693}
    694
    695#[test]
    696fn test_term_integration_disabled() {
    697    let report = TerminalIntegration::new(false);
    698    let mut out = String::new();
    699    out.push_str(&report.remove().to_string());
    700    out.push_str(&report.value(10.0).to_string());
    701    out.push_str(&report.indeterminate().to_string());
    702    assert!(out.is_empty());
    703}
    704
    705#[test]
    706fn test_term_integration_error_state() {
    707    let mut report = TerminalIntegration::new(true);
    708    assert_eq!(report.value(10.0), StatusValue::Value(10.0));
    709    report.error();
    710    assert_eq!(report.value(50.0), StatusValue::Error(50.0));
    711}