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