diff options
author | Valentin Popov <valentin@popov.link> | 2024-01-08 00:21:28 +0300 |
---|---|---|
committer | Valentin Popov <valentin@popov.link> | 2024-01-08 00:21:28 +0300 |
commit | 1b6a04ca5504955c571d1c97504fb45ea0befee4 (patch) | |
tree | 7579f518b23313e8a9748a88ab6173d5e030b227 /vendor/indicatif/src/state.rs | |
parent | 5ecd8cf2cba827454317368b68571df0d13d7842 (diff) | |
download | fparkan-1b6a04ca5504955c571d1c97504fb45ea0befee4.tar.xz fparkan-1b6a04ca5504955c571d1c97504fb45ea0befee4.zip |
Initial vendor packages
Signed-off-by: Valentin Popov <valentin@popov.link>
Diffstat (limited to 'vendor/indicatif/src/state.rs')
-rw-r--r-- | vendor/indicatif/src/state.rs | 798 |
1 files changed, 798 insertions, 0 deletions
diff --git a/vendor/indicatif/src/state.rs b/vendor/indicatif/src/state.rs new file mode 100644 index 0000000..3bcdc44 --- /dev/null +++ b/vendor/indicatif/src/state.rs @@ -0,0 +1,798 @@ +use std::borrow::Cow; +use std::io; +use std::sync::Arc; +use std::time::Duration; +#[cfg(not(target_arch = "wasm32"))] +use std::time::Instant; + +#[cfg(target_arch = "wasm32")] +use instant::Instant; +use portable_atomic::{AtomicU64, AtomicU8, Ordering}; + +use crate::draw_target::ProgressDrawTarget; +use crate::style::ProgressStyle; + +pub(crate) struct BarState { + pub(crate) draw_target: ProgressDrawTarget, + pub(crate) on_finish: ProgressFinish, + pub(crate) style: ProgressStyle, + pub(crate) state: ProgressState, + pub(crate) tab_width: usize, +} + +impl BarState { + pub(crate) fn new( + len: Option<u64>, + draw_target: ProgressDrawTarget, + pos: Arc<AtomicPosition>, + ) -> Self { + Self { + draw_target, + on_finish: ProgressFinish::default(), + style: ProgressStyle::default_bar(), + state: ProgressState::new(len, pos), + tab_width: DEFAULT_TAB_WIDTH, + } + } + + /// Finishes the progress bar using the [`ProgressFinish`] behavior stored + /// in the [`ProgressStyle`]. + pub(crate) fn finish_using_style(&mut self, now: Instant, finish: ProgressFinish) { + self.state.status = Status::DoneVisible; + match finish { + ProgressFinish::AndLeave => { + if let Some(len) = self.state.len { + self.state.pos.set(len); + } + } + ProgressFinish::WithMessage(msg) => { + if let Some(len) = self.state.len { + self.state.pos.set(len); + } + self.state.message = TabExpandedString::new(msg, self.tab_width); + } + ProgressFinish::AndClear => { + if let Some(len) = self.state.len { + self.state.pos.set(len); + } + self.state.status = Status::DoneHidden; + } + ProgressFinish::Abandon => {} + ProgressFinish::AbandonWithMessage(msg) => { + self.state.message = TabExpandedString::new(msg, self.tab_width); + } + } + + // There's no need to update the estimate here; once the `status` is no longer + // `InProgress`, we will use the length and elapsed time to estimate. + let _ = self.draw(true, now); + } + + pub(crate) fn reset(&mut self, now: Instant, mode: Reset) { + // Always reset the estimator; this is the only reset that will occur if mode is + // `Reset::Eta`. + self.state.est.reset(now); + + if let Reset::Elapsed | Reset::All = mode { + self.state.started = now; + } + + if let Reset::All = mode { + self.state.pos.reset(now); + self.state.status = Status::InProgress; + + for tracker in self.style.format_map.values_mut() { + tracker.reset(&self.state, now); + } + + let _ = self.draw(false, now); + } + } + + pub(crate) fn update(&mut self, now: Instant, f: impl FnOnce(&mut ProgressState), tick: bool) { + f(&mut self.state); + if tick { + self.tick(now); + } + } + + pub(crate) fn set_length(&mut self, now: Instant, len: u64) { + self.state.len = Some(len); + self.update_estimate_and_draw(now); + } + + pub(crate) fn inc_length(&mut self, now: Instant, delta: u64) { + if let Some(len) = self.state.len { + self.state.len = Some(len.saturating_add(delta)); + } + self.update_estimate_and_draw(now); + } + + pub(crate) fn set_tab_width(&mut self, tab_width: usize) { + self.tab_width = tab_width; + self.state.message.set_tab_width(tab_width); + self.state.prefix.set_tab_width(tab_width); + self.style.set_tab_width(tab_width); + } + + pub(crate) fn set_style(&mut self, style: ProgressStyle) { + self.style = style; + self.style.set_tab_width(self.tab_width); + } + + pub(crate) fn tick(&mut self, now: Instant) { + self.state.tick = self.state.tick.saturating_add(1); + self.update_estimate_and_draw(now); + } + + pub(crate) fn update_estimate_and_draw(&mut self, now: Instant) { + let pos = self.state.pos.pos.load(Ordering::Relaxed); + self.state.est.record(pos, now); + + for tracker in self.style.format_map.values_mut() { + tracker.tick(&self.state, now); + } + + let _ = self.draw(false, now); + } + + pub(crate) fn println(&mut self, now: Instant, msg: &str) { + let width = self.draw_target.width(); + let mut drawable = match self.draw_target.drawable(true, now) { + Some(drawable) => drawable, + None => return, + }; + + let mut draw_state = drawable.state(); + let lines: Vec<String> = msg.lines().map(Into::into).collect(); + // Empty msg should trigger newline as we are in println + if lines.is_empty() { + draw_state.lines.push(String::new()); + } else { + draw_state.lines.extend(lines); + } + draw_state.orphan_lines_count = draw_state.lines.len(); + if !matches!(self.state.status, Status::DoneHidden) { + self.style + .format_state(&self.state, &mut draw_state.lines, width); + } + + drop(draw_state); + let _ = drawable.draw(); + } + + pub(crate) fn suspend<F: FnOnce() -> R, R>(&mut self, now: Instant, f: F) -> R { + if let Some((state, _)) = self.draw_target.remote() { + return state.write().unwrap().suspend(f, now); + } + + if let Some(drawable) = self.draw_target.drawable(true, now) { + let _ = drawable.clear(); + } + + let ret = f(); + let _ = self.draw(true, Instant::now()); + ret + } + + pub(crate) fn draw(&mut self, mut force_draw: bool, now: Instant) -> io::Result<()> { + let width = self.draw_target.width(); + + // `|= self.is_finished()` should not be needed here, but we used to always draw for + // finished progress bars, so it's kept as to not cause compatibility issues in weird cases. + force_draw |= self.state.is_finished(); + let mut drawable = match self.draw_target.drawable(force_draw, now) { + Some(drawable) => drawable, + None => return Ok(()), + }; + + let mut draw_state = drawable.state(); + + if !matches!(self.state.status, Status::DoneHidden) { + self.style + .format_state(&self.state, &mut draw_state.lines, width); + } + + drop(draw_state); + drawable.draw() + } +} + +impl Drop for BarState { + fn drop(&mut self) { + // Progress bar is already finished. Do not need to do anything other than notify + // the `MultiProgress` that we're now a zombie. + if self.state.is_finished() { + self.draw_target.mark_zombie(); + return; + } + + self.finish_using_style(Instant::now(), self.on_finish.clone()); + + // Notify the `MultiProgress` that we're now a zombie. + self.draw_target.mark_zombie(); + } +} + +pub(crate) enum Reset { + Eta, + Elapsed, + All, +} + +/// The state of a progress bar at a moment in time. +#[non_exhaustive] +pub struct ProgressState { + pos: Arc<AtomicPosition>, + len: Option<u64>, + pub(crate) tick: u64, + pub(crate) started: Instant, + status: Status, + est: Estimator, + pub(crate) message: TabExpandedString, + pub(crate) prefix: TabExpandedString, +} + +impl ProgressState { + pub(crate) fn new(len: Option<u64>, pos: Arc<AtomicPosition>) -> Self { + let now = Instant::now(); + Self { + pos, + len, + tick: 0, + status: Status::InProgress, + started: now, + est: Estimator::new(now), + message: TabExpandedString::NoTabs("".into()), + prefix: TabExpandedString::NoTabs("".into()), + } + } + + /// Indicates that the progress bar finished. + pub fn is_finished(&self) -> bool { + match self.status { + Status::InProgress => false, + Status::DoneVisible => true, + Status::DoneHidden => true, + } + } + + /// Returns the completion as a floating-point number between 0 and 1 + pub fn fraction(&self) -> f32 { + let pos = self.pos.pos.load(Ordering::Relaxed); + let pct = match (pos, self.len) { + (_, None) => 0.0, + (_, Some(0)) => 1.0, + (0, _) => 0.0, + (pos, Some(len)) => pos as f32 / len as f32, + }; + pct.clamp(0.0, 1.0) + } + + /// The expected ETA + pub fn eta(&self) -> Duration { + if self.is_finished() { + return Duration::new(0, 0); + } + + let len = match self.len { + Some(len) => len, + None => return Duration::new(0, 0), + }; + + let pos = self.pos.pos.load(Ordering::Relaxed); + + let sps = self.est.steps_per_second(Instant::now()); + + // Infinite duration should only ever happen at the beginning, so in this case it's okay to + // just show an ETA of 0 until progress starts to occur. + if sps == 0.0 { + return Duration::new(0, 0); + } + + secs_to_duration(len.saturating_sub(pos) as f64 / sps) + } + + /// The expected total duration (that is, elapsed time + expected ETA) + pub fn duration(&self) -> Duration { + if self.len.is_none() || self.is_finished() { + return Duration::new(0, 0); + } + self.started.elapsed().saturating_add(self.eta()) + } + + /// The number of steps per second + pub fn per_sec(&self) -> f64 { + if let Status::InProgress = self.status { + self.est.steps_per_second(Instant::now()) + } else { + let len = self.len.unwrap_or_else(|| self.pos()); + len as f64 / self.started.elapsed().as_secs_f64() + } + } + + pub fn elapsed(&self) -> Duration { + self.started.elapsed() + } + + pub fn pos(&self) -> u64 { + self.pos.pos.load(Ordering::Relaxed) + } + + pub fn set_pos(&mut self, pos: u64) { + self.pos.set(pos); + } + + #[allow(clippy::len_without_is_empty)] + pub fn len(&self) -> Option<u64> { + self.len + } + + pub fn set_len(&mut self, len: u64) { + self.len = Some(len); + } +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub(crate) enum TabExpandedString { + NoTabs(Cow<'static, str>), + WithTabs { + original: Cow<'static, str>, + expanded: String, + tab_width: usize, + }, +} + +impl TabExpandedString { + pub(crate) fn new(s: Cow<'static, str>, tab_width: usize) -> Self { + let expanded = s.replace('\t', &" ".repeat(tab_width)); + if s == expanded { + Self::NoTabs(s) + } else { + Self::WithTabs { + original: s, + expanded, + tab_width, + } + } + } + + pub(crate) fn expanded(&self) -> &str { + match &self { + Self::NoTabs(s) => { + debug_assert!(!s.contains('\t')); + s + } + Self::WithTabs { expanded, .. } => expanded, + } + } + + pub(crate) fn set_tab_width(&mut self, new_tab_width: usize) { + if let Self::WithTabs { + original, + expanded, + tab_width, + } = self + { + if *tab_width != new_tab_width { + *tab_width = new_tab_width; + *expanded = original.replace('\t', &" ".repeat(new_tab_width)); + } + } + } +} + +/// Double-smoothed exponentially weighted estimator +/// +/// This uses an exponentially weighted *time-based* estimator, meaning that it exponentially +/// downweights old data based on its age. The rate at which this occurs is currently a constant +/// value of 15 seconds for 90% weighting. This means that all data older than 15 seconds has a +/// collective weight of 0.1 in the estimate, and all data older than 30 seconds has a collective +/// weight of 0.01, and so on. +/// +/// The primary value exposed by `Estimator` is `steps_per_second`. This value is doubly-smoothed, +/// meaning that is the result of using an exponentially weighted estimator (as described above) to +/// estimate the value of another exponentially weighted estimator, which estimates the value of +/// the raw data. +/// +/// The purpose of this extra smoothing step is to reduce instantaneous fluctations in the estimate +/// when large updates are received. Without this, estimates might have a large spike followed by a +/// slow asymptotic approach to zero (until the next spike). +#[derive(Debug)] +pub(crate) struct Estimator { + smoothed_steps_per_sec: f64, + double_smoothed_steps_per_sec: f64, + prev_steps: u64, + prev_time: Instant, + start_time: Instant, +} + +impl Estimator { + fn new(now: Instant) -> Self { + Self { + smoothed_steps_per_sec: 0.0, + double_smoothed_steps_per_sec: 0.0, + prev_steps: 0, + prev_time: now, + start_time: now, + } + } + + fn record(&mut self, new_steps: u64, now: Instant) { + // sanity check: don't record data if time or steps have not advanced + if new_steps <= self.prev_steps || now <= self.prev_time { + // Reset on backwards seek to prevent breakage from seeking to the end for length determination + // See https://github.com/console-rs/indicatif/issues/480 + if new_steps < self.prev_steps { + self.prev_steps = new_steps; + self.reset(now); + } + return; + } + + let delta_steps = new_steps - self.prev_steps; + let delta_t = duration_to_secs(now - self.prev_time); + + // the rate of steps we saw in this update + let new_steps_per_second = delta_steps as f64 / delta_t; + + // update the estimate: a weighted average of the old estimate and new data + let weight = estimator_weight(delta_t); + self.smoothed_steps_per_sec = + self.smoothed_steps_per_sec * weight + new_steps_per_second * (1.0 - weight); + + // An iterative estimate like `smoothed_steps_per_sec` is supposed to be an exponentially + // weighted average from t=0 back to t=-inf; Since we initialize it to 0, we neglect the + // (non-existent) samples in the weighted average prior to the first one, so the resulting + // average must be normalized. We normalize the single estimate here in order to use it as + // a source for the double smoothed estimate. See comment on normalization in + // `steps_per_second` for details. + let delta_t_start = duration_to_secs(now - self.start_time); + let total_weight = 1.0 - estimator_weight(delta_t_start); + let normalized_smoothed_steps_per_sec = self.smoothed_steps_per_sec / total_weight; + + // determine the double smoothed value (EWA smoothing of the single EWA) + self.double_smoothed_steps_per_sec = self.double_smoothed_steps_per_sec * weight + + normalized_smoothed_steps_per_sec * (1.0 - weight); + + self.prev_steps = new_steps; + self.prev_time = now; + } + + /// Reset the state of the estimator. Once reset, estimates will not depend on any data prior + /// to `now`. This does not reset the stored position of the progress bar. + pub(crate) fn reset(&mut self, now: Instant) { + self.smoothed_steps_per_sec = 0.0; + self.double_smoothed_steps_per_sec = 0.0; + + // only reset prev_time, not prev_steps + self.prev_time = now; + self.start_time = now; + } + + /// Average time per step in seconds, using double exponential smoothing + fn steps_per_second(&self, now: Instant) -> f64 { + // Because the value stored in the Estimator is only updated when the Estimator receives an + // update, this value will become stuck if progress stalls. To return an accurate estimate, + // we determine how much time has passed since the last update, and treat this as a + // pseudo-update with 0 steps. + let delta_t = duration_to_secs(now - self.prev_time); + let reweight = estimator_weight(delta_t); + + // Normalization of estimates: + // + // The raw estimate is a single value (smoothed_steps_per_second) that is iteratively + // updated. At each update, the previous value of the estimate is downweighted according to + // its age, receiving the iterative weight W(t) = 0.1 ^ (t/15). + // + // Since W(Sum(t_n)) = Prod(W(t_n)), the total weight of a sample after a series of + // iterative steps is simply W(t_e) - W(t_b), where t_e is the time since the end of the + // sample, and t_b is the time since the beginning. The resulting estimate is therefore a + // weighted average with sample weights W(t_e) - W(t_b). + // + // Notice that the weighting function generates sample weights that sum to 1 only when the + // sample times span from t=0 to t=inf; but this is not the case. We have a first sample + // with finite, positive t_b = t_f. In the raw estimate, we handle times prior to t_f by + // setting an initial value of 0, meaning that these (non-existent) samples have no weight. + // + // Therefore, the raw estimate must be normalized by dividing it by the sum of the weights + // in the weighted average. This sum is just W(0) - W(t_f), where t_f is the time since the + // first sample, and W(0) = 1. + let delta_t_start = duration_to_secs(now - self.start_time); + let total_weight = 1.0 - estimator_weight(delta_t_start); + + // Generate updated values for `smoothed_steps_per_sec` and `double_smoothed_steps_per_sec` + // (sps and dsps) without storing them. Note that we normalize sps when using it as a + // source to update dsps, and then normalize dsps itself before returning it. + let sps = self.smoothed_steps_per_sec * reweight / total_weight; + let dsps = self.double_smoothed_steps_per_sec * reweight + sps * (1.0 - reweight); + dsps / total_weight + } +} + +pub(crate) struct AtomicPosition { + pub(crate) pos: AtomicU64, + capacity: AtomicU8, + prev: AtomicU64, + start: Instant, +} + +impl AtomicPosition { + pub(crate) fn new() -> Self { + Self { + pos: AtomicU64::new(0), + capacity: AtomicU8::new(MAX_BURST), + prev: AtomicU64::new(0), + start: Instant::now(), + } + } + + pub(crate) fn allow(&self, now: Instant) -> bool { + if now < self.start { + return false; + } + + let mut capacity = self.capacity.load(Ordering::Acquire); + // `prev` is the number of ms after `self.started` we last returned `true`, in ns + let prev = self.prev.load(Ordering::Acquire); + // `elapsed` is the number of ns since `self.started` + let elapsed = (now - self.start).as_nanos() as u64; + // `diff` is the number of ns since we last returned `true` + let diff = elapsed.saturating_sub(prev); + + // If `capacity` is 0 and not enough time (1ms) has passed since `prev` + // to add new capacity, return `false`. The goal of this method is to + // make this decision as efficient as possible. + if capacity == 0 && diff < INTERVAL { + return false; + } + + // We now calculate `new`, the number of ms, in ns, since we last returned `true`, + // and `remainder`, which represents a number of ns less than 1ms which we cannot + // convert into capacity now, so we're saving it for later. We do this by + // substracting this from `elapsed` before storing it into `self.prev`. + let (new, remainder) = ((diff / INTERVAL), (diff % INTERVAL)); + // We add `new` to `capacity`, subtract one for returning `true` from here, + // then make sure it does not exceed a maximum of `MAX_BURST`. + capacity = Ord::min(MAX_BURST as u128, (capacity as u128) + (new as u128) - 1) as u8; + + // Then, we just store `capacity` and `prev` atomically for the next iteration + self.capacity.store(capacity, Ordering::Release); + self.prev.store(elapsed - remainder, Ordering::Release); + true + } + + fn reset(&self, now: Instant) { + self.set(0); + let elapsed = (now.saturating_duration_since(self.start)).as_millis() as u64; + self.prev.store(elapsed, Ordering::Release); + } + + pub(crate) fn inc(&self, delta: u64) { + self.pos.fetch_add(delta, Ordering::SeqCst); + } + + pub(crate) fn set(&self, pos: u64) { + self.pos.store(pos, Ordering::Release); + } +} + +const INTERVAL: u64 = 1_000_000; +const MAX_BURST: u8 = 10; + +/// Behavior of a progress bar when it is finished +/// +/// This is invoked when a [`ProgressBar`] or [`ProgressBarIter`] completes and +/// [`ProgressBar::is_finished`] is false. +/// +/// [`ProgressBar`]: crate::ProgressBar +/// [`ProgressBarIter`]: crate::ProgressBarIter +/// [`ProgressBar::is_finished`]: crate::ProgressBar::is_finished +#[derive(Clone, Debug)] +pub enum ProgressFinish { + /// Finishes the progress bar and leaves the current message + /// + /// Same behavior as calling [`ProgressBar::finish()`](crate::ProgressBar::finish). + AndLeave, + /// Finishes the progress bar and sets a message + /// + /// Same behavior as calling [`ProgressBar::finish_with_message()`](crate::ProgressBar::finish_with_message). + WithMessage(Cow<'static, str>), + /// Finishes the progress bar and completely clears it (this is the default) + /// + /// Same behavior as calling [`ProgressBar::finish_and_clear()`](crate::ProgressBar::finish_and_clear). + AndClear, + /// Finishes the progress bar and leaves the current message and progress + /// + /// Same behavior as calling [`ProgressBar::abandon()`](crate::ProgressBar::abandon). + Abandon, + /// Finishes the progress bar and sets a message, and leaves the current progress + /// + /// Same behavior as calling [`ProgressBar::abandon_with_message()`](crate::ProgressBar::abandon_with_message). + AbandonWithMessage(Cow<'static, str>), +} + +impl Default for ProgressFinish { + fn default() -> Self { + Self::AndClear + } +} + +/// Get the appropriate dilution weight for Estimator data given the data's age (in seconds) +/// +/// Whenever an update occurs, we will create a new estimate using a weight `w_i` like so: +/// +/// ```math +/// <new estimate> = <previous estimate> * w_i + <new data> * (1 - w_i) +/// ``` +/// +/// In other words, the new estimate is a weighted average of the previous estimate and the new +/// data. We want to choose weights such that for any set of samples where `t_0, t_1, ...` are +/// the durations of the samples: +/// +/// ```math +/// Sum(t_i) = ews ==> Prod(w_i) = 0.1 +/// ``` +/// +/// With this constraint it is easy to show that +/// +/// ```math +/// w_i = 0.1 ^ (t_i / ews) +/// ``` +/// +/// Notice that the constraint implies that estimates are independent of the durations of the +/// samples, a very useful feature. +fn estimator_weight(age: f64) -> f64 { + const EXPONENTIAL_WEIGHTING_SECONDS: f64 = 15.0; + 0.1_f64.powf(age / EXPONENTIAL_WEIGHTING_SECONDS) +} + +fn duration_to_secs(d: Duration) -> f64 { + d.as_secs() as f64 + f64::from(d.subsec_nanos()) / 1_000_000_000f64 +} + +fn secs_to_duration(s: f64) -> Duration { + let secs = s.trunc() as u64; + let nanos = (s.fract() * 1_000_000_000f64) as u32; + Duration::new(secs, nanos) +} + +#[derive(Debug)] +pub(crate) enum Status { + InProgress, + DoneVisible, + DoneHidden, +} + +pub(crate) const DEFAULT_TAB_WIDTH: usize = 8; + +#[cfg(test)] +mod tests { + use super::*; + use crate::ProgressBar; + + // https://github.com/rust-lang/rust-clippy/issues/10281 + #[allow(clippy::uninlined_format_args)] + #[test] + fn test_steps_per_second() { + let test_rate = |items_per_second| { + let mut now = Instant::now(); + let mut est = Estimator::new(now); + let mut pos = 0; + + for _ in 0..20 { + pos += items_per_second; + now += Duration::from_secs(1); + est.record(pos, now); + } + let avg_steps_per_second = est.steps_per_second(now); + + assert!(avg_steps_per_second > 0.0); + assert!(avg_steps_per_second.is_finite()); + + let absolute_error = (avg_steps_per_second - items_per_second as f64).abs(); + let relative_error = absolute_error / items_per_second as f64; + assert!( + relative_error < 1.0 / 1e9, + "Expected rate: {}, actual: {}, relative error: {}", + items_per_second, + avg_steps_per_second, + relative_error + ); + }; + + test_rate(1); + test_rate(1_000); + test_rate(1_000_000); + test_rate(1_000_000_000); + test_rate(1_000_000_001); + test_rate(100_000_000_000); + test_rate(1_000_000_000_000); + test_rate(100_000_000_000_000); + test_rate(1_000_000_000_000_000); + } + + #[test] + fn test_double_exponential_ave() { + let mut now = Instant::now(); + let mut est = Estimator::new(now); + let mut pos = 0; + + // note: this is the default weight set in the Estimator + let weight = 15; + + for _ in 0..weight { + pos += 1; + now += Duration::from_secs(1); + est.record(pos, now); + } + now += Duration::from_secs(weight); + + // The first level EWA: + // -> 90% weight @ 0 eps, 9% weight @ 1 eps, 1% weight @ 0 eps + // -> then normalized by deweighting the 1% weight (before -30 seconds) + let single_target = 0.09 / 0.99; + + // The second level EWA: + // -> same logic as above, but using the first level EWA as the source + let double_target = (0.9 * single_target + 0.09) / 0.99; + assert_eq!(est.steps_per_second(now), double_target); + } + + #[test] + fn test_estimator_rewind_position() { + let mut now = Instant::now(); + let mut est = Estimator::new(now); + + now += Duration::from_secs(1); + est.record(1, now); + + // should not panic + now += Duration::from_secs(1); + est.record(0, now); + + // check that reset occurred (estimator at 1 event per sec) + now += Duration::from_secs(1); + est.record(1, now); + assert_eq!(est.steps_per_second(now), 1.0); + + // check that progress bar handles manual seeking + let pb = ProgressBar::hidden(); + pb.set_length(10); + pb.set_position(1); + pb.tick(); + // Should not panic. + pb.set_position(0); + } + + #[test] + fn test_reset_eta() { + let mut now = Instant::now(); + let mut est = Estimator::new(now); + + // two per second, then reset + now += Duration::from_secs(1); + est.record(2, now); + est.reset(now); + + // now one per second, and verify + now += Duration::from_secs(1); + est.record(3, now); + assert_eq!(est.steps_per_second(now), 1.0); + } + + #[test] + fn test_duration_stuff() { + let duration = Duration::new(42, 100_000_000); + let secs = duration_to_secs(duration); + assert_eq!(secs_to_duration(secs), duration); + } + + #[test] + fn test_atomic_position_large_time_difference() { + let atomic_position = AtomicPosition::new(); + let later = atomic_position.start + Duration::from_nanos(INTERVAL * u64::from(u8::MAX)); + // Should not panic. + atomic_position.allow(later); + } +} |