From 3dbe701f0b43308a77ea474a62fb47edba7fd7b0 Mon Sep 17 00:00:00 2001 From: kennytm Date: Sat, 9 Jun 2018 04:25:13 +0800 Subject: [PATCH 1/3] Displays a one line progress of what crates are currently built. cc #2536, #3448. --- src/cargo/core/compiler/job_queue.rs | 39 +++++++++++++------ src/cargo/util/progress.rs | 56 ++++++++++++++++++++-------- 2 files changed, 67 insertions(+), 28 deletions(-) diff --git a/src/cargo/core/compiler/job_queue.rs b/src/cargo/core/compiler/job_queue.rs index de633e7af37..4c5e24f921e 100644 --- a/src/cargo/core/compiler/job_queue.rs +++ b/src/cargo/core/compiler/job_queue.rs @@ -14,6 +14,7 @@ use core::{PackageId, Target}; use handle_error; use util::{internal, profile, CargoResult, CargoResultExt, ProcessBuilder}; use util::{Config, DependencyQueue, Dirty, Fresh, Freshness}; +use util::Progress; use super::job::Job; use super::{BuildContext, BuildPlan, CompileMode, Context, Kind, Unit}; @@ -28,7 +29,7 @@ pub struct JobQueue<'a> { queue: DependencyQueue, Vec<(Job, Freshness)>>, tx: Sender>, rx: Receiver>, - active: usize, + active: HashSet>, pending: HashMap, PendingBuild>, compiled: HashSet<&'a PackageId>, documented: HashSet<&'a PackageId>, @@ -98,7 +99,7 @@ impl<'a> JobQueue<'a> { queue: DependencyQueue::new(), tx, rx, - active: 0, + active: HashSet::new(), pending: HashMap::new(), compiled: HashSet::new(), documented: HashSet::new(), @@ -180,6 +181,8 @@ impl<'a> JobQueue<'a> { // successful and otherwise wait for pending work to finish if it failed // and then immediately return. let mut error = None; + let mut progress = Progress::new("Building", cx.bcx.config); + let queue_len = self.queue.len(); loop { // Dequeue as much work as we can, learning about everything // possible that can run. Note that this is also the point where we @@ -196,7 +199,7 @@ impl<'a> JobQueue<'a> { ); for (job, f) in jobs { queue.push((key, job, f.combine(fresh))); - if self.active + queue.len() > 0 { + if !self.active.is_empty() || !queue.is_empty() { jobserver_helper.request_token(); } } @@ -205,14 +208,14 @@ impl<'a> JobQueue<'a> { // Now that we've learned of all possible work that we can execute // try to spawn it so long as we've got a jobserver token which says // we're able to perform some parallel work. - while error.is_none() && self.active < tokens.len() + 1 && !queue.is_empty() { + while error.is_none() && self.active.len() < tokens.len() + 1 && !queue.is_empty() { let (key, job, fresh) = queue.remove(0); self.run(key, fresh, job, cx.bcx.config, scope, build_plan)?; } // If after all that we're not actually running anything then we're // done! - if self.active == 0 { + if self.active.is_empty() { break; } @@ -221,9 +224,19 @@ impl<'a> JobQueue<'a> { // jobserver interface is architected we may acquire a token that we // don't actually use, and if this happens just relinquish it back // to the jobserver itself. - tokens.truncate(self.active - 1); - - match self.rx.recv().unwrap() { + tokens.truncate(self.active.len() - 1); + + let count = queue_len - self.queue.len(); + let mut active_names = self.active.iter().map(|key| match key.mode { + CompileMode::Doc { .. } => format!("{}(doc)", key.pkg.name()), + _ => key.pkg.name().to_string(), + }).collect::>(); + active_names.sort_unstable(); + drop(progress.tick_now(count, queue_len, format!(": {}", active_names.join(", ")))); + let event = self.rx.recv().unwrap(); + progress.clear(); + + match event { Message::Run(cmd) => { cx.bcx .config @@ -245,8 +258,9 @@ impl<'a> JobQueue<'a> { } Message::Finish(key, result) => { info!("end: {:?}", key); - self.active -= 1; - if self.active > 0 { + + self.active.remove(&key); + if !self.active.is_empty() { assert!(!tokens.is_empty()); drop(tokens.pop()); } @@ -256,7 +270,7 @@ impl<'a> JobQueue<'a> { let msg = "The following warnings were emitted during compilation:"; self.emit_warnings(Some(msg), &key, cx)?; - if self.active > 0 { + if !self.active.is_empty() { error = Some(format_err!("build failed")); handle_error(e, &mut *cx.bcx.config.shell()); cx.bcx.config.shell().warn( @@ -274,6 +288,7 @@ impl<'a> JobQueue<'a> { } } } + drop(progress); let build_type = if self.is_release { "release" } else { "dev" }; // NOTE: This may be a bit inaccurate, since this may not display the @@ -334,7 +349,7 @@ impl<'a> JobQueue<'a> { ) -> CargoResult<()> { info!("start: {:?}", key); - self.active += 1; + self.active.insert(key); *self.counts.get_mut(key.pkg).unwrap() -= 1; let my_tx = self.tx.clone(); diff --git a/src/cargo/util/progress.rs b/src/cargo/util/progress.rs index 0a5af5b5eb1..c33512152aa 100644 --- a/src/cargo/util/progress.rs +++ b/src/cargo/util/progress.rs @@ -1,6 +1,5 @@ use std::cmp; use std::env; -use std::iter; use std::time::{Duration, Instant}; use core::shell::Verbosity; @@ -12,6 +11,7 @@ pub struct Progress<'cfg> { struct State<'cfg> { config: &'cfg Config, + max_width: usize, width: usize, first: bool, last_update: Instant, @@ -33,6 +33,7 @@ impl<'cfg> Progress<'cfg> { Progress { state: cfg.shell().err_width().map(|n| State { config: cfg, + max_width: n, width: cmp::min(n, 80), first: true, last_update: Instant::now(), @@ -44,14 +45,27 @@ impl<'cfg> Progress<'cfg> { pub fn tick(&mut self, cur: usize, max: usize) -> CargoResult<()> { match self.state { - Some(ref mut s) => s.tick(cur, max), + Some(ref mut s) => s.tick(cur, max, String::new(), true), + None => Ok(()), + } + } + + pub fn clear(&mut self) { + if let Some(ref mut s) = self.state { + clear(s.max_width, s.config); + } + } + + pub fn tick_now(&mut self, cur: usize, max: usize, msg: String) -> CargoResult<()> { + match self.state { + Some(ref mut s) => s.tick(cur, max, msg, false), None => Ok(()), } } } impl<'cfg> State<'cfg> { - fn tick(&mut self, cur: usize, max: usize) -> CargoResult<()> { + fn tick(&mut self, cur: usize, max: usize, msg: String, throttle: bool) -> CargoResult<()> { if self.done { return Ok(()); } @@ -68,19 +82,21 @@ 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 self.first { - let delay = Duration::from_millis(500); - if self.last_update.elapsed() < delay { - return Ok(()); - } - self.first = false; - } else { - let interval = Duration::from_millis(100); - if self.last_update.elapsed() < interval { - return Ok(()); + if throttle { + if self.first { + let delay = Duration::from_millis(500); + if self.last_update.elapsed() < delay { + return Ok(()); + } + self.first = false; + } else { + let interval = Duration::from_millis(100); + if self.last_update.elapsed() < interval { + return Ok(()); + } } + self.last_update = Instant::now(); } - self.last_update = Instant::now(); // Render the percentage at the far right and then figure how long the // progress bar is @@ -116,6 +132,14 @@ impl<'cfg> State<'cfg> { string.push_str("]"); string.push_str(&stats); + let avail_msg_len = self.max_width - self.width; + if avail_msg_len >= msg.len() + 3 { + string.push_str(&msg); + } else if avail_msg_len >= 4 { + string.push_str(&msg[..(avail_msg_len - 3)]); + string.push_str("..."); + } + // 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)?; @@ -125,12 +149,12 @@ impl<'cfg> State<'cfg> { } fn clear(width: usize, config: &Config) { - let blank = iter::repeat(" ").take(width).collect::(); + let blank = " ".repeat(width); drop(write!(config.shell().err(), "{}\r", blank)); } impl<'cfg> Drop for State<'cfg> { fn drop(&mut self) { - clear(self.width, self.config); + clear(self.max_width, self.config); } } From a753b50087e6bd49c065e15bae833bcccad3490a Mon Sep 17 00:00:00 2001 From: kennytm Date: Fri, 29 Jun 2018 05:15:59 +0800 Subject: [PATCH 2/3] Addressed comments. --- src/cargo/core/compiler/job_queue.rs | 18 ++++++++++-------- src/cargo/util/mod.rs | 2 +- src/cargo/util/progress.rs | 18 ++++++++++++++++-- 3 files changed, 27 insertions(+), 11 deletions(-) diff --git a/src/cargo/core/compiler/job_queue.rs b/src/cargo/core/compiler/job_queue.rs index 4c5e24f921e..c9a651ed09b 100644 --- a/src/cargo/core/compiler/job_queue.rs +++ b/src/cargo/core/compiler/job_queue.rs @@ -14,7 +14,7 @@ use core::{PackageId, Target}; use handle_error; use util::{internal, profile, CargoResult, CargoResultExt, ProcessBuilder}; use util::{Config, DependencyQueue, Dirty, Fresh, Freshness}; -use util::Progress; +use util::{Progress, ProgressStyle}; use super::job::Job; use super::{BuildContext, BuildPlan, CompileMode, Context, Kind, Unit}; @@ -29,7 +29,7 @@ pub struct JobQueue<'a> { queue: DependencyQueue, Vec<(Job, Freshness)>>, tx: Sender>, rx: Receiver>, - active: HashSet>, + active: Vec>, pending: HashMap, PendingBuild>, compiled: HashSet<&'a PackageId>, documented: HashSet<&'a PackageId>, @@ -99,7 +99,7 @@ impl<'a> JobQueue<'a> { queue: DependencyQueue::new(), tx, rx, - active: HashSet::new(), + active: Vec::new(), pending: HashMap::new(), compiled: HashSet::new(), documented: HashSet::new(), @@ -181,7 +181,7 @@ impl<'a> JobQueue<'a> { // successful and otherwise wait for pending work to finish if it failed // and then immediately return. let mut error = None; - let mut progress = Progress::new("Building", cx.bcx.config); + let mut progress = Progress::with_style("Building", ProgressStyle::Ratio, cx.bcx.config); let queue_len = self.queue.len(); loop { // Dequeue as much work as we can, learning about everything @@ -227,11 +227,10 @@ impl<'a> JobQueue<'a> { tokens.truncate(self.active.len() - 1); let count = queue_len - self.queue.len(); - let mut active_names = self.active.iter().map(|key| match key.mode { + let active_names = self.active.iter().map(|key| match key.mode { CompileMode::Doc { .. } => format!("{}(doc)", key.pkg.name()), _ => key.pkg.name().to_string(), }).collect::>(); - active_names.sort_unstable(); drop(progress.tick_now(count, queue_len, format!(": {}", active_names.join(", ")))); let event = self.rx.recv().unwrap(); progress.clear(); @@ -259,7 +258,10 @@ impl<'a> JobQueue<'a> { Message::Finish(key, result) => { info!("end: {:?}", key); - self.active.remove(&key); + // self.active.remove_item(&key); // <- switch to this when stabilized. + if let Some(pos) = self.active.iter().position(|k| *k == key) { + self.active.remove(pos); + } if !self.active.is_empty() { assert!(!tokens.is_empty()); drop(tokens.pop()); @@ -349,7 +351,7 @@ impl<'a> JobQueue<'a> { ) -> CargoResult<()> { info!("start: {:?}", key); - self.active.insert(key); + self.active.push(key); *self.counts.get_mut(key.pkg).unwrap() -= 1; let my_tx = self.tx.clone(); diff --git a/src/cargo/util/mod.rs b/src/cargo/util/mod.rs index 2d9505d9a75..a553601f676 100644 --- a/src/cargo/util/mod.rs +++ b/src/cargo/util/mod.rs @@ -17,7 +17,7 @@ pub use self::to_semver::ToSemver; pub use self::to_url::ToUrl; pub use self::vcs::{FossilRepo, GitRepo, HgRepo, PijulRepo}; pub use self::read2::read2; -pub use self::progress::Progress; +pub use self::progress::{Progress, ProgressStyle}; pub mod config; pub mod errors; diff --git a/src/cargo/util/progress.rs b/src/cargo/util/progress.rs index c33512152aa..f4135bddf81 100644 --- a/src/cargo/util/progress.rs +++ b/src/cargo/util/progress.rs @@ -9,8 +9,14 @@ pub struct Progress<'cfg> { state: Option>, } +pub enum ProgressStyle { + Percentage, + Ratio, +} + struct State<'cfg> { config: &'cfg Config, + style: ProgressStyle, max_width: usize, width: usize, first: bool, @@ -20,7 +26,7 @@ struct State<'cfg> { } 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 let dumb = match env::var("TERM") { Ok(term) => term == "dumb", @@ -33,6 +39,7 @@ impl<'cfg> Progress<'cfg> { Progress { state: cfg.shell().err_width().map(|n| State { config: cfg, + style, max_width: n, width: cmp::min(n, 80), first: true, @@ -43,6 +50,10 @@ impl<'cfg> Progress<'cfg> { } } + 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<()> { match self.state { Some(ref mut s) => s.tick(cur, max, String::new(), true), @@ -102,7 +113,10 @@ impl<'cfg> State<'cfg> { // 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) { Some(n) => n, From a8081a007e9476cd32c98c5b5e08027c4a345004 Mon Sep 17 00:00:00 2001 From: kennytm Date: Sat, 30 Jun 2018 01:39:35 +0800 Subject: [PATCH 3/3] More fixes. --- Cargo.toml | 1 + src/cargo/core/compiler/job_queue.rs | 15 +- src/cargo/lib.rs | 1 + src/cargo/util/progress.rs | 198 +++++++++++++++++++++++---- 4 files changed, 184 insertions(+), 31 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9042c260a17..3c77e6d70f8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,6 +53,7 @@ termcolor = "0.3" toml = "0.4.2" url = "1.1" clap = "2.31.2" +unicode-width = "0.1.5" # Not actually needed right now but required to make sure that rls/cargo build # with the same set of features in rust-lang/rust diff --git a/src/cargo/core/compiler/job_queue.rs b/src/cargo/core/compiler/job_queue.rs index c9a651ed09b..56fdee74682 100644 --- a/src/cargo/core/compiler/job_queue.rs +++ b/src/cargo/core/compiler/job_queue.rs @@ -182,7 +182,7 @@ impl<'a> JobQueue<'a> { // and then immediately return. let mut error = None; let mut progress = Progress::with_style("Building", ProgressStyle::Ratio, cx.bcx.config); - let queue_len = self.queue.len(); + let total = self.queue.len(); loop { // Dequeue as much work as we can, learning about everything // possible that can run. Note that this is also the point where we @@ -226,12 +226,12 @@ impl<'a> JobQueue<'a> { // to the jobserver itself. tokens.truncate(self.active.len() - 1); - let count = queue_len - self.queue.len(); + let count = total - self.queue.len(); let active_names = self.active.iter().map(|key| match key.mode { CompileMode::Doc { .. } => format!("{}(doc)", key.pkg.name()), _ => key.pkg.name().to_string(), }).collect::>(); - drop(progress.tick_now(count, queue_len, format!(": {}", active_names.join(", ")))); + drop(progress.tick_now(count, total, &format!(": {}", active_names.join(", ")))); let event = self.rx.recv().unwrap(); progress.clear(); @@ -259,9 +259,12 @@ impl<'a> JobQueue<'a> { info!("end: {:?}", key); // self.active.remove_item(&key); // <- switch to this when stabilized. - if let Some(pos) = self.active.iter().position(|k| *k == key) { - self.active.remove(pos); - } + let pos = self + .active + .iter() + .position(|k| *k == key) + .expect("an unrecorded package has finished compiling"); + self.active.remove(pos); if !self.active.is_empty() { assert!(!tokens.is_empty()); drop(tokens.pop()); diff --git a/src/cargo/lib.rs b/src/cargo/lib.rs index e51dfe87a05..d99cea32f35 100644 --- a/src/cargo/lib.rs +++ b/src/cargo/lib.rs @@ -44,6 +44,7 @@ extern crate tar; extern crate tempfile; extern crate termcolor; extern crate toml; +extern crate unicode_width; extern crate url; use std::fmt; diff --git a/src/cargo/util/progress.rs b/src/cargo/util/progress.rs index f4135bddf81..8a376d189a8 100644 --- a/src/cargo/util/progress.rs +++ b/src/cargo/util/progress.rs @@ -5,6 +5,8 @@ use std::time::{Duration, Instant}; use core::shell::Verbosity; use util::{CargoResult, Config}; +use unicode_width::UnicodeWidthChar; + pub struct Progress<'cfg> { state: Option>, } @@ -16,15 +18,19 @@ pub enum ProgressStyle { struct State<'cfg> { config: &'cfg Config, - style: ProgressStyle, - max_width: usize, - width: usize, + format: Format, first: bool, last_update: Instant, name: String, done: bool, } +struct Format { + style: ProgressStyle, + max_width: usize, + width: usize, +} + impl<'cfg> 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 @@ -39,9 +45,11 @@ impl<'cfg> Progress<'cfg> { Progress { state: cfg.shell().err_width().map(|n| State { config: cfg, - style, - max_width: n, - width: cmp::min(n, 80), + format: Format { + style, + max_width: n, + width: cmp::min(n, 80), + }, first: true, last_update: Instant::now(), name: name.to_string(), @@ -56,18 +64,18 @@ impl<'cfg> Progress<'cfg> { pub fn tick(&mut self, cur: usize, max: usize) -> CargoResult<()> { match self.state { - Some(ref mut s) => s.tick(cur, max, String::new(), true), + Some(ref mut s) => s.tick(cur, max, "", true), None => Ok(()), } } pub fn clear(&mut self) { if let Some(ref mut s) = self.state { - clear(s.max_width, s.config); + clear(s.format.max_width, s.config); } } - pub fn tick_now(&mut self, cur: usize, max: usize, msg: String) -> CargoResult<()> { + 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, false), None => Ok(()), @@ -76,7 +84,7 @@ impl<'cfg> Progress<'cfg> { } impl<'cfg> State<'cfg> { - fn tick(&mut self, cur: usize, max: usize, msg: String, throttle: bool) -> CargoResult<()> { + fn tick(&mut self, cur: usize, max: usize, msg: &str, throttle: bool) -> CargoResult<()> { if self.done { return Ok(()); } @@ -109,6 +117,22 @@ impl<'cfg> State<'cfg> { self.last_update = Instant::now(); } + if 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. + if let Some(string) = self.format.progress_status(cur, max, msg) { + self.config.shell().status_header(&self.name)?; + write!(self.config.shell().err(), "{}\r", string)?; + } + Ok(()) + } +} + +impl Format { + fn progress_status(&self, cur: usize, max: usize, msg: &str) -> Option { // Render the percentage at the far right and then figure how long the // progress bar is let pct = (cur as f64) / (max as f64); @@ -120,9 +144,11 @@ impl<'cfg> State<'cfg> { let extra_len = stats.len() + 2 /* [ and ] */ + 15 /* status header */; 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; @@ -132,7 +158,6 @@ impl<'cfg> State<'cfg> { string.push_str("="); } if cur == max { - self.done = true; string.push_str("="); } else { string.push_str(">"); @@ -146,19 +171,26 @@ impl<'cfg> State<'cfg> { string.push_str("]"); string.push_str(&stats); - let avail_msg_len = self.max_width - self.width; - if avail_msg_len >= msg.len() + 3 { - string.push_str(&msg); - } else if avail_msg_len >= 4 { - string.push_str(&msg[..(avail_msg_len - 3)]); - string.push_str("..."); + let mut avail_msg_len = self.max_width - self.width; + let mut ellipsis_pos = 0; + if avail_msg_len > 3 { + 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; + } + } } - // 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) } } @@ -169,6 +201,122 @@ fn clear(width: usize, config: &Config) { impl<'cfg> Drop for State<'cfg> { fn drop(&mut self) { - clear(self.max_width, self.config); + clear(self.format.max_width, self.config); } } + +#[test] +fn test_progress_status() { + let format = Format { + style: ProgressStyle::Ratio, + width: 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, + width: 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, + width: 25, + max_width: 25, + }; + assert_eq!( + format.progress_status(1, 1, ""), + Some("[] 100.00%".to_string()) + ); + + let format = Format { + style: ProgressStyle::Percentage, + width: 24, + max_width: 24, + }; + assert_eq!( + format.progress_status(1, 1, ""), + None + ); +}