use std::collections::{HashMap, VecDeque};
use std::io::{self, Read};
use std::time::SystemTime;
use crate::console::{Console, ConsoleOptions, Renderable};
use crate::filesize;
use crate::live::Live;
use crate::progress_bar::ProgressBar;
use crate::segment::Segment;
use crate::spinner::Spinner;
use crate::style::Style;
use crate::table::Table;
use crate::text::{JustifyMethod, Text};
pub type TaskId = usize;
#[derive(Debug, Clone)]
pub struct ProgressSample {
pub timestamp: f64,
pub completed: f64,
}
#[derive(Debug, Clone)]
pub struct Task {
pub id: TaskId,
pub description: String,
pub total: Option<f64>,
pub completed: f64,
pub visible: bool,
pub fields: HashMap<String, String>,
pub start_time: Option<f64>,
pub stop_time: Option<f64>,
pub finished_time: Option<f64>,
pub finished_speed: Option<f64>,
samples: VecDeque<ProgressSample>,
progress: Vec<ProgressSample>,
}
impl Task {
pub fn new(id: TaskId, description: &str, total: Option<f64>) -> Self {
Task {
id,
description: description.to_string(),
total,
completed: 0.0,
visible: true,
fields: HashMap::new(),
start_time: None,
stop_time: None,
finished_time: None,
finished_speed: None,
samples: VecDeque::new(),
progress: Vec::new(),
}
}
pub fn started(&self) -> bool {
self.start_time.is_some()
}
pub fn finished(&self) -> bool {
self.finished_time.is_some()
}
pub fn remaining(&self) -> Option<f64> {
self.total.map(|t| (t - self.completed).max(0.0))
}
pub fn elapsed(&self) -> Option<f64> {
self.start_time.map(|start| {
let end = self.stop_time.unwrap_or_else(current_time_secs);
(end - start).max(0.0)
})
}
pub fn percentage(&self) -> f64 {
match self.total {
Some(total) if total > 0.0 => ((self.completed / total) * 100.0).clamp(0.0, 100.0),
_ => 0.0,
}
}
pub fn speed(&self) -> Option<f64> {
if self.finished() {
return self.finished_speed;
}
if self.samples.len() < 2 {
return None;
}
let first = self.samples.front().unwrap();
let last = self.samples.back().unwrap();
let time_delta = last.timestamp - first.timestamp;
if time_delta <= 0.0 {
return None;
}
let completed_delta = last.completed - first.completed;
Some(completed_delta / time_delta)
}
pub fn time_remaining(&self) -> Option<f64> {
if self.finished() {
return Some(0.0);
}
let remaining = self.remaining()?;
let speed = self.speed()?;
if speed <= 0.0 {
return None;
}
Some(remaining / speed)
}
fn record_sample(&mut self, timestamp: f64, speed_estimate_period: f64) {
self.samples.push_back(ProgressSample {
timestamp,
completed: self.completed,
});
self.progress.push(ProgressSample {
timestamp,
completed: self.completed,
});
let cutoff = timestamp - speed_estimate_period;
while let Some(front) = self.samples.front() {
if front.timestamp < cutoff {
self.samples.pop_front();
} else {
break;
}
}
}
}
fn current_time_secs() -> f64 {
SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.map(|d| d.as_secs_f64())
.unwrap_or(0.0)
}
fn format_time(seconds: f64) -> String {
let total = seconds.round() as u64;
let h = total / 3600;
let m = (total % 3600) / 60;
let s = total % 60;
if h > 0 {
format!("{h}:{m:02}:{s:02}")
} else {
format!("{m}:{s:02}")
}
}
pub trait ProgressColumn: Send + Sync {
fn render(&self, task: &Task) -> Text;
fn max_refresh(&self) -> Option<f64> {
None
}
}
#[derive(Debug, Clone)]
pub struct TextColumn {
pub text: String,
pub style: Option<Style>,
pub justify: JustifyMethod,
}
impl TextColumn {
pub fn new(text: &str) -> Self {
TextColumn {
text: text.to_string(),
style: None,
justify: JustifyMethod::Left,
}
}
#[must_use]
pub fn with_style(mut self, style: Style) -> Self {
self.style = Some(style);
self
}
#[must_use]
pub fn with_justify(mut self, justify: JustifyMethod) -> Self {
self.justify = justify;
self
}
fn substitute(&self, task: &Task) -> String {
let mut result = self.text.clone();
result = result.replace("{task.description}", &task.description);
if result.contains("{task.percentage") {
let pct = task.percentage();
if let Some(start) = result.find("{task.percentage:") {
if let Some(end) = result[start..].find('}') {
let spec = &result[start..start + end + 1];
let formatted = if spec.contains(".0f") {
format!("{pct:.0}")
} else if spec.contains(".1f") {
format!("{pct:.1}")
} else if spec.contains(".2f") {
format!("{pct:.2}")
} else {
format!("{pct:.1}")
};
result = result.replace(spec, &formatted);
}
}
result = result.replace("{task.percentage}", &format!("{pct:.1}"));
}
result = result.replace("{task.completed}", &format!("{}", task.completed));
let total_str = match task.total {
Some(t) => format!("{t}"),
None => "?".to_string(),
};
result = result.replace("{task.total}", &total_str);
let speed_str = match task.speed() {
Some(s) => format!("{s:.1}"),
None => "?".to_string(),
};
result = result.replace("{task.speed}", &speed_str);
for (key, value) in &task.fields {
let placeholder = format!("{{task.fields.{key}}}");
result = result.replace(&placeholder, value);
}
result
}
}
impl ProgressColumn for TextColumn {
fn render(&self, task: &Task) -> Text {
let content = self.substitute(task);
let style = self.style.clone().unwrap_or_else(Style::null);
let mut text = Text::new(&content, style);
text.justify = Some(self.justify);
text
}
}
#[derive(Debug, Clone)]
pub struct BarColumn {
pub bar_width: Option<usize>,
pub style: String,
pub complete_style: String,
pub finished_style: String,
pub pulse_style: String,
}
impl BarColumn {
pub fn new() -> Self {
BarColumn {
bar_width: Some(40),
style: "bar.back".to_string(),
complete_style: "bar.complete".to_string(),
finished_style: "bar.finished".to_string(),
pulse_style: "bar.pulse".to_string(),
}
}
#[must_use]
pub fn with_bar_width(mut self, width: Option<usize>) -> Self {
self.bar_width = width;
self
}
}
impl Default for BarColumn {
fn default() -> Self {
Self::new()
}
}
impl ProgressColumn for BarColumn {
fn render(&self, task: &Task) -> Text {
let bar = ProgressBar::new()
.with_total(task.total)
.with_completed(task.completed)
.with_width(self.bar_width)
.with_style(&self.style)
.with_complete_style(&self.complete_style)
.with_finished_style(&self.finished_style)
.with_pulse_style(&self.pulse_style);
let console = Console::builder()
.width(self.bar_width.unwrap_or(40))
.color_system("truecolor")
.build();
let opts = console.options();
let segments = bar.rich_console(&console, &opts);
let mut text = Text::empty();
for seg in &segments {
text.append_str(&seg.text, seg.style.clone());
}
text.end = String::new();
text
}
}
#[derive(Debug, Clone)]
pub struct SpinnerColumn {
pub spinner_name: String,
pub style: Option<Style>,
pub finished_text: Text,
}
impl SpinnerColumn {
pub fn new(name: &str) -> Self {
SpinnerColumn {
spinner_name: name.to_string(),
style: None,
finished_text: Text::styled(
"\u{2714}",
Style::parse("green").unwrap_or_else(|_| Style::null()),
),
}
}
#[must_use]
pub fn with_style(mut self, style: Style) -> Self {
self.style = Some(style);
self
}
#[must_use]
pub fn with_finished_text(mut self, text: Text) -> Self {
self.finished_text = text;
self
}
}
impl Default for SpinnerColumn {
fn default() -> Self {
Self::new("dots")
}
}
impl ProgressColumn for SpinnerColumn {
fn render(&self, task: &Task) -> Text {
if task.finished() {
return self.finished_text.clone();
}
let mut spinner = match Spinner::new(&self.spinner_name) {
Ok(s) => s,
Err(_) => return Text::new("?", Style::null()),
};
if let Some(ref style) = self.style {
spinner = spinner.with_style(style.clone());
}
let elapsed = task.elapsed().unwrap_or(0.0);
spinner.render(elapsed)
}
fn max_refresh(&self) -> Option<f64> {
Some(0.08)
}
}
#[derive(Debug, Clone)]
pub struct TimeElapsedColumn;
impl Default for TimeElapsedColumn {
fn default() -> Self {
Self
}
}
impl ProgressColumn for TimeElapsedColumn {
fn render(&self, task: &Task) -> Text {
let elapsed = task.elapsed().unwrap_or(0.0);
let formatted = format_time(elapsed);
Text::new(
&formatted,
Style::parse("progress.elapsed").unwrap_or_else(|_| Style::null()),
)
}
}
#[derive(Debug, Clone)]
pub struct TimeRemainingColumn {
pub compact: bool,
pub elapsed_when_finished: bool,
}
impl TimeRemainingColumn {
pub fn new() -> Self {
TimeRemainingColumn {
compact: false,
elapsed_when_finished: false,
}
}
}
impl Default for TimeRemainingColumn {
fn default() -> Self {
Self::new()
}
}
impl ProgressColumn for TimeRemainingColumn {
fn render(&self, task: &Task) -> Text {
let style = Style::parse("progress.remaining").unwrap_or_else(|_| Style::null());
if task.finished() {
if self.elapsed_when_finished {
let elapsed = task.elapsed().unwrap_or(0.0);
return Text::new(&format_time(elapsed), style);
}
return Text::new("0:00", style);
}
match task.time_remaining() {
Some(remaining) if remaining.is_finite() => Text::new(&format_time(remaining), style),
_ => Text::new("-:--:--", style),
}
}
}
#[derive(Debug, Clone)]
pub struct TaskProgressColumn {
pub separator: String,
}
impl TaskProgressColumn {
pub fn new() -> Self {
TaskProgressColumn {
separator: "/".to_string(),
}
}
#[must_use]
pub fn with_separator(mut self, sep: &str) -> Self {
self.separator = sep.to_string();
self
}
}
impl Default for TaskProgressColumn {
fn default() -> Self {
Self::new()
}
}
impl ProgressColumn for TaskProgressColumn {
fn render(&self, task: &Task) -> Text {
let style = Style::parse("progress.percentage").unwrap_or_else(|_| Style::null());
let completed = task.completed;
let total_str = match task.total {
Some(t) => format!("{t}"),
None => "?".to_string(),
};
Text::new(&format!("{completed}{}{total_str}", self.separator), style)
}
}
#[derive(Debug, Clone)]
pub struct FileSizeColumn;
impl Default for FileSizeColumn {
fn default() -> Self {
Self
}
}
impl ProgressColumn for FileSizeColumn {
fn render(&self, task: &Task) -> Text {
let size = task.completed as u64;
let formatted = filesize::decimal(size, 1, " ");
Text::new(
&formatted,
Style::parse("progress.filesize").unwrap_or_else(|_| Style::null()),
)
}
}
#[derive(Debug, Clone)]
pub struct TotalFileSizeColumn;
impl Default for TotalFileSizeColumn {
fn default() -> Self {
Self
}
}
impl ProgressColumn for TotalFileSizeColumn {
fn render(&self, task: &Task) -> Text {
let size = task.total.unwrap_or(0.0) as u64;
let formatted = filesize::decimal(size, 1, " ");
Text::new(
&formatted,
Style::parse("progress.filesize.total").unwrap_or_else(|_| Style::null()),
)
}
}
#[derive(Debug, Clone)]
pub struct MofNCompleteColumn {
pub separator: String,
}
impl MofNCompleteColumn {
pub fn new() -> Self {
MofNCompleteColumn {
separator: "/".to_string(),
}
}
#[must_use]
pub fn with_separator(mut self, sep: &str) -> Self {
self.separator = sep.to_string();
self
}
}
impl Default for MofNCompleteColumn {
fn default() -> Self {
Self::new()
}
}
impl ProgressColumn for MofNCompleteColumn {
fn render(&self, task: &Task) -> Text {
let completed = task.completed as u64;
let total_str = match task.total {
Some(t) => format!("{}", t as u64),
None => "?".to_string(),
};
let style = Style::parse("progress.percentage").unwrap_or_else(|_| Style::null());
Text::new(&format!("{completed}{}{total_str}", self.separator), style)
}
}
#[derive(Debug, Clone)]
pub struct DownloadColumn;
impl Default for DownloadColumn {
fn default() -> Self {
Self
}
}
impl ProgressColumn for DownloadColumn {
fn render(&self, task: &Task) -> Text {
let completed = filesize::decimal(task.completed as u64, 1, " ");
let total = match task.total {
Some(t) => filesize::decimal(t as u64, 1, " "),
None => "?".to_string(),
};
let style = Style::parse("progress.download").unwrap_or_else(|_| Style::null());
Text::new(&format!("{completed}/{total}"), style)
}
}
#[derive(Debug, Clone)]
pub struct TransferSpeedColumn;
impl Default for TransferSpeedColumn {
fn default() -> Self {
Self
}
}
impl ProgressColumn for TransferSpeedColumn {
fn render(&self, task: &Task) -> Text {
let style = Style::parse("progress.data.speed").unwrap_or_else(|_| Style::null());
match task.speed() {
Some(speed) => {
let formatted = filesize::decimal(speed as u64, 1, " ");
Text::new(&format!("{formatted}/s"), style)
}
None => Text::new("?", style),
}
}
}
pub struct RenderableColumn {
pub callback: Box<dyn Fn(&Task) -> Text + Send + Sync>,
}
impl RenderableColumn {
pub fn new<F>(callback: F) -> Self
where
F: Fn(&Task) -> Text + Send + Sync + 'static,
{
RenderableColumn {
callback: Box::new(callback),
}
}
}
impl ProgressColumn for RenderableColumn {
fn render(&self, task: &Task) -> Text {
(self.callback)(task)
}
}
pub struct Progress {
columns: Vec<Box<dyn ProgressColumn>>,
tasks: Vec<Task>,
live: Live,
task_id_counter: usize,
speed_estimate_period: f64,
get_time: Box<dyn Fn() -> f64 + Send>,
disable: bool,
expand: bool,
}
impl Progress {
pub fn new(columns: Vec<Box<dyn ProgressColumn>>) -> Self {
Progress {
columns,
tasks: Vec::new(),
live: Live::new(Text::empty())
.with_auto_refresh(true)
.with_refresh_per_second(10.0),
task_id_counter: 0,
speed_estimate_period: 30.0,
get_time: Box::new(current_time_secs),
disable: false,
expand: false,
}
}
pub fn default_columns() -> Vec<Box<dyn ProgressColumn>> {
vec![
Box::new(TextColumn::new("{task.description}")),
Box::new(BarColumn::default()),
Box::new(TaskProgressColumn::default()),
Box::new(TimeRemainingColumn::default()),
]
}
#[must_use]
pub fn with_console(mut self, console: Console) -> Self {
self.live = self.live.with_console(console);
self
}
#[must_use]
pub fn with_auto_refresh(mut self, auto_refresh: bool) -> Self {
self.live = self.live.with_auto_refresh(auto_refresh);
self
}
#[must_use]
pub fn with_transient(mut self, transient: bool) -> Self {
self.live = self.live.with_transient(transient);
self
}
#[must_use]
pub fn with_refresh_per_second(mut self, rate: f64) -> Self {
self.live = self.live.with_refresh_per_second(rate);
self
}
#[must_use]
pub fn with_speed_estimate_period(mut self, seconds: f64) -> Self {
self.speed_estimate_period = seconds;
self
}
#[must_use]
pub fn with_disable(mut self, disable: bool) -> Self {
self.disable = disable;
self
}
#[must_use]
pub fn with_expand(mut self, expand: bool) -> Self {
self.expand = expand;
self
}
#[must_use]
pub fn with_get_time<F>(mut self, f: F) -> Self
where
F: Fn() -> f64 + Send + 'static,
{
self.get_time = Box::new(f);
self
}
pub fn add_task(&mut self, description: &str, total: Option<f64>) -> TaskId {
let id = self.task_id_counter;
self.task_id_counter += 1;
let mut task = Task::new(id, description, total);
let now = (self.get_time)();
task.start_time = Some(now);
self.tasks.push(task);
id
}
pub fn update(
&mut self,
task_id: TaskId,
completed: Option<f64>,
total: Option<f64>,
advance: Option<f64>,
description: Option<&str>,
visible: Option<bool>,
) {
let now = (self.get_time)();
if let Some(task) = self.tasks.iter_mut().find(|t| t.id == task_id) {
if let Some(desc) = description {
task.description = desc.to_string();
}
if let Some(t) = total {
task.total = Some(t);
}
if let Some(c) = completed {
task.completed = c;
}
if let Some(a) = advance {
task.completed += a;
}
if let Some(v) = visible {
task.visible = v;
}
if task.started() && !task.finished() {
task.record_sample(now, self.speed_estimate_period);
}
if let Some(t) = task.total {
if task.completed >= t && task.finished_time.is_none() {
task.finished_speed = task.speed();
task.finished_time = Some(now);
}
}
}
}
pub fn advance(&mut self, task_id: TaskId, advance: f64) {
self.update(task_id, None, None, Some(advance), None, None);
}
pub fn start_task(&mut self, task_id: TaskId) {
let now = (self.get_time)();
if let Some(task) = self.tasks.iter_mut().find(|t| t.id == task_id) {
if task.start_time.is_none() {
task.start_time = Some(now);
}
}
}
pub fn stop_task(&mut self, task_id: TaskId) {
let now = (self.get_time)();
if let Some(task) = self.tasks.iter_mut().find(|t| t.id == task_id) {
task.stop_time = Some(now);
}
}
pub fn remove_task(&mut self, task_id: TaskId) {
self.tasks.retain(|t| t.id != task_id);
}
pub fn get_task(&self, task_id: TaskId) -> Option<&Task> {
self.tasks.iter().find(|t| t.id == task_id)
}
pub fn get_task_mut(&mut self, task_id: TaskId) -> Option<&mut Task> {
self.tasks.iter_mut().find(|t| t.id == task_id)
}
pub fn tasks(&self) -> &[Task] {
&self.tasks
}
pub fn finished_count(&self) -> usize {
self.tasks.iter().filter(|t| t.finished()).count()
}
pub fn visible_count(&self) -> usize {
self.tasks.iter().filter(|t| t.visible).count()
}
pub fn reset(&mut self, task_id: TaskId) {
let now = (self.get_time)();
if let Some(task) = self.tasks.iter_mut().find(|t| t.id == task_id) {
task.completed = 0.0;
task.start_time = Some(now);
task.stop_time = None;
task.finished_time = None;
task.finished_speed = None;
task.samples.clear();
}
}
pub fn all_tasks_finished(&self) -> bool {
self.tasks
.iter()
.filter(|t| t.visible)
.all(|t| t.finished())
}
pub fn print(&self, renderable: &dyn Renderable) {
self.live.console_mut().print(renderable);
}
pub fn log(&self, message: &str) {
self.live.console_mut().log(message);
}
pub fn track<I>(
&mut self,
iter: I,
description: &str,
total: Option<f64>,
) -> ProgressTracker<'_, I::IntoIter>
where
I: IntoIterator,
{
let task_id = self.add_task(description, total);
ProgressTracker {
inner: iter.into_iter(),
progress: self,
task_id,
}
}
pub fn start(&mut self) {
if self.disable {
return;
}
self.live.start();
}
pub fn stop(&mut self) {
if self.disable {
return;
}
self.live.stop();
}
pub fn refresh(&mut self) {
if self.disable {
return;
}
let table_text = self.render_tasks_text();
self.live.update_renderable(table_text, true);
}
pub fn make_tasks_table(&self) -> Table {
let headers: Vec<&str> = self.columns.iter().map(|_| "").collect();
let mut table = Table::grid(&headers);
table.padding = (0, 1, 0, 0);
if self.expand {
table.set_expand(true);
}
for col in &mut table.columns {
col.no_wrap = true;
}
for task in &self.tasks {
if !task.visible {
continue;
}
let cells: Vec<String> = self
.columns
.iter()
.map(|col| {
let text = col.render(task);
text.plain().to_string()
})
.collect();
let cell_refs: Vec<&str> = cells.iter().map(|s| s.as_str()).collect();
table.add_row(&cell_refs);
}
table
}
fn render_tasks_text(&self) -> Text {
let visible_tasks: Vec<&Task> = self.tasks.iter().filter(|t| t.visible).collect();
if visible_tasks.is_empty() {
return Text::empty();
}
let separator = Text::new(" ", Style::null());
let mut result = Text::empty();
for (i, task) in visible_tasks.iter().enumerate() {
if i > 0 {
result.append_str("\n", None);
}
for (j, col) in self.columns.iter().enumerate() {
if j > 0 {
result.append_text(&separator);
}
let rendered = col.render(task);
result.append_text(&rendered);
}
}
result
}
}
impl Renderable for Progress {
fn rich_console(&self, _console: &Console, _options: &ConsoleOptions) -> Vec<Segment> {
let text = self.render_tasks_text();
text.render()
}
}
pub struct ProgressTracker<'a, I> {
inner: I,
progress: &'a mut Progress,
task_id: TaskId,
}
impl<'a, I> ProgressTracker<'a, I> {
pub fn task_id(&self) -> TaskId {
self.task_id
}
}
impl<I> Iterator for ProgressTracker<'_, I>
where
I: Iterator,
{
type Item = I::Item;
fn next(&mut self) -> Option<Self::Item> {
let item = self.inner.next()?;
self.progress.advance(self.task_id, 1.0);
Some(item)
}
fn size_hint(&self) -> (usize, Option<usize>) {
self.inner.size_hint()
}
}
pub struct TrackIterator<I> {
inner: I,
progress: Progress,
task_id: TaskId,
started: bool,
}
impl<I> TrackIterator<I>
where
I: Iterator,
{
pub fn new(iter: I, description: &str, total: Option<f64>) -> Self {
let mut progress = Progress::new(Progress::default_columns()).with_auto_refresh(false);
let task_id = progress.add_task(description, total);
TrackIterator {
inner: iter,
progress,
task_id,
started: false,
}
}
}
impl<I> Iterator for TrackIterator<I>
where
I: Iterator,
{
type Item = I::Item;
fn next(&mut self) -> Option<Self::Item> {
if !self.started {
self.progress.start();
self.started = true;
}
match self.inner.next() {
Some(item) => {
self.progress.advance(self.task_id, 1.0);
self.progress.refresh();
Some(item)
}
None => {
self.progress.stop();
None
}
}
}
fn size_hint(&self) -> (usize, Option<usize>) {
self.inner.size_hint()
}
}
impl<I> Drop for TrackIterator<I> {
fn drop(&mut self) {
if self.started {
self.progress.stop();
}
}
}
pub fn track<I>(iter: I, description: &str, total: Option<f64>) -> TrackIterator<I::IntoIter>
where
I: IntoIterator,
{
TrackIterator::new(iter.into_iter(), description, total)
}
pub trait ProgressIteratorExt: Iterator + Sized {
fn progress(self, description: &str) -> ProgressIter<Self>;
fn progress_with_total(self, description: &str, total: f64) -> ProgressIter<Self>;
}
impl<I: Iterator> ProgressIteratorExt for I {
fn progress(self, description: &str) -> ProgressIter<Self> {
let total = self.size_hint().1.map(|n| n as f64);
ProgressIter::new(self, description, total)
}
fn progress_with_total(self, description: &str, total: f64) -> ProgressIter<Self> {
ProgressIter::new(self, description, Some(total))
}
}
pub struct ProgressIter<I> {
inner: I,
progress: Progress,
task_id: TaskId,
started: bool,
}
impl<I: Iterator> ProgressIter<I> {
fn new(iter: I, description: &str, total: Option<f64>) -> Self {
let mut progress = Progress::new(Progress::default_columns()).with_auto_refresh(true);
let task_id = progress.add_task(description, total);
ProgressIter {
inner: iter,
progress,
task_id,
started: false,
}
}
pub fn task_id(&self) -> TaskId {
self.task_id
}
}
impl<I: Iterator> Iterator for ProgressIter<I> {
type Item = I::Item;
fn next(&mut self) -> Option<Self::Item> {
if !self.started {
self.progress.start();
self.started = true;
}
match self.inner.next() {
Some(item) => {
self.progress.advance(self.task_id, 1.0);
self.progress.refresh();
Some(item)
}
None => {
self.progress.stop();
None
}
}
}
fn size_hint(&self) -> (usize, Option<usize>) {
self.inner.size_hint()
}
}
impl<I> Drop for ProgressIter<I> {
fn drop(&mut self) {
if self.started {
self.progress.stop();
}
}
}
pub struct ProgressReader<R> {
inner: R,
callback: Box<dyn FnMut(usize)>,
total_read: usize,
}
impl<R> ProgressReader<R> {
pub fn new(inner: R, callback: impl FnMut(usize) + 'static) -> Self {
ProgressReader {
inner,
callback: Box::new(callback),
total_read: 0,
}
}
pub fn total_read(&self) -> usize {
self.total_read
}
pub fn into_inner(self) -> R {
self.inner
}
}
impl<R: Read> Read for ProgressReader<R> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
let n = self.inner.read(buf)?;
self.total_read += n;
(self.callback)(n);
Ok(n)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn test_console() -> Console {
Console::builder()
.width(80)
.height(25)
.quiet(true)
.markup(false)
.no_color(true)
.force_terminal(true)
.build()
}
#[test]
fn test_progress_sample_creation() {
let sample = ProgressSample {
timestamp: 1.0,
completed: 50.0,
};
assert!((sample.timestamp - 1.0).abs() < f64::EPSILON);
assert!((sample.completed - 50.0).abs() < f64::EPSILON);
}
#[test]
fn test_progress_sample_clone() {
let sample = ProgressSample {
timestamp: 2.5,
completed: 75.0,
};
let cloned = sample.clone();
assert!((cloned.timestamp - 2.5).abs() < f64::EPSILON);
assert!((cloned.completed - 75.0).abs() < f64::EPSILON);
}
#[test]
fn test_task_creation() {
let task = Task::new(0, "Test task", Some(100.0));
assert_eq!(task.id, 0);
assert_eq!(task.description, "Test task");
assert_eq!(task.total, Some(100.0));
assert!((task.completed - 0.0).abs() < f64::EPSILON);
assert!(task.visible);
assert!(task.fields.is_empty());
assert!(task.start_time.is_none());
assert!(task.stop_time.is_none());
assert!(task.finished_time.is_none());
assert!(task.finished_speed.is_none());
}
#[test]
fn test_task_creation_no_total() {
let task = Task::new(1, "Indeterminate", None);
assert_eq!(task.total, None);
}
#[test]
fn test_task_started() {
let mut task = Task::new(0, "test", Some(100.0));
assert!(!task.started());
task.start_time = Some(1.0);
assert!(task.started());
}
#[test]
fn test_task_finished() {
let mut task = Task::new(0, "test", Some(100.0));
assert!(!task.finished());
task.finished_time = Some(2.0);
assert!(task.finished());
}
#[test]
fn test_task_remaining_with_total() {
let mut task = Task::new(0, "test", Some(100.0));
task.completed = 30.0;
assert_eq!(task.remaining(), Some(70.0));
}
#[test]
fn test_task_remaining_over_total() {
let mut task = Task::new(0, "test", Some(100.0));
task.completed = 120.0;
assert_eq!(task.remaining(), Some(0.0));
}
#[test]
fn test_task_remaining_no_total() {
let task = Task::new(0, "test", None);
assert_eq!(task.remaining(), None);
}
#[test]
fn test_task_elapsed_not_started() {
let task = Task::new(0, "test", Some(100.0));
assert_eq!(task.elapsed(), None);
}
#[test]
fn test_task_elapsed_with_stop() {
let mut task = Task::new(0, "test", Some(100.0));
task.start_time = Some(10.0);
task.stop_time = Some(15.0);
let elapsed = task.elapsed().unwrap();
assert!((elapsed - 5.0).abs() < f64::EPSILON);
}
#[test]
fn test_task_percentage_normal() {
let mut task = Task::new(0, "test", Some(100.0));
task.completed = 50.0;
assert!((task.percentage() - 50.0).abs() < f64::EPSILON);
}
#[test]
fn test_task_percentage_zero_completed() {
let task = Task::new(0, "test", Some(100.0));
assert!((task.percentage() - 0.0).abs() < f64::EPSILON);
}
#[test]
fn test_task_percentage_full() {
let mut task = Task::new(0, "test", Some(100.0));
task.completed = 100.0;
assert!((task.percentage() - 100.0).abs() < f64::EPSILON);
}
#[test]
fn test_task_percentage_over() {
let mut task = Task::new(0, "test", Some(100.0));
task.completed = 150.0;
assert!((task.percentage() - 100.0).abs() < f64::EPSILON);
}
#[test]
fn test_task_percentage_no_total() {
let task = Task::new(0, "test", None);
assert!((task.percentage() - 0.0).abs() < f64::EPSILON);
}
#[test]
fn test_task_percentage_zero_total() {
let task = Task::new(0, "test", Some(0.0));
assert!((task.percentage() - 0.0).abs() < f64::EPSILON);
}
#[test]
fn test_task_percentage_custom_total() {
let mut task = Task::new(0, "test", Some(200.0));
task.completed = 100.0;
assert!((task.percentage() - 50.0).abs() < f64::EPSILON);
}
#[test]
fn test_task_speed_no_samples() {
let task = Task::new(0, "test", Some(100.0));
assert!(task.speed().is_none());
}
#[test]
fn test_task_speed_one_sample() {
let mut task = Task::new(0, "test", Some(100.0));
task.start_time = Some(0.0);
task.completed = 10.0;
task.record_sample(1.0, 30.0);
assert!(task.speed().is_none()); }
#[test]
fn test_task_speed_two_samples() {
let mut task = Task::new(0, "test", Some(100.0));
task.start_time = Some(0.0);
task.completed = 10.0;
task.record_sample(1.0, 30.0);
task.completed = 30.0;
task.record_sample(3.0, 30.0);
let speed = task.speed().unwrap();
assert!((speed - 10.0).abs() < f64::EPSILON);
}
#[test]
fn test_task_speed_multiple_samples() {
let mut task = Task::new(0, "test", Some(100.0));
task.start_time = Some(0.0);
for i in 0..10 {
task.completed = (i + 1) as f64 * 5.0;
task.record_sample((i + 1) as f64, 30.0);
}
let speed = task.speed().unwrap();
assert!((speed - 5.0).abs() < 0.1);
}
#[test]
fn test_task_speed_with_sliding_window() {
let mut task = Task::new(0, "test", Some(100.0));
task.start_time = Some(0.0);
task.completed = 10.0;
task.record_sample(1.0, 5.0);
task.completed = 20.0;
task.record_sample(2.0, 5.0);
task.completed = 60.0;
task.record_sample(10.0, 5.0);
task.completed = 70.0;
task.record_sample(11.0, 5.0);
let speed = task.speed().unwrap();
assert!((speed - 10.0).abs() < 0.1);
}
#[test]
fn test_task_speed_finished() {
let mut task = Task::new(0, "test", Some(100.0));
task.start_time = Some(0.0);
task.completed = 50.0;
task.record_sample(1.0, 30.0);
task.completed = 100.0;
task.record_sample(2.0, 30.0);
task.finished_speed = Some(50.0);
task.finished_time = Some(2.0);
assert_eq!(task.speed(), Some(50.0));
}
#[test]
fn test_task_time_remaining_no_speed() {
let mut task = Task::new(0, "test", Some(100.0));
task.completed = 50.0;
assert!(task.time_remaining().is_none());
}
#[test]
fn test_task_time_remaining_no_total() {
let task = Task::new(0, "test", None);
assert!(task.time_remaining().is_none());
}
#[test]
fn test_task_time_remaining_with_speed() {
let mut task = Task::new(0, "test", Some(100.0));
task.start_time = Some(0.0);
task.completed = 20.0;
task.record_sample(1.0, 30.0);
task.completed = 40.0;
task.record_sample(2.0, 30.0);
let remaining = task.time_remaining().unwrap();
assert!((remaining - 3.0).abs() < 0.1);
}
#[test]
fn test_task_time_remaining_finished() {
let mut task = Task::new(0, "test", Some(100.0));
task.completed = 100.0;
task.finished_time = Some(5.0);
assert_eq!(task.time_remaining(), Some(0.0));
}
#[test]
fn test_task_record_sample() {
let mut task = Task::new(0, "test", Some(100.0));
task.completed = 10.0;
task.record_sample(1.0, 30.0);
assert_eq!(task.samples.len(), 1);
assert_eq!(task.progress.len(), 1);
}
#[test]
fn test_task_record_sample_pruning() {
let mut task = Task::new(0, "test", Some(100.0));
for i in 0..5 {
task.completed = (i + 1) as f64 * 10.0;
task.record_sample((i + 1) as f64 * 10.0, 10.0);
}
assert_eq!(task.samples.len(), 2);
assert_eq!(task.progress.len(), 5);
}
#[test]
fn test_format_time_zero() {
assert_eq!(format_time(0.0), "0:00");
}
#[test]
fn test_format_time_seconds() {
assert_eq!(format_time(45.0), "0:45");
}
#[test]
fn test_format_time_minutes() {
assert_eq!(format_time(125.0), "2:05");
}
#[test]
fn test_format_time_hours() {
assert_eq!(format_time(3661.0), "1:01:01");
}
#[test]
fn test_format_time_rounding() {
assert_eq!(format_time(59.6), "1:00");
}
#[test]
fn test_text_column_description() {
let col = TextColumn::new("{task.description}");
let task = Task::new(0, "Downloading", Some(100.0));
let text = col.render(&task);
assert_eq!(text.plain(), "Downloading");
}
#[test]
fn test_text_column_percentage() {
let col = TextColumn::new("{task.percentage:.0f}%");
let mut task = Task::new(0, "test", Some(100.0));
task.completed = 50.0;
let text = col.render(&task);
assert_eq!(text.plain(), "50%");
}
#[test]
fn test_text_column_percentage_1f() {
let col = TextColumn::new("{task.percentage:.1f}%");
let mut task = Task::new(0, "test", Some(100.0));
task.completed = 33.3;
let text = col.render(&task);
assert_eq!(text.plain(), "33.3%");
}
#[test]
fn test_text_column_completed_and_total() {
let col = TextColumn::new("{task.completed}/{task.total}");
let mut task = Task::new(0, "test", Some(200.0));
task.completed = 50.0;
let text = col.render(&task);
assert_eq!(text.plain(), "50/200");
}
#[test]
fn test_text_column_no_total() {
let col = TextColumn::new("{task.total}");
let task = Task::new(0, "test", None);
let text = col.render(&task);
assert_eq!(text.plain(), "?");
}
#[test]
fn test_text_column_speed_unknown() {
let col = TextColumn::new("{task.speed}");
let task = Task::new(0, "test", Some(100.0));
let text = col.render(&task);
assert_eq!(text.plain(), "?");
}
#[test]
fn test_text_column_fields() {
let col = TextColumn::new("Status: {task.fields.status}");
let mut task = Task::new(0, "test", Some(100.0));
task.fields
.insert("status".to_string(), "running".to_string());
let text = col.render(&task);
assert_eq!(text.plain(), "Status: running");
}
#[test]
fn test_text_column_with_style() {
let style = Style::parse("bold").unwrap();
let col = TextColumn::new("test").with_style(style.clone());
assert_eq!(col.style, Some(style));
}
#[test]
fn test_text_column_with_justify() {
let col = TextColumn::new("test").with_justify(JustifyMethod::Right);
assert_eq!(col.justify, JustifyMethod::Right);
}
#[test]
fn test_bar_column_default() {
let col = BarColumn::default();
assert_eq!(col.bar_width, Some(40));
assert_eq!(col.style, "bar.back");
assert_eq!(col.complete_style, "bar.complete");
}
#[test]
fn test_bar_column_render() {
let col = BarColumn::new().with_bar_width(Some(10));
let mut task = Task::new(0, "test", Some(100.0));
task.completed = 50.0;
let text = col.render(&task);
assert!(!text.plain().is_empty());
}
#[test]
fn test_bar_column_render_no_total() {
let col = BarColumn::new().with_bar_width(Some(10));
let task = Task::new(0, "test", None);
let text = col.render(&task);
assert!(!text.plain().is_empty());
}
#[test]
fn test_bar_column_render_complete() {
let col = BarColumn::new().with_bar_width(Some(10));
let mut task = Task::new(0, "test", Some(100.0));
task.completed = 100.0;
let text = col.render(&task);
assert!(!text.plain().is_empty());
}
#[test]
fn test_spinner_column_default() {
let col = SpinnerColumn::default();
assert_eq!(col.spinner_name, "dots");
}
#[test]
fn test_spinner_column_render() {
let col = SpinnerColumn::new("dots");
let mut task = Task::new(0, "test", Some(100.0));
task.start_time = Some(0.0);
let text = col.render(&task);
assert!(!text.plain().is_empty());
}
#[test]
fn test_spinner_column_render_finished() {
let col = SpinnerColumn::new("dots");
let mut task = Task::new(0, "test", Some(100.0));
task.finished_time = Some(1.0);
let text = col.render(&task);
assert!(text.plain().contains('\u{2714}'));
}
#[test]
fn test_spinner_column_max_refresh() {
let col = SpinnerColumn::default();
assert!(col.max_refresh().is_some());
}
#[test]
fn test_spinner_column_with_style() {
let style = Style::parse("bold").unwrap();
let col = SpinnerColumn::new("dots").with_style(style.clone());
assert_eq!(col.style, Some(style));
}
#[test]
fn test_spinner_column_with_finished_text() {
let text = Text::new("DONE", Style::null());
let col = SpinnerColumn::new("dots").with_finished_text(text.clone());
assert_eq!(col.finished_text.plain(), "DONE");
}
#[test]
fn test_spinner_column_invalid_name() {
let col = SpinnerColumn::new("nonexistent_spinner_xyz");
let task = Task::new(0, "test", Some(100.0));
let text = col.render(&task);
assert_eq!(text.plain(), "?");
}
#[test]
fn test_time_elapsed_not_started() {
let col = TimeElapsedColumn;
let task = Task::new(0, "test", Some(100.0));
let text = col.render(&task);
assert_eq!(text.plain(), "0:00");
}
#[test]
fn test_time_elapsed_with_time() {
let col = TimeElapsedColumn;
let mut task = Task::new(0, "test", Some(100.0));
task.start_time = Some(0.0);
task.stop_time = Some(65.0);
let text = col.render(&task);
assert_eq!(text.plain(), "1:05");
}
#[test]
fn test_time_elapsed_hours() {
let col = TimeElapsedColumn;
let mut task = Task::new(0, "test", Some(100.0));
task.start_time = Some(0.0);
task.stop_time = Some(3723.0);
let text = col.render(&task);
assert_eq!(text.plain(), "1:02:03");
}
#[test]
fn test_time_remaining_unknown() {
let col = TimeRemainingColumn::default();
let task = Task::new(0, "test", Some(100.0));
let text = col.render(&task);
assert_eq!(text.plain(), "-:--:--");
}
#[test]
fn test_time_remaining_finished() {
let col = TimeRemainingColumn::default();
let mut task = Task::new(0, "test", Some(100.0));
task.finished_time = Some(5.0);
let text = col.render(&task);
assert_eq!(text.plain(), "0:00");
}
#[test]
fn test_time_remaining_with_speed() {
let col = TimeRemainingColumn::default();
let mut task = Task::new(0, "test", Some(100.0));
task.start_time = Some(0.0);
task.completed = 20.0;
task.record_sample(1.0, 30.0);
task.completed = 40.0;
task.record_sample(2.0, 30.0);
let text = col.render(&task);
assert_eq!(text.plain(), "0:03");
}
#[test]
fn test_time_remaining_elapsed_when_finished() {
let col = TimeRemainingColumn {
compact: false,
elapsed_when_finished: true,
};
let mut task = Task::new(0, "test", Some(100.0));
task.start_time = Some(0.0);
task.stop_time = Some(120.0);
task.finished_time = Some(120.0);
let text = col.render(&task);
assert_eq!(text.plain(), "2:00");
}
#[test]
fn test_task_progress_column() {
let col = TaskProgressColumn::default();
let mut task = Task::new(0, "test", Some(100.0));
task.completed = 42.0;
let text = col.render(&task);
assert_eq!(text.plain(), "42/100");
}
#[test]
fn test_task_progress_column_no_total() {
let col = TaskProgressColumn::default();
let task = Task::new(0, "test", None);
let text = col.render(&task);
assert_eq!(text.plain(), "0/?");
}
#[test]
fn test_task_progress_column_custom_separator() {
let col = TaskProgressColumn::new().with_separator(" of ");
let mut task = Task::new(0, "test", Some(50.0));
task.completed = 25.0;
let text = col.render(&task);
assert_eq!(text.plain(), "25 of 50");
}
#[test]
fn test_file_size_column_bytes() {
let col = FileSizeColumn;
let mut task = Task::new(0, "test", Some(1000.0));
task.completed = 500.0;
let text = col.render(&task);
assert_eq!(text.plain(), "500 bytes");
}
#[test]
fn test_file_size_column_kb() {
let col = FileSizeColumn;
let mut task = Task::new(0, "test", Some(100000.0));
task.completed = 5000.0;
let text = col.render(&task);
assert_eq!(text.plain(), "5.0 kB");
}
#[test]
fn test_file_size_column_mb() {
let col = FileSizeColumn;
let mut task = Task::new(0, "test", Some(100000000.0));
task.completed = 5000000.0;
let text = col.render(&task);
assert_eq!(text.plain(), "5.0 MB");
}
#[test]
fn test_total_file_size_column() {
let col = TotalFileSizeColumn;
let task = Task::new(0, "test", Some(5000.0));
let text = col.render(&task);
assert_eq!(text.plain(), "5.0 kB");
}
#[test]
fn test_total_file_size_column_no_total() {
let col = TotalFileSizeColumn;
let task = Task::new(0, "test", None);
let text = col.render(&task);
assert_eq!(text.plain(), "0 bytes");
}
#[test]
fn test_mofn_column() {
let col = MofNCompleteColumn::default();
let mut task = Task::new(0, "test", Some(50.0));
task.completed = 25.0;
let text = col.render(&task);
assert_eq!(text.plain(), "25/50");
}
#[test]
fn test_mofn_column_no_total() {
let col = MofNCompleteColumn::default();
let task = Task::new(0, "test", None);
let text = col.render(&task);
assert_eq!(text.plain(), "0/?");
}
#[test]
fn test_mofn_column_custom_separator() {
let col = MofNCompleteColumn::new().with_separator(" / ");
let mut task = Task::new(0, "test", Some(100.0));
task.completed = 75.0;
let text = col.render(&task);
assert_eq!(text.plain(), "75 / 100");
}
#[test]
fn test_download_column() {
let col = DownloadColumn;
let mut task = Task::new(0, "test", Some(1000000.0));
task.completed = 500000.0;
let text = col.render(&task);
assert_eq!(text.plain(), "500.0 kB/1.0 MB");
}
#[test]
fn test_download_column_no_total() {
let col = DownloadColumn;
let mut task = Task::new(0, "test", None);
task.completed = 1000.0;
let text = col.render(&task);
assert_eq!(text.plain(), "1.0 kB/?");
}
#[test]
fn test_transfer_speed_column_no_speed() {
let col = TransferSpeedColumn;
let task = Task::new(0, "test", Some(100.0));
let text = col.render(&task);
assert_eq!(text.plain(), "?");
}
#[test]
fn test_transfer_speed_column_with_speed() {
let col = TransferSpeedColumn;
let mut task = Task::new(0, "test", Some(100000.0));
task.start_time = Some(0.0);
task.completed = 1000.0;
task.record_sample(1.0, 30.0);
task.completed = 11000.0;
task.record_sample(2.0, 30.0);
let text = col.render(&task);
assert_eq!(text.plain(), "10.0 kB/s");
}
#[test]
fn test_progress_creation() {
let progress = Progress::new(Progress::default_columns());
assert!(progress.tasks.is_empty());
assert_eq!(progress.task_id_counter, 0);
}
#[test]
fn test_progress_default_columns() {
let cols = Progress::default_columns();
assert_eq!(cols.len(), 4);
}
#[test]
fn test_progress_add_task() {
let mut progress = Progress::new(Progress::default_columns()).with_get_time(|| 1.0);
let id = progress.add_task("Test", Some(100.0));
assert_eq!(id, 0);
assert_eq!(progress.tasks.len(), 1);
assert_eq!(progress.tasks[0].description, "Test");
assert_eq!(progress.tasks[0].total, Some(100.0));
assert!(progress.tasks[0].started());
}
#[test]
fn test_progress_add_multiple_tasks() {
let mut progress = Progress::new(Progress::default_columns()).with_get_time(|| 1.0);
let id1 = progress.add_task("First", Some(100.0));
let id2 = progress.add_task("Second", Some(200.0));
assert_eq!(id1, 0);
assert_eq!(id2, 1);
assert_eq!(progress.tasks.len(), 2);
}
#[test]
fn test_progress_update_completed() {
let mut progress = Progress::new(Progress::default_columns()).with_get_time(|| 1.0);
let id = progress.add_task("Test", Some(100.0));
progress.update(id, Some(50.0), None, None, None, None);
assert!((progress.tasks[0].completed - 50.0).abs() < f64::EPSILON);
}
#[test]
fn test_progress_update_total() {
let mut progress = Progress::new(Progress::default_columns()).with_get_time(|| 1.0);
let id = progress.add_task("Test", Some(100.0));
progress.update(id, None, Some(200.0), None, None, None);
assert_eq!(progress.tasks[0].total, Some(200.0));
}
#[test]
fn test_progress_update_advance() {
let mut progress = Progress::new(Progress::default_columns()).with_get_time(|| 1.0);
let id = progress.add_task("Test", Some(100.0));
progress.update(id, None, None, Some(10.0), None, None);
assert!((progress.tasks[0].completed - 10.0).abs() < f64::EPSILON);
progress.update(id, None, None, Some(15.0), None, None);
assert!((progress.tasks[0].completed - 25.0).abs() < f64::EPSILON);
}
#[test]
fn test_progress_update_description() {
let mut progress = Progress::new(Progress::default_columns()).with_get_time(|| 1.0);
let id = progress.add_task("Old", Some(100.0));
progress.update(id, None, None, None, Some("New"), None);
assert_eq!(progress.tasks[0].description, "New");
}
#[test]
fn test_progress_update_visible() {
let mut progress = Progress::new(Progress::default_columns()).with_get_time(|| 1.0);
let id = progress.add_task("Test", Some(100.0));
assert!(progress.tasks[0].visible);
progress.update(id, None, None, None, None, Some(false));
assert!(!progress.tasks[0].visible);
}
#[test]
fn test_progress_advance() {
let mut progress = Progress::new(Progress::default_columns()).with_get_time(|| 1.0);
let id = progress.add_task("Test", Some(100.0));
progress.advance(id, 30.0);
assert!((progress.tasks[0].completed - 30.0).abs() < f64::EPSILON);
}
#[test]
fn test_progress_start_task() {
let mut progress = Progress::new(Progress::default_columns()).with_get_time(|| 1.0);
let id = progress.add_task("Test", Some(100.0));
assert!(progress.tasks[0].started());
progress.start_task(id);
assert!(progress.tasks[0].started());
}
#[test]
fn test_progress_stop_task() {
let mut progress = Progress::new(Progress::default_columns()).with_get_time(|| 1.0);
let id = progress.add_task("Test", Some(100.0));
progress.stop_task(id);
assert!(progress.tasks[0].stop_time.is_some());
}
#[test]
fn test_progress_remove_task() {
let mut progress = Progress::new(Progress::default_columns()).with_get_time(|| 1.0);
let id = progress.add_task("Test", Some(100.0));
assert_eq!(progress.tasks.len(), 1);
progress.remove_task(id);
assert_eq!(progress.tasks.len(), 0);
}
#[test]
fn test_progress_remove_nonexistent_task() {
let mut progress = Progress::new(Progress::default_columns()).with_get_time(|| 1.0);
progress.add_task("Test", Some(100.0));
progress.remove_task(999);
assert_eq!(progress.tasks.len(), 1);
}
#[test]
fn test_progress_get_task() {
let mut progress = Progress::new(Progress::default_columns()).with_get_time(|| 1.0);
let id = progress.add_task("Test", Some(100.0));
let task = progress.get_task(id).unwrap();
assert_eq!(task.description, "Test");
}
#[test]
fn test_progress_get_task_nonexistent() {
let progress = Progress::new(Progress::default_columns());
assert!(progress.get_task(999).is_none());
}
#[test]
fn test_progress_get_task_mut() {
let mut progress = Progress::new(Progress::default_columns()).with_get_time(|| 1.0);
let id = progress.add_task("Test", Some(100.0));
{
let task = progress.get_task_mut(id).unwrap();
task.description = "Modified".to_string();
}
assert_eq!(progress.tasks[0].description, "Modified");
}
#[test]
fn test_progress_finished_count() {
let mut progress = Progress::new(Progress::default_columns()).with_get_time(|| 1.0);
let id1 = progress.add_task("First", Some(100.0));
let _id2 = progress.add_task("Second", Some(100.0));
assert_eq!(progress.finished_count(), 0);
progress.update(id1, Some(100.0), None, None, None, None);
assert_eq!(progress.finished_count(), 1);
}
#[test]
fn test_progress_visible_count() {
let mut progress = Progress::new(Progress::default_columns()).with_get_time(|| 1.0);
let id1 = progress.add_task("First", Some(100.0));
let _id2 = progress.add_task("Second", Some(100.0));
assert_eq!(progress.visible_count(), 2);
progress.update(id1, None, None, None, None, Some(false));
assert_eq!(progress.visible_count(), 1);
}
#[test]
fn test_progress_auto_finish() {
let mut progress = Progress::new(Progress::default_columns()).with_get_time(|| 1.0);
let id = progress.add_task("Test", Some(100.0));
progress.update(id, Some(100.0), None, None, None, None);
assert!(progress.tasks[0].finished());
assert!(progress.tasks[0].finished_time.is_some());
}
#[test]
fn test_progress_auto_finish_over_total() {
let mut progress = Progress::new(Progress::default_columns()).with_get_time(|| 1.0);
let id = progress.add_task("Test", Some(100.0));
progress.update(id, Some(150.0), None, None, None, None);
assert!(progress.tasks[0].finished());
}
#[test]
fn test_progress_make_tasks_table_empty() {
let progress = Progress::new(Progress::default_columns());
let table = progress.make_tasks_table();
assert_eq!(table.rows.len(), 0);
}
#[test]
fn test_progress_make_tasks_table_with_tasks() {
let mut progress = Progress::new(Progress::default_columns()).with_get_time(|| 1.0);
progress.add_task("First", Some(100.0));
progress.add_task("Second", Some(200.0));
let table = progress.make_tasks_table();
assert_eq!(table.rows.len(), 2);
assert_eq!(table.columns.len(), 4); }
#[test]
fn test_progress_make_tasks_table_hidden_tasks() {
let mut progress = Progress::new(Progress::default_columns()).with_get_time(|| 1.0);
let _id1 = progress.add_task("Visible", Some(100.0));
let id2 = progress.add_task("Hidden", Some(100.0));
progress.update(id2, None, None, None, None, Some(false));
let table = progress.make_tasks_table();
assert_eq!(table.rows.len(), 1);
}
#[test]
fn test_progress_renderable() {
let mut progress = Progress::new(Progress::default_columns()).with_get_time(|| 1.0);
progress.add_task("Test", Some(100.0));
let console = test_console();
let opts = console.options();
let segments = progress.rich_console(&console, &opts);
assert!(!segments.is_empty());
let text: String = segments.iter().map(|s| s.text.as_str()).collect();
assert!(text.contains("Test"));
}
#[test]
fn test_progress_renderable_empty() {
let progress = Progress::new(Progress::default_columns());
let console = test_console();
let opts = console.options();
let segments = progress.rich_console(&console, &opts);
let text: String = segments.iter().map(|s| s.text.as_str()).collect();
assert!(text.is_empty() || text.trim().is_empty());
}
#[test]
fn test_progress_with_speed_estimate_period() {
let progress = Progress::new(Progress::default_columns()).with_speed_estimate_period(60.0);
assert!((progress.speed_estimate_period - 60.0).abs() < f64::EPSILON);
}
#[test]
fn test_progress_with_disable() {
let progress = Progress::new(Progress::default_columns()).with_disable(true);
assert!(progress.disable);
}
#[test]
fn test_progress_with_expand() {
let progress = Progress::new(Progress::default_columns()).with_expand(true);
assert!(progress.expand);
}
#[test]
fn test_progress_with_get_time() {
let progress = Progress::new(Progress::default_columns()).with_get_time(|| 42.0);
assert!(((progress.get_time)() - 42.0).abs() < f64::EPSILON);
}
#[test]
fn test_progress_start_stop() {
let mut progress = Progress::new(Progress::default_columns())
.with_console(test_console())
.with_auto_refresh(false);
progress.start();
progress.stop();
}
#[test]
fn test_progress_start_stop_disabled() {
let mut progress = Progress::new(Progress::default_columns()).with_disable(true);
progress.start();
progress.stop();
}
#[test]
fn test_progress_refresh() {
let mut progress = Progress::new(Progress::default_columns())
.with_console(test_console())
.with_auto_refresh(false)
.with_get_time(|| 1.0);
progress.add_task("Test", Some(100.0));
progress.start();
progress.refresh();
progress.stop();
}
#[test]
fn test_progress_refresh_disabled() {
let mut progress = Progress::new(Progress::default_columns()).with_disable(true);
progress.refresh();
}
#[test]
fn test_progress_multiple_tasks() {
let mut progress = Progress::new(Progress::default_columns()).with_get_time(|| 1.0);
let id1 = progress.add_task("Download", Some(1000.0));
let id2 = progress.add_task("Process", Some(500.0));
let id3 = progress.add_task("Upload", Some(2000.0));
progress.advance(id1, 500.0);
progress.advance(id2, 250.0);
progress.advance(id3, 100.0);
assert!((progress.tasks[0].completed - 500.0).abs() < f64::EPSILON);
assert!((progress.tasks[1].completed - 250.0).abs() < f64::EPSILON);
assert!((progress.tasks[2].completed - 100.0).abs() < f64::EPSILON);
}
#[test]
fn test_progress_indeterminate_task() {
let mut progress = Progress::new(Progress::default_columns()).with_get_time(|| 1.0);
let id = progress.add_task("Loading...", None);
progress.advance(id, 10.0);
assert!((progress.tasks[0].completed - 10.0).abs() < f64::EPSILON);
assert_eq!(progress.tasks[0].total, None);
assert!((progress.tasks[0].percentage() - 0.0).abs() < f64::EPSILON);
assert!(!progress.tasks[0].finished());
}
#[test]
fn test_track_iterator() {
let items: Vec<i32> = track(0..5, "Counting", Some(5.0)).collect();
assert_eq!(items, vec![0, 1, 2, 3, 4]);
}
#[test]
fn test_track_iterator_empty() {
let items: Vec<i32> = track(std::iter::empty::<i32>(), "Empty", Some(0.0)).collect();
assert!(items.is_empty());
}
#[test]
fn test_track_iterator_count() {
let count = track(0..10, "Test", Some(10.0)).count();
assert_eq!(count, 10);
}
#[test]
fn test_progress_tasks_accessor() {
let mut progress = Progress::new(Progress::default_columns()).with_get_time(|| 1.0);
progress.add_task("A", Some(100.0));
progress.add_task("B", Some(200.0));
let tasks = progress.tasks();
assert_eq!(tasks.len(), 2);
assert_eq!(tasks[0].description, "A");
assert_eq!(tasks[1].description, "B");
}
#[test]
fn test_progress_update_nonexistent_task() {
let mut progress = Progress::new(Progress::default_columns()).with_get_time(|| 1.0);
progress.update(999, Some(50.0), None, None, None, None);
progress.advance(999, 10.0);
progress.start_task(999);
progress.stop_task(999);
}
#[test]
fn test_task_fields_in_render() {
let col = TextColumn::new("{task.fields.status} - {task.description}");
let mut task = Task::new(0, "My Task", Some(100.0));
task.fields
.insert("status".to_string(), "active".to_string());
let text = col.render(&task);
assert_eq!(text.plain(), "active - My Task");
}
#[test]
fn test_progress_custom_columns() {
let columns: Vec<Box<dyn ProgressColumn>> = vec![
Box::new(SpinnerColumn::default()),
Box::new(TextColumn::new("{task.description}")),
Box::new(BarColumn::new().with_bar_width(Some(20))),
Box::new(TaskProgressColumn::default()),
Box::new(TimeElapsedColumn),
Box::new(TimeRemainingColumn::default()),
];
let mut progress = Progress::new(columns).with_get_time(|| 1.0);
let id = progress.add_task("Custom", Some(100.0));
progress.advance(id, 50.0);
let table = progress.make_tasks_table();
assert_eq!(table.columns.len(), 6);
assert_eq!(table.rows.len(), 1);
}
#[test]
fn test_progress_all_column_types() {
let columns: Vec<Box<dyn ProgressColumn>> = vec![
Box::new(SpinnerColumn::default()),
Box::new(TextColumn::new("{task.description}")),
Box::new(BarColumn::default()),
Box::new(TaskProgressColumn::default()),
Box::new(TimeElapsedColumn),
Box::new(TimeRemainingColumn::default()),
Box::new(FileSizeColumn),
Box::new(TotalFileSizeColumn),
Box::new(MofNCompleteColumn::default()),
Box::new(DownloadColumn),
Box::new(TransferSpeedColumn),
];
let mut progress = Progress::new(columns).with_get_time(|| 1.0);
let id = progress.add_task("Full test", Some(100000.0));
progress.advance(id, 50000.0);
let task = progress.get_task(id).unwrap();
for (i, col) in progress.columns.iter().enumerate() {
let text = col.render(task);
assert!(
!text.plain().is_empty() || i == 0, "column {} rendered empty text",
i
);
}
}
#[test]
fn test_renderable_column_basic() {
let col = RenderableColumn::new(|task: &Task| Text::new(&task.description, Style::null()));
let task = Task::new(0, "Hello", Some(100.0));
let text = col.render(&task);
assert_eq!(text.plain(), "Hello");
}
#[test]
fn test_renderable_column_custom_content() {
let col = RenderableColumn::new(|task: &Task| {
let msg = format!(
"Step {} of {}",
task.completed as u64,
task.total.map(|t| t as u64).unwrap_or(0)
);
Text::new(&msg, Style::null())
});
let mut task = Task::new(0, "test", Some(10.0));
task.completed = 7.0;
let text = col.render(&task);
assert_eq!(text.plain(), "Step 7 of 10");
}
#[test]
fn test_renderable_column_uses_task_fields() {
let col = RenderableColumn::new(|task: &Task| {
let status = task
.fields
.get("status")
.map(|s| s.as_str())
.unwrap_or("unknown");
Text::new(status, Style::null())
});
let mut task = Task::new(0, "test", Some(100.0));
task.fields
.insert("status".to_string(), "downloading".to_string());
let text = col.render(&task);
assert_eq!(text.plain(), "downloading");
}
#[test]
fn test_renderable_column_with_percentage() {
let col = RenderableColumn::new(|task: &Task| {
Text::new(&format!("{:.0}%", task.percentage()), Style::null())
});
let mut task = Task::new(0, "test", Some(200.0));
task.completed = 100.0;
let text = col.render(&task);
assert_eq!(text.plain(), "50%");
}
#[test]
fn test_renderable_column_in_progress() {
let columns: Vec<Box<dyn ProgressColumn>> = vec![
Box::new(TextColumn::new("{task.description}")),
Box::new(RenderableColumn::new(|task: &Task| {
Text::new(&format!("[{}]", task.completed as u64), Style::null())
})),
];
let mut progress = Progress::new(columns).with_get_time(|| 1.0);
let id = progress.add_task("Demo", Some(100.0));
progress.advance(id, 42.0);
let task = progress.get_task(id).unwrap();
let rendered = progress.columns[1].render(task);
assert_eq!(rendered.plain(), "[42]");
}
#[test]
fn test_renderable_column_indeterminate_task() {
let col = RenderableColumn::new(|task: &Task| {
if task.total.is_none() {
Text::new("...", Style::null())
} else {
Text::new("ok", Style::null())
}
});
let task = Task::new(0, "test", None);
assert_eq!(col.render(&task).plain(), "...");
let task2 = Task::new(1, "test", Some(10.0));
assert_eq!(col.render(&task2).plain(), "ok");
}
#[test]
fn test_progress_track_iterates_all_items() {
let mut progress = Progress::new(Progress::default_columns())
.with_disable(true)
.with_get_time(|| 1.0);
let items: Vec<i32> = progress.track(0..5, "Counting", Some(5.0)).collect();
assert_eq!(items, vec![0, 1, 2, 3, 4]);
}
#[test]
fn test_progress_track_advances_task() {
let mut progress = Progress::new(Progress::default_columns())
.with_disable(true)
.with_get_time(|| 1.0);
let task_id;
{
let mut tracker = progress.track(0..3, "Test", Some(3.0));
task_id = tracker.task_id();
while tracker.next().is_some() {}
}
let task = progress.get_task(task_id).unwrap();
assert!((task.completed - 3.0).abs() < f64::EPSILON);
}
#[test]
fn test_progress_track_with_known_total_finishes() {
let mut progress = Progress::new(Progress::default_columns())
.with_disable(true)
.with_get_time(|| 1.0);
let task_id;
{
let mut tracker = progress.track(0..10, "Finish", Some(10.0));
task_id = tracker.task_id();
while tracker.next().is_some() {}
}
let task = progress.get_task(task_id).unwrap();
assert!(
task.finished(),
"task should be finished after iterating all items"
);
assert!((task.percentage() - 100.0).abs() < f64::EPSILON);
}
#[test]
fn test_progress_track_with_none_total_indeterminate() {
let mut progress = Progress::new(Progress::default_columns())
.with_disable(true)
.with_get_time(|| 1.0);
let task_id;
{
let mut tracker = progress.track(0..4, "Indeterminate", None);
task_id = tracker.task_id();
while tracker.next().is_some() {}
}
let task = progress.get_task(task_id).unwrap();
assert!(!task.finished());
assert!((task.completed - 4.0).abs() < f64::EPSILON);
assert!((task.percentage() - 0.0).abs() < f64::EPSILON);
}
#[test]
fn test_progress_track_empty_iterator() {
let mut progress = Progress::new(Progress::default_columns())
.with_disable(true)
.with_get_time(|| 1.0);
let items: Vec<i32> = progress
.track(std::iter::empty::<i32>(), "Empty", Some(0.0))
.collect();
assert!(items.is_empty());
}
#[test]
fn test_progress_track_size_hint() {
let mut progress = Progress::new(Progress::default_columns())
.with_disable(true)
.with_get_time(|| 1.0);
let tracker = progress.track(0..10, "Sized", Some(10.0));
let (lower, upper) = tracker.size_hint();
assert_eq!(lower, 10);
assert_eq!(upper, Some(10));
}
#[test]
fn test_progress_track_partial_iteration() {
let mut progress = Progress::new(Progress::default_columns())
.with_disable(true)
.with_get_time(|| 1.0);
let task_id;
{
let mut tracker = progress.track(0..10, "Partial", Some(10.0));
task_id = tracker.task_id();
tracker.next();
tracker.next();
tracker.next();
}
let task = progress.get_task(task_id).unwrap();
assert!((task.completed - 3.0).abs() < f64::EPSILON);
assert!(!task.finished());
assert!((task.percentage() - 30.0).abs() < f64::EPSILON);
}
#[test]
fn test_progress_track_creates_task() {
let mut progress = Progress::new(Progress::default_columns())
.with_disable(true)
.with_get_time(|| 1.0);
assert_eq!(progress.tasks().len(), 0);
let _tracker = progress.track(0..5, "NewTask", Some(5.0));
assert_eq!(progress.tasks().len(), 1);
assert_eq!(progress.tasks()[0].description, "NewTask");
assert_eq!(progress.tasks()[0].total, Some(5.0));
}
#[test]
fn test_progress_iter_collects_all() {
let items: Vec<i32> = (0..10).progress("Collecting").collect();
assert_eq!(items, (0..10).collect::<Vec<_>>());
}
#[test]
fn test_progress_iter_advances_count() {
let mut pi = (0..7).progress("Counting");
pi.progress = Progress::new(Progress::default_columns()).with_disable(true);
pi.task_id = pi.progress.add_task("Counting", Some(7.0));
while pi.next().is_some() {}
let task = pi.progress.get_task(pi.task_id).unwrap();
assert!((task.completed - 7.0).abs() < f64::EPSILON);
}
#[test]
fn test_progress_iter_size_hint() {
let v = vec![1, 2, 3, 4, 5];
let pi = v.iter().progress("Hint");
let (lo, hi) = pi.size_hint();
assert_eq!(lo, 5);
assert_eq!(hi, Some(5));
}
#[test]
fn test_progress_iter_vec() {
let v = vec!["a", "b", "c"];
let result: Vec<&&str> = v.iter().progress("Vec").collect();
assert_eq!(result, vec![&"a", &"b", &"c"]);
}
#[test]
fn test_progress_iter_range() {
let result: Vec<u64> = (0u64..5).progress("Range").collect();
assert_eq!(result, vec![0, 1, 2, 3, 4]);
}
#[test]
fn test_progress_iter_with_total() {
let mut pi = (0..3).progress_with_total("Explicit", 3.0);
pi.progress = Progress::new(Progress::default_columns()).with_disable(true);
pi.task_id = pi.progress.add_task("Explicit", Some(3.0));
while pi.next().is_some() {}
let task = pi.progress.get_task(pi.task_id).unwrap();
assert_eq!(task.total, Some(3.0));
assert!((task.completed - 3.0).abs() < f64::EPSILON);
}
#[test]
fn test_progress_iter_empty() {
let result: Vec<i32> = std::iter::empty::<i32>().progress("Empty").collect();
assert!(result.is_empty());
}
#[test]
fn test_progress_iter_size_hint_range() {
let pi = (0..100).progress("Range hint");
let (lo, hi) = pi.size_hint();
assert_eq!(lo, 100);
assert_eq!(hi, Some(100));
}
#[test]
fn test_track_iterator_size_hint() {
let ti = track(0..50, "test", Some(50.0));
let (lo, hi) = ti.size_hint();
assert_eq!(lo, 50);
assert_eq!(hi, Some(50));
}
#[test]
fn test_reset_task() {
let mut progress = Progress::new(Progress::default_columns())
.with_disable(true)
.with_get_time(|| 1.0);
let tid = progress.add_task("resettable", Some(100.0));
progress.advance(tid, 50.0);
progress.update(tid, Some(100.0), None, None, None, None);
assert!(progress.get_task(tid).unwrap().finished());
progress.reset(tid);
let task = progress.get_task(tid).unwrap();
assert!((task.completed - 0.0).abs() < f64::EPSILON);
assert!(task.start_time.is_some());
assert!(task.stop_time.is_none());
assert!(task.finished_time.is_none());
assert!(task.finished_speed.is_none());
assert!(!task.finished());
}
#[test]
fn test_reset_nonexistent_task() {
let mut progress = Progress::new(Progress::default_columns())
.with_disable(true)
.with_get_time(|| 1.0);
progress.reset(999);
}
#[test]
fn test_all_tasks_finished_true() {
let mut progress = Progress::new(Progress::default_columns())
.with_disable(true)
.with_get_time(|| 1.0);
let t1 = progress.add_task("a", Some(10.0));
let t2 = progress.add_task("b", Some(20.0));
progress.update(t1, Some(10.0), None, None, None, None);
progress.update(t2, Some(20.0), None, None, None, None);
assert!(progress.all_tasks_finished());
}
#[test]
fn test_all_tasks_finished_false() {
let mut progress = Progress::new(Progress::default_columns())
.with_disable(true)
.with_get_time(|| 1.0);
let t1 = progress.add_task("a", Some(10.0));
let _t2 = progress.add_task("b", Some(20.0));
progress.update(t1, Some(10.0), None, None, None, None);
assert!(!progress.all_tasks_finished());
}
#[test]
fn test_all_tasks_finished_empty() {
let progress = Progress::new(Progress::default_columns()).with_disable(true);
assert!(progress.all_tasks_finished());
}
#[test]
fn test_progress_reader_counts_bytes() {
use std::io::Read;
let data = vec![0u8; 256];
let mut reader = ProgressReader::new(data.as_slice(), |_| {});
let mut buf = [0u8; 64];
reader.read(&mut buf).unwrap();
reader.read(&mut buf).unwrap();
reader.read(&mut buf).unwrap();
assert_eq!(reader.total_read(), 192);
}
#[test]
fn test_progress_reader_calls_callback() {
use std::cell::RefCell;
use std::io::Read;
use std::rc::Rc;
let counts = Rc::new(RefCell::new(Vec::<usize>::new()));
let counts_clone = Rc::clone(&counts);
let data = vec![1u8; 100];
let mut reader =
ProgressReader::new(data.as_slice(), move |n| counts_clone.borrow_mut().push(n));
let mut buf = [0u8; 30];
reader.read(&mut buf).unwrap();
reader.read(&mut buf).unwrap();
reader.read(&mut buf).unwrap();
reader.read(&mut buf).unwrap(); let recorded = counts.borrow();
assert_eq!(recorded.len(), 4);
assert_eq!(recorded[0], 30);
assert_eq!(recorded[1], 30);
assert_eq!(recorded[2], 30);
assert_eq!(recorded[3], 10);
}
#[test]
fn test_progress_reader_empty() {
use std::io::Read;
let data: Vec<u8> = vec![];
let mut reader = ProgressReader::new(data.as_slice(), |_| {});
let mut buf = [0u8; 64];
let n = reader.read(&mut buf).unwrap();
assert_eq!(n, 0);
assert_eq!(reader.total_read(), 0);
}
}