use std::sync::{Arc, Mutex};
use gilt::progress::{
BarColumn, DownloadColumn, Progress, ProgressColumn, SpinnerColumn, TextColumn,
TimeElapsedColumn, TimeRemainingColumn, TransferSpeedColumn,
};
use gilt::progress::{Task, TaskId};
fn make_task_with_elapsed(
id: usize,
description: &str,
total: Option<f64>,
elapsed_secs: f64,
) -> Task {
let mut task = Task::new(id, description, total);
task.start_time = Some(0.0);
task.stop_time = Some(elapsed_secs);
task
}
#[test]
fn bar_column_renders_partial_progress() {
let mut task = Task::new(0, "test", Some(100.0));
task.completed = 50.0;
let col = BarColumn::default();
let text = col.render(&task);
let plain = text.plain();
assert!(
!plain.is_empty(),
"bar column should produce non-empty output"
);
assert!(
plain.contains('\u{2501}') || !plain.is_empty(),
"partial bar should contain bar characters"
);
let char_count = plain.chars().count();
assert!(
char_count > 0 && char_count <= 40,
"bar width should be at most 40, got {char_count}"
);
}
#[test]
fn text_column_substitutes_task_description() {
let task = Task::new(0, "downloading", Some(100.0));
let col = TextColumn::new("{task.description}");
let text = col.render(&task);
assert_eq!(text.plain(), "downloading");
}
#[test]
fn time_elapsed_column_format() {
let zero = make_task_with_elapsed(0, "t", Some(100.0), 0.0);
let col = TimeElapsedColumn;
let rendered_zero = col.render(&zero).plain().to_string();
assert_eq!(rendered_zero, "0:00", "0 elapsed should be '0:00'");
let non_zero = make_task_with_elapsed(1, "t", Some(100.0), 73.0);
let rendered_nonzero = col.render(&non_zero).plain().to_string();
assert_eq!(rendered_nonzero, "1:13", "73s elapsed should be '1:13'");
}
#[test]
fn time_remaining_column_unknown_total_renders_dashes() {
let task = make_task_with_elapsed(0, "t", None, 5.0);
let col = TimeRemainingColumn::default();
let rendered = col.render(&task).plain().to_string();
assert_eq!(rendered, "-:--:--", "no-total task should render '-:--:--'");
}
#[test]
fn compact_time_remaining_column_format() {
let mut task = Task::new(0, "t", Some(100.0));
task.completed = 100.0;
task.start_time = Some(0.0);
task.finished_time = Some(10.0);
let col = TimeRemainingColumn {
compact: true,
elapsed_when_finished: false,
};
let rendered = col.render(&task).plain().to_string();
assert_eq!(rendered, "0:00", "finished task should render '0:00'");
}
#[test]
fn spinner_column_animates_with_elapsed() {
let early = make_task_with_elapsed(0, "t", Some(100.0), 0.0);
let later = make_task_with_elapsed(1, "t", Some(100.0), 0.5);
let col = SpinnerColumn::default();
let frame_early = col.render(&early).plain().to_string();
let frame_later = col.render(&later).plain().to_string();
assert!(
!frame_early.is_empty(),
"spinner frame at elapsed=0 should not be empty"
);
assert!(
!frame_later.is_empty(),
"spinner frame at elapsed=0.5 should not be empty"
);
assert_ne!(
frame_early, frame_later,
"spinner frames at elapsed=0 and elapsed=0.5 should differ"
);
}
#[test]
fn download_column_uses_decimal_units_below_threshold() {
let mut task = Task::new(0, "t", Some(2_000_000.0));
task.completed = 1_500.0;
let col = DownloadColumn::new();
let rendered = col.render(&task).plain().to_string();
assert!(
rendered.contains("kB"),
"1500 bytes should render with 'kB', got: {rendered}"
);
}
#[test]
fn download_column_uses_binary_units() {
let mut task = Task::new(0, "t", Some(10_485_760.0)); task.completed = 2_097_152.0;
let col = DownloadColumn::new().with_binary_units(true);
let rendered = col.render(&task).plain().to_string();
assert!(
rendered.contains("MiB"),
"2 MiB with binary units should render 'MiB', got: {rendered}"
);
}
#[test]
fn transfer_speed_column_after_some_advance() {
let clock = Arc::new(Mutex::new(0.0_f64));
let clock_clone = clock.clone();
let mut progress = Progress::new(vec![])
.with_disable(true)
.with_get_time(move || *clock_clone.lock().unwrap());
let id: TaskId = progress.add_task("dl", Some(1000.0));
progress.advance(id, 100.0);
*clock.lock().unwrap() = 1.0;
progress.advance(id, 200.0);
let task = progress.get_task(id).unwrap();
let col = TransferSpeedColumn::new();
let rendered = col.render(task).plain().to_string();
assert_ne!(rendered, "?", "speed column should show a rate, got '?'");
assert!(
rendered.contains("/s"),
"speed column should contain '/s', got: {rendered}"
);
}
#[test]
fn task_ids_are_monotonic() {
let mut p = Progress::new(vec![]).with_disable(true);
let id0 = p.add_task("a", Some(10.0));
let id1 = p.add_task("b", Some(10.0));
let id2 = p.add_task("c", Some(10.0));
assert!(id0 < id1, "task IDs should be monotonically increasing");
assert!(id1 < id2, "task IDs should be monotonically increasing");
}
#[test]
fn task_finished_when_completed_equals_total() {
let mut p = Progress::new(vec![]).with_disable(true);
let id = p.add_task("work", Some(10.0));
p.advance(id, 10.0);
let task = p.get_task(id).unwrap();
assert!(
task.finished(),
"task should be finished after completing total"
);
}
#[test]
fn task_create_with_total_zero() {
let task = Task::new(0, "zero", Some(0.0));
let pct = task.percentage();
assert_eq!(pct, 0.0, "percentage with total=0 should be 0.0, not panic");
}
#[test]
fn task_speed_window_estimate() {
let clock = Arc::new(Mutex::new(0.0_f64));
let clock_clone = clock.clone();
let mut p = Progress::new(vec![])
.with_disable(true)
.with_get_time(move || *clock_clone.lock().unwrap());
let id = p.add_task("work", Some(1000.0));
for i in 1..=10_u64 {
*clock.lock().unwrap() = i as f64 * 0.1;
p.advance(id, 100.0);
}
let task = p.get_task(id).unwrap();
let speed = task
.speed()
.expect("speed should be known after 10 samples");
assert!(
speed > 500.0 && speed < 2000.0,
"speed estimate should be ~1000 units/s, got {speed}"
);
}
#[test]
fn task_progress_finished_speed_freezes_after_completion() {
let clock = Arc::new(Mutex::new(0.0_f64));
let clock_clone = clock.clone();
let mut p = Progress::new(vec![])
.with_disable(true)
.with_get_time(move || *clock_clone.lock().unwrap());
let id = p.add_task("work", Some(100.0));
*clock.lock().unwrap() = 0.5;
p.advance(id, 50.0);
*clock.lock().unwrap() = 1.0;
p.advance(id, 50.0);
let task = p.get_task(id).unwrap();
assert!(task.finished(), "task should be finished");
let frozen = task.speed();
assert!(
frozen.is_some(),
"finished task should have a speed snapshot"
);
*clock.lock().unwrap() = 2.0;
p.advance(id, 10.0);
let task = p.get_task(id).unwrap();
assert_eq!(
task.speed(),
frozen,
"speed should be frozen after task completion"
);
}
#[test]
fn progress_with_none_total_renders_pulse_bar() {
let task = Task::new(0, "spin", None);
let col = BarColumn::default();
let text = col.render(&task);
let plain = text.plain();
assert!(
!plain.is_empty(),
"pulse bar should produce non-empty output"
);
}
#[test]
fn reset_clears_completed_and_restarts_start_time() {
let clock = Arc::new(Mutex::new(10.0_f64));
let clock_clone = clock.clone();
let mut p = Progress::new(vec![])
.with_disable(true)
.with_get_time(move || *clock_clone.lock().unwrap());
let id = p.add_task("work", Some(100.0));
p.advance(id, 50.0);
*clock.lock().unwrap() = 20.0;
p.reset(id);
let task = p.get_task(id).unwrap();
assert_eq!(task.completed, 0.0, "reset should clear completed to 0");
assert!(
task.finished_time.is_none(),
"reset should clear finished_time"
);
assert_eq!(
task.start_time,
Some(20.0),
"reset should restart start_time to current clock"
);
}
#[test]
fn expand_bar_fills_remaining_width() {
let mut p = Progress::new(vec![
Box::new(TextColumn::new("{task.description}")),
Box::new(BarColumn::default()),
])
.with_disable(true)
.with_expand(true);
p.add_task("work", Some(100.0));
let table = p.make_tasks_table();
assert!(
table.expand(),
"make_tasks_table() should produce an expanded table when with_expand(true)"
);
}