1use std::io::{self, Write};
24use std::sync::Mutex;
25
26use indicatif::{ProgressBar, ProgressDrawTarget, ProgressStyle};
27
28pub(crate) trait ProgressReporter: Send + Sync {
34 fn start_phase(&self, name: &str, total: Option<u64>);
36
37 fn inc(&self, delta: u64);
39
40 fn finish_phase(&self);
42}
43
44pub(crate) struct NoopProgress;
47
48impl ProgressReporter for NoopProgress {
49 fn start_phase(&self, _name: &str, _total: Option<u64>) {}
50 fn inc(&self, _delta: u64) {}
51 fn finish_phase(&self) {}
52}
53
54pub(crate) struct LogProgress {
58 phase: Mutex<Option<PhaseState>>,
59}
60
61struct PhaseState {
62 name: String,
63 total: Option<u64>,
64 done: u64,
65}
66
67impl LogProgress {
68 pub(crate) fn new() -> Self {
69 Self {
70 phase: Mutex::new(None),
71 }
72 }
73}
74
75fn write_progress_line(line: String) {
76 let _ = writeln!(io::stderr().lock(), "{line}");
77}
78
79impl ProgressReporter for LogProgress {
80 fn start_phase(&self, name: &str, total: Option<u64>) {
81 let Ok(mut phase) = self.phase.lock() else {
82 return;
83 };
84 *phase = Some(PhaseState {
85 name: name.to_string(),
86 total,
87 done: 0,
88 });
89 match total {
90 Some(total) => write_progress_line(format!("Starting phase '{name}' ({total} units)")),
91 None => write_progress_line(format!("Starting phase '{name}'")),
92 }
93 }
94
95 fn inc(&self, delta: u64) {
96 let Ok(mut guard) = self.phase.lock() else {
97 return;
98 };
99 if let Some(phase) = guard.as_mut() {
100 phase.done += delta;
101 match phase.total {
102 Some(total) => {
103 write_progress_line(format!("Phase '{}': {}/{}", phase.name, phase.done, total))
104 }
105 None => write_progress_line(format!("Phase '{}': {}", phase.name, phase.done)),
106 }
107 }
108 }
109
110 fn finish_phase(&self) {
111 let Ok(mut guard) = self.phase.lock() else {
112 return;
113 };
114 if let Some(phase) = guard.take() {
115 write_progress_line(format!(
116 "Finished phase '{}' ({} units)",
117 phase.name, phase.done
118 ));
119 }
120 }
121}
122
123pub(crate) struct IndicatifProgress {
130 bar: Mutex<Option<ProgressBar>>,
131}
132
133impl IndicatifProgress {
134 pub(crate) fn new() -> Self {
135 Self {
136 bar: Mutex::new(None),
137 }
138 }
139}
140
141impl ProgressReporter for IndicatifProgress {
142 fn start_phase(&self, name: &str, total: Option<u64>) {
143 let Ok(mut guard) = self.bar.lock() else {
144 return;
145 };
146
147 if let Some(prev) = guard.take() {
149 prev.finish_and_clear();
150 }
151
152 let bar = ProgressBar::with_draw_target(total, ProgressDrawTarget::stderr());
153 match total {
154 Some(_) => {
155 let style =
156 ProgressStyle::with_template("{msg} [{bar:40}] {pos}/{len} ({percent}%)")
157 .unwrap_or_else(|_| ProgressStyle::default_bar())
158 .progress_chars("=>-");
159 bar.set_style(style);
160 }
161 None => {
162 let style = ProgressStyle::with_template("{spinner} {msg} {pos}")
163 .unwrap_or_else(|_| ProgressStyle::default_spinner());
164 bar.set_style(style);
165 }
166 }
167 bar.set_message(name.to_string());
168 *guard = Some(bar);
169 }
170
171 fn inc(&self, delta: u64) {
172 let Ok(guard) = self.bar.lock() else {
173 return;
174 };
175 if let Some(bar) = guard.as_ref() {
176 bar.inc(delta);
177 }
178 }
179
180 fn finish_phase(&self) {
181 let Ok(mut guard) = self.bar.lock() else {
182 return;
183 };
184 if let Some(bar) = guard.take() {
185 bar.finish_and_clear();
186 }
187 }
188}
189
190#[must_use = "dropping the guard immediately finishes the phase"]
196pub(crate) struct ProgressPhase<'a> {
197 reporter: &'a dyn ProgressReporter,
198 finished: bool,
199}
200
201impl<'a> ProgressPhase<'a> {
202 pub(crate) fn start(
203 reporter: &'a dyn ProgressReporter,
204 name: &str,
205 total: Option<u64>,
206 ) -> Self {
207 reporter.start_phase(name, total);
208 Self {
209 reporter,
210 finished: false,
211 }
212 }
213
214 pub(crate) fn finish(mut self) {
215 self.finish_once();
216 }
217
218 fn finish_once(&mut self) {
219 if !self.finished {
220 self.reporter.finish_phase();
221 self.finished = true;
222 }
223 }
224}
225
226impl Drop for ProgressPhase<'_> {
227 fn drop(&mut self) {
228 self.finish_once();
229 }
230}
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235
236 #[test]
237 fn test_log_progress_is_safe_across_phase_lifecycle() {
238 let progress = LogProgress::new();
241 let reporter: &dyn ProgressReporter = &progress;
242
243 reporter.inc(1); reporter.start_phase("Import data tasks", Some(2));
245 reporter.inc(1);
246 reporter.inc(1);
247 reporter.finish_phase();
248 reporter.finish_phase(); }
250
251 #[test]
252 fn test_indicatif_progress_is_safe_across_phase_lifecycle() {
253 let progress = IndicatifProgress::new();
259 let reporter: &dyn ProgressReporter = &progress;
260
261 reporter.inc(1); reporter.start_phase("Import data tasks", Some(2));
263 reporter.inc(1);
264 reporter.inc(1);
265 reporter.start_phase("Streaming", None); reporter.inc(5);
267 reporter.finish_phase();
268 reporter.finish_phase(); }
270}
271
272#[cfg(test)]
273pub(crate) mod test_util {
274 use std::sync::Mutex;
275
276 use super::ProgressReporter;
277
278 #[derive(Debug, Clone, PartialEq, Eq)]
281 pub(crate) enum ProgressEvent {
282 StartPhase { name: String, total: Option<u64> },
283 Inc { delta: u64 },
284 FinishPhase,
285 }
286
287 #[derive(Default)]
289 pub(crate) struct RecordingProgress {
290 events: Mutex<Vec<ProgressEvent>>,
291 }
292
293 impl RecordingProgress {
294 pub(crate) fn events(&self) -> Vec<ProgressEvent> {
295 self.events.lock().unwrap().clone()
296 }
297
298 pub(crate) fn total_inc(&self) -> u64 {
300 self.events
301 .lock()
302 .unwrap()
303 .iter()
304 .filter_map(|event| match event {
305 ProgressEvent::Inc { delta } => Some(*delta),
306 _ => None,
307 })
308 .sum()
309 }
310
311 fn push(&self, event: ProgressEvent) {
312 self.events.lock().unwrap().push(event);
313 }
314 }
315
316 impl ProgressReporter for RecordingProgress {
317 fn start_phase(&self, name: &str, total: Option<u64>) {
318 self.push(ProgressEvent::StartPhase {
319 name: name.to_string(),
320 total,
321 });
322 }
323
324 fn inc(&self, delta: u64) {
325 self.push(ProgressEvent::Inc { delta });
326 }
327
328 fn finish_phase(&self) {
329 self.push(ProgressEvent::FinishPhase);
330 }
331 }
332}