1use std::time::{Duration, Instant};
15use tracing::warn;
16
17pub struct Progress {
19 name: String,
20 pub work_done: u64,
21 started_at: Instant,
22 work_total: u64,
23}
24
25impl Progress {
26 pub fn new(name: &str, work_todo: u64) -> Self {
40 Self {
41 name: name.to_owned(),
42 started_at: Instant::now(),
43 work_done: 0,
44 work_total: work_todo,
45 }
46 }
47
48 pub fn inc_work_done(&mut self) {
57 self.work_done = self.work_done + 1;
58 }
59
60 pub fn inc_work_done_by(&mut self, units: u64) {
62 self.work_done = self.work_done + units;
63 }
64
65 pub fn set_work_done(&mut self, units: u64) {
67 self.work_done = units;
68 }
69
70 fn estimate_time_left(&self) -> Duration {
73 if self.work_done > self.work_total {
74 warn!(self.work_done, self.work_total, "work done is larger than work total, using work done == work total to calculate time left");
75 }
76 let work_not_done = self
77 .work_total
78 .checked_sub(self.work_done)
79 .unwrap_or(self.work_total);
80 let not_done_to_done_ratio = work_not_done as f64 / self.work_done as f64;
81 let seconds_since_start = Instant::now() - self.started_at;
82 let eta_seconds = not_done_to_done_ratio * seconds_since_start.as_secs() as f64;
83
84 Duration::from_secs(eta_seconds as u64)
85 }
86
87 pub fn get_progress_string(&self) -> String {
90 let time_elapsed = format!("{:.0?}", Instant::now().duration_since(self.started_at));
91
92 let eta = if self.work_done == self.work_total {
93 "done!".to_string()
94 } else {
95 humantime::format_duration(self.estimate_time_left()).to_string()
96 };
97
98 format!(
99 "{} {}/{} - {:.1}% started {} ago, eta: {}",
100 self.name,
101 self.work_done,
102 self.work_total,
103 self.work_done as f64 / self.work_total as f64 * 100f64,
104 time_elapsed,
105 eta
106 )
107 }
108}
109
110#[cfg(test)]
111mod tests {
112 use std::thread;
113
114 use super::*;
115
116 #[test]
117 fn inc_work_done_test() {
118 let mut progress = Progress::new("test progress", 100);
119 progress.inc_work_done();
120 assert_eq!(progress.work_done, 1);
121 }
122
123 #[test]
124 fn inc_work_done_by_test() {
125 let mut progress = Progress::new("test progress", 100);
126 progress.inc_work_done_by(10);
127 assert_eq!(progress.work_done, 10);
128 }
129
130 #[test]
131 fn set_work_done_test() {
132 let mut progress = Progress::new("test progress", 100);
133 progress.set_work_done(50);
134 assert_eq!(progress.work_done, 50);
135 }
136
137 #[test]
138 fn estimate_eta_test() {
139 let mut progress = Progress::new("test progress", 100);
140 progress.set_work_done(50);
141 thread::sleep(Duration::from_secs(1));
142 let eta = progress.estimate_time_left();
143 assert_eq!(eta, Duration::from_secs(1));
144 }
145
146 #[test]
147 fn get_progress_string_test() {
148 let mut progress = Progress::new("test progress", 100);
149 progress.set_work_done(50);
150
151 let progress_string = progress.get_progress_string();
154
155 assert!(progress_string.starts_with("test progress 50/100 - 50.0% started"));
156 assert!(progress_string.ends_with("ago, eta: 0s"));
157 }
158
159 #[test]
160 fn work_complete_string_test() {
161 let mut progress = Progress::new("test progress", 100);
162 progress.set_work_done(100);
163
164 let progress_string = progress.get_progress_string();
167
168 assert!(progress_string.starts_with("test progress 100/100 - 100.0% started"));
169 assert!(progress_string.ends_with("ago, eta: done!"));
170 }
171
172 #[test]
173 fn guard_against_work_done_overflow_test() {
174 let mut progress = Progress::new("test progress", 2);
175 progress.inc_work_done();
176 progress.inc_work_done();
177 progress.inc_work_done();
178 progress.estimate_time_left();
179 }
180}