]> git.proxmox.com Git - cargo.git/blobdiff - src/cargo/util/progress.rs
Upgrade to Rust 2018
[cargo.git] / src / cargo / util / progress.rs
index 6c3addadefbc59980db09b005dfb94168951da4a..21429a3aa2ee80c633ee0e70a4688512d9cff3db 100644 (file)
@@ -1,62 +1,85 @@
 use std::cmp;
 use std::env;
-use std::iter;
-use std::time::{Instant, Duration};
+use std::time::{Duration, Instant};
 
-use core::shell::Verbosity;
-use util::{Config, CargoResult};
+use crate::core::shell::Verbosity;
+use crate::util::{CargoResult, Config};
+
+use unicode_width::UnicodeWidthChar;
 
 pub struct Progress<'cfg> {
     state: Option<State<'cfg>>,
 }
 
-struct State<'cfg> {
-    config: &'cfg Config,
-    width: usize,
+pub enum ProgressStyle {
+    Percentage,
+    Ratio,
+}
+
+struct Throttle {
     first: bool,
     last_update: Instant,
+}
+
+struct State<'cfg> {
+    config: &'cfg Config,
+    format: Format,
     name: String,
     done: bool,
+    throttle: Throttle,
+}
+
+struct Format {
+    style: ProgressStyle,
+    max_width: usize,
+    max_print: usize,
 }
 
 impl<'cfg> Progress<'cfg> {
-    pub fn new(name: &str, cfg: &'cfg Config) -> Progress<'cfg> {
+    pub fn with_style(name: &str, style: ProgressStyle, cfg: &'cfg Config) -> Progress<'cfg> {
         // report no progress when -q (for quiet) or TERM=dumb are set
+        // or if running on Continuous Integration service like Travis where the
+        // output logs get mangled.
         let dumb = match env::var("TERM") {
             Ok(term) => term == "dumb",
             Err(_) => false,
         };
-        if cfg.shell().verbosity() == Verbosity::Quiet || dumb {
-            return Progress { state: None }
+        if cfg.shell().verbosity() == Verbosity::Quiet || dumb || env::var("CI").is_ok() {
+            return Progress { state: None };
         }
 
         Progress {
-            state: cfg.shell().err_width().map(|n| {
-                State {
-                    config: cfg,
-                    width: cmp::min(n, 80),
-                    first: true,
-                    last_update: Instant::now(),
-                    name: name.to_string(),
-                    done: false,
-                }
+            state: cfg.shell().err_width().map(|n| State {
+                config: cfg,
+                format: Format {
+                    style,
+                    max_width: n,
+                    max_print: 80,
+                },
+                name: name.to_string(),
+                done: false,
+                throttle: Throttle::new(),
             }),
         }
     }
 
-    pub fn tick(&mut self, cur: usize, max: usize) -> CargoResult<()> {
-        match self.state {
-            Some(ref mut s) => s.tick(cur, max),
-            None => Ok(())
-        }
+    pub fn disable(&mut self) {
+        self.state = None;
     }
-}
 
-impl<'cfg> State<'cfg> {
-    fn tick(&mut self, cur: usize, max: usize) -> CargoResult<()> {
-        if self.done {
-            return Ok(())
-        }
+    pub fn is_enabled(&self) -> bool {
+        self.state.is_some()
+    }
+
+    pub fn new(name: &str, cfg: &'cfg Config) -> Progress<'cfg> {
+        Self::with_style(name, ProgressStyle::Percentage, cfg)
+    }
+
+    pub fn tick(&mut self, cur: usize, max: usize) -> CargoResult<()> {
+        let s = match &mut self.state {
+            Some(s) => s,
+            None => return Ok(()),
+        };
 
         // Don't update too often as it can cause excessive performance loss
         // just putting stuff onto the terminal. We also want to avoid
@@ -70,41 +93,148 @@ impl<'cfg> State<'cfg> {
         // 2. If we've drawn something, then we rate limit ourselves to only
         //    draw to the console every so often. Currently there's a 100ms
         //    delay between updates.
+        if !s.throttle.allowed() {
+            return Ok(())
+        }
+
+        s.tick(cur, max, "")
+    }
+
+    pub fn tick_now(&mut self, cur: usize, max: usize, msg: &str) -> CargoResult<()> {
+        match self.state {
+            Some(ref mut s) => s.tick(cur, max, msg),
+            None => Ok(()),
+        }
+    }
+
+    pub fn update_allowed(&mut self) -> bool {
+        match &mut self.state {
+            Some(s) => s.throttle.allowed(),
+            None => false,
+        }
+    }
+
+    pub fn print_now(&mut self, msg: &str) -> CargoResult<()> {
+        match &mut self.state {
+            Some(s) => s.print("", msg),
+            None => Ok(()),
+        }
+    }
+
+    pub fn clear(&mut self) {
+        if let Some(ref mut s) = self.state {
+            s.clear();
+        }
+    }
+}
+
+impl Throttle {
+    fn new() -> Throttle {
+        Throttle {
+            first: true,
+            last_update: Instant::now(),
+        }
+    }
+
+    fn allowed(&mut self) -> bool {
         if self.first {
             let delay = Duration::from_millis(500);
             if self.last_update.elapsed() < delay {
-                return Ok(())
+                return false
             }
-            self.first = false;
         } else {
             let interval = Duration::from_millis(100);
             if self.last_update.elapsed() < interval {
-                return Ok(())
+                return false
             }
         }
+        self.update();
+        true
+    }
+
+    fn update(&mut self) {
+        self.first = false;
         self.last_update = Instant::now();
+    }
+}
+
+impl<'cfg> State<'cfg> {
+    fn tick(&mut self, cur: usize, max: usize, msg: &str) -> CargoResult<()> {
+        if self.done {
+            return Ok(());
+        }
+
+        if max > 0 && cur == max {
+            self.done = true;
+        }
+
+        // Write out a pretty header, then the progress bar itself, and then
+        // return back to the beginning of the line for the next print.
+        self.try_update_max_width();
+        if let Some(pbar) = self.format.progress(cur, max) {
+            self.print(&pbar, msg)?;
+        }
+        Ok(())
+    }
+
+    fn print(&mut self, prefix: &str, msg: &str) -> CargoResult<()> {
+        self.throttle.update();
+        self.try_update_max_width();
+
+        // make sure we have enough room for the header
+        if self.format.max_width < 15 {
+            return Ok(())
+        }
+        self.config.shell().status_header(&self.name)?;
+        let mut line = prefix.to_string();
+        self.format.render(&mut line, msg);
+
+        while line.len() < self.format.max_width - 15 {
+            line.push(' ');
+        }
+
+        write!(self.config.shell().err(), "{}\r", line)?;
+        Ok(())
+    }
 
+    fn clear(&mut self) {
+        self.config.shell().err_erase_line();
+    }
+
+    fn try_update_max_width(&mut self) {
+        if let Some(n) = self.config.shell().err_width() {
+            self.format.max_width = n;
+        }
+    }
+}
+
+impl Format {
+    fn progress(&self, cur: usize, max: usize) -> Option<String> {
         // Render the percentage at the far right and then figure how long the
         // progress bar is
         let pct = (cur as f64) / (max as f64);
         let pct = if !pct.is_finite() { 0.0 } else { pct };
-        let stats = format!(" {:6.02}%", pct * 100.0);
+        let stats = match self.style {
+            ProgressStyle::Percentage => format!(" {:6.02}%", pct * 100.0),
+            ProgressStyle::Ratio => format!(" {}/{}", cur, max),
+        };
         let extra_len = stats.len() + 2 /* [ and ] */ + 15 /* status header */;
-        let display_width = match self.width.checked_sub(extra_len) {
+        let display_width = match self.width().checked_sub(extra_len) {
             Some(n) => n,
-            None => return Ok(()),
+            None => return None,
         };
-        let mut string = String::from("[");
+
+        let mut string = String::with_capacity(self.max_width);
+        string.push('[');
         let hashes = display_width as f64 * pct;
         let hashes = hashes as usize;
 
         // Draw the `===>`
         if hashes > 0 {
-            for _ in 0..hashes-1 {
+            for _ in 0..hashes - 1 {
                 string.push_str("=");
             }
             if cur == max {
-                self.done = true;
                 string.push_str("=");
             } else {
                 string.push_str(">");
@@ -118,21 +248,161 @@ impl<'cfg> State<'cfg> {
         string.push_str("]");
         string.push_str(&stats);
 
-        // Write out a pretty header, then the progress bar itself, and then
-        // return back to the beginning of the line for the next print.
-        self.config.shell().status_header(&self.name)?;
-        write!(self.config.shell().err(), "{}\r", string)?;
-        Ok(())
+        Some(string)
+    }
+
+    fn render(&self, string: &mut String, msg: &str) {
+        let mut avail_msg_len = self.max_width - string.len() - 15;
+        let mut ellipsis_pos = 0;
+        if avail_msg_len <= 3 {
+            return
+        }
+        for c in msg.chars() {
+            let display_width = c.width().unwrap_or(0);
+            if avail_msg_len >= display_width {
+                avail_msg_len -= display_width;
+                string.push(c);
+                if avail_msg_len >= 3 {
+                    ellipsis_pos = string.len();
+                }
+            } else {
+                string.truncate(ellipsis_pos);
+                string.push_str("...");
+                break;
+            }
+        }
     }
-}
 
-fn clear(width: usize, config: &Config) {
-    let blank = iter::repeat(" ").take(width).collect::<String>();
-    drop(write!(config.shell().err(), "{}\r", blank));
+    #[cfg(test)]
+    fn progress_status(&self, cur: usize, max: usize, msg: &str) -> Option<String> {
+        let mut ret = self.progress(cur, max)?;
+        self.render(&mut ret, msg);
+        Some(ret)
+    }
+
+    fn width(&self) -> usize {
+        cmp::min(self.max_width, self.max_print)
+    }
 }
 
 impl<'cfg> Drop for State<'cfg> {
     fn drop(&mut self) {
-        clear(self.width, self.config);
+        self.clear();
     }
 }
+
+#[test]
+fn test_progress_status() {
+    let format = Format {
+        style: ProgressStyle::Ratio,
+        max_print: 40,
+        max_width: 60,
+    };
+    assert_eq!(
+        format.progress_status(0, 4, ""),
+        Some("[                   ] 0/4".to_string())
+    );
+    assert_eq!(
+        format.progress_status(1, 4, ""),
+        Some("[===>               ] 1/4".to_string())
+    );
+    assert_eq!(
+        format.progress_status(2, 4, ""),
+        Some("[========>          ] 2/4".to_string())
+    );
+    assert_eq!(
+        format.progress_status(3, 4, ""),
+        Some("[=============>     ] 3/4".to_string())
+    );
+    assert_eq!(
+        format.progress_status(4, 4, ""),
+        Some("[===================] 4/4".to_string())
+    );
+
+    assert_eq!(
+        format.progress_status(3999, 4000, ""),
+        Some("[===========> ] 3999/4000".to_string())
+    );
+    assert_eq!(
+        format.progress_status(4000, 4000, ""),
+        Some("[=============] 4000/4000".to_string())
+    );
+
+    assert_eq!(
+        format.progress_status(3, 4, ": short message"),
+        Some("[=============>     ] 3/4: short message".to_string())
+    );
+    assert_eq!(
+        format.progress_status(3, 4, ": msg thats just fit"),
+        Some("[=============>     ] 3/4: msg thats just fit".to_string())
+    );
+    assert_eq!(
+        format.progress_status(3, 4, ": msg that's just fit"),
+        Some("[=============>     ] 3/4: msg that's just...".to_string())
+    );
+
+    // combining diacritics have width zero and thus can fit max_width.
+    let zalgo_msg = "z̸̧̢̗͉̝̦͍̱ͧͦͨ̑̅̌ͥ́͢a̢ͬͨ̽ͯ̅̑ͥ͋̏̑ͫ̄͢͏̫̝̪̤͎̱̣͍̭̞̙̱͙͍̘̭͚l̶̡̛̥̝̰̭̹̯̯̞̪͇̱̦͙͔̘̼͇͓̈ͨ͗ͧ̓͒ͦ̀̇ͣ̈ͭ͊͛̃̑͒̿̕͜g̸̷̢̩̻̻͚̠͓̞̥͐ͩ͌̑ͥ̊̽͋͐̐͌͛̐̇̑ͨ́ͅo͙̳̣͔̰̠̜͕͕̞̦̙̭̜̯̹̬̻̓͑ͦ͋̈̉͌̃ͯ̀̂͠ͅ ̸̡͎̦̲̖̤̺̜̮̱̰̥͔̯̅̏ͬ̂ͨ̋̃̽̈́̾̔̇ͣ̚͜͜h̡ͫ̐̅̿̍̀͜҉̛͇̭̹̰̠͙̞ẽ̶̙̹̳̖͉͎̦͂̋̓ͮ̔ͬ̐̀͂̌͑̒͆̚͜͠ ͓͓̟͍̮̬̝̝̰͓͎̼̻ͦ͐̾̔͒̃̓͟͟c̮̦͍̺͈͚̯͕̄̒͐̂͊̊͗͊ͤͣ̀͘̕͝͞o̶͍͚͍̣̮͌ͦ̽̑ͩ̅ͮ̐̽̏͗́͂̅ͪ͠m̷̧͖̻͔̥̪̭͉͉̤̻͖̩̤͖̘ͦ̂͌̆̂ͦ̒͊ͯͬ͊̉̌ͬ͝͡e̵̹̣͍̜̺̤̤̯̫̹̠̮͎͙̯͚̰̼͗͐̀̒͂̉̀̚͝͞s̵̲͍͙͖̪͓͓̺̱̭̩̣͖̣ͤͤ͂̎̈͗͆ͨͪ̆̈͗͝͠";
+    assert_eq!(
+        format.progress_status(3, 4, zalgo_msg),
+        Some("[=============>     ] 3/4".to_string() + zalgo_msg)
+    );
+
+    // some non-ASCII ellipsize test
+    assert_eq!(
+        format.progress_status(3, 4, "_123456789123456e\u{301}\u{301}8\u{301}90a"),
+        Some("[=============>     ] 3/4_123456789123456e\u{301}\u{301}...".to_string())
+    );
+    assert_eq!(
+        format.progress_status(3, 4, ":每個漢字佔據了兩個字元"),
+        Some("[=============>     ] 3/4:每個漢字佔據了...".to_string())
+    );
+}
+
+#[test]
+fn test_progress_status_percentage() {
+    let format = Format {
+        style: ProgressStyle::Percentage,
+        max_print: 40,
+        max_width: 60,
+    };
+    assert_eq!(
+        format.progress_status(0, 77, ""),
+        Some("[               ]   0.00%".to_string())
+    );
+    assert_eq!(
+        format.progress_status(1, 77, ""),
+        Some("[               ]   1.30%".to_string())
+    );
+    assert_eq!(
+        format.progress_status(76, 77, ""),
+        Some("[=============> ]  98.70%".to_string())
+    );
+    assert_eq!(
+        format.progress_status(77, 77, ""),
+        Some("[===============] 100.00%".to_string())
+    );
+}
+
+#[test]
+fn test_progress_status_too_short() {
+    let format = Format {
+        style: ProgressStyle::Percentage,
+        max_print: 25,
+        max_width: 25,
+    };
+    assert_eq!(
+        format.progress_status(1, 1, ""),
+        Some("[] 100.00%".to_string())
+    );
+
+    let format = Format {
+        style: ProgressStyle::Percentage,
+        max_print: 24,
+        max_width: 24,
+    };
+    assert_eq!(
+        format.progress_status(1, 1, ""),
+        None
+    );
+}