1use eyre::Result;
2use reth_cli_util::cancellation::CancellationToken;
3use std::{
4 io::{self, Read, Write},
5 sync::{
6 atomic::{AtomicU64, Ordering},
7 Arc, Condvar, Mutex,
8 },
9 time::{Duration, Instant},
10};
11use tokio::sync::Notify;
12use tracing::info;
13
14const BYTE_UNITS: [&str; 4] = ["B", "KB", "MB", "GB"];
15
16pub(crate) struct DownloadProgress {
18 pub(crate) downloaded: u64,
20 total_size: u64,
22 last_displayed: Instant,
24 started_at: Instant,
26}
27
28impl DownloadProgress {
29 pub(crate) fn new(total_size: u64) -> Self {
31 let now = Instant::now();
32 Self { downloaded: 0, total_size, last_displayed: now, started_at: now }
33 }
34
35 pub(crate) fn format_size(size: u64) -> String {
37 let mut size = size as f64;
38 let mut unit_index = 0;
39
40 while size >= 1024.0 && unit_index < BYTE_UNITS.len() - 1 {
41 size /= 1024.0;
42 unit_index += 1;
43 }
44
45 format!("{:.2} {}", size, BYTE_UNITS[unit_index])
46 }
47
48 pub(crate) fn format_duration(duration: Duration) -> String {
50 let secs = duration.as_secs();
51 if secs < 60 {
52 format!("{secs}s")
53 } else if secs < 3600 {
54 format!("{}m {}s", secs / 60, secs % 60)
55 } else {
56 format!("{}h {}m", secs / 3600, (secs % 3600) / 60)
57 }
58 }
59
60 pub(crate) fn update(&mut self, chunk_size: u64) -> Result<()> {
62 self.downloaded += chunk_size;
63
64 if self.last_displayed.elapsed() >= Duration::from_millis(100) {
65 let formatted_downloaded = Self::format_size(self.downloaded);
66 let formatted_total = Self::format_size(self.total_size);
67 let progress = (self.downloaded as f64 / self.total_size as f64) * 100.0;
68
69 let elapsed = self.started_at.elapsed();
70 let eta = if self.downloaded > 0 {
71 let remaining = self.total_size.saturating_sub(self.downloaded);
72 let speed = self.downloaded as f64 / elapsed.as_secs_f64();
73 if speed > 0.0 {
74 Duration::from_secs_f64(remaining as f64 / speed)
75 } else {
76 Duration::ZERO
77 }
78 } else {
79 Duration::ZERO
80 };
81 let eta_str = Self::format_duration(eta);
82
83 print!(
84 "\rDownloading and extracting... {progress:.2}% ({formatted_downloaded} / {formatted_total}) ETA: {eta_str} ",
85 );
86 io::stdout().flush()?;
87 self.last_displayed = Instant::now();
88 }
89
90 Ok(())
91 }
92}
93
94#[derive(Debug, Clone, Copy)]
95struct PhaseStart {
96 started_at: Instant,
97 baseline_bytes: u64,
98}
99
100pub(crate) struct SharedProgress {
102 pub(crate) session_fetched_bytes: AtomicU64,
104 pub(crate) completed_download_bytes: AtomicU64,
106 pub(crate) active_download_bytes: AtomicU64,
108 pub(crate) total_download_bytes: u64,
110 pub(crate) completed_output_bytes: AtomicU64,
112 pub(crate) active_extracted_output_bytes: AtomicU64,
114 pub(crate) active_verified_output_bytes: AtomicU64,
116 pub(crate) total_output_bytes: u64,
118 pub(crate) total_archives: u64,
120 pub(crate) started_at: Instant,
122 extraction_phase: Mutex<Option<PhaseStart>>,
124 verification_phase: Mutex<Option<PhaseStart>>,
126 pub(crate) archives_done: AtomicU64,
128 pub(crate) active_downloads: AtomicU64,
130 pub(crate) active_download_requests: AtomicU64,
132 pub(crate) active_extractions: AtomicU64,
134 pub(crate) active_verifications: AtomicU64,
136 pub(crate) done: Notify,
138 cancel_token: CancellationToken,
140}
141
142impl SharedProgress {
143 pub(crate) fn new(
145 total_download_bytes: u64,
146 total_output_bytes: u64,
147 total_archives: u64,
148 cancel_token: CancellationToken,
149 ) -> Arc<Self> {
150 Arc::new(Self {
151 session_fetched_bytes: AtomicU64::new(0),
152 completed_download_bytes: AtomicU64::new(0),
153 active_download_bytes: AtomicU64::new(0),
154 total_download_bytes,
155 completed_output_bytes: AtomicU64::new(0),
156 active_extracted_output_bytes: AtomicU64::new(0),
157 active_verified_output_bytes: AtomicU64::new(0),
158 total_output_bytes,
159 total_archives,
160 started_at: Instant::now(),
161 extraction_phase: Mutex::new(None),
162 verification_phase: Mutex::new(None),
163 archives_done: AtomicU64::new(0),
164 active_downloads: AtomicU64::new(0),
165 active_download_requests: AtomicU64::new(0),
166 active_extractions: AtomicU64::new(0),
167 active_verifications: AtomicU64::new(0),
168 done: Notify::new(),
169 cancel_token,
170 })
171 }
172
173 pub(crate) fn is_cancelled(&self) -> bool {
175 self.cancel_token.is_cancelled()
176 }
177
178 pub(crate) fn record_session_fetched_bytes(&self, bytes: u64) {
180 self.session_fetched_bytes.fetch_add(bytes, Ordering::Relaxed);
181 }
182
183 pub(crate) fn add_active_download_bytes(&self, bytes: u64) {
184 self.active_download_bytes.fetch_add(bytes, Ordering::Relaxed);
185 }
186
187 pub(crate) fn sub_active_download_bytes(&self, bytes: u64) {
188 sub_bytes(&self.active_download_bytes, bytes);
189 }
190
191 fn add_active_extracted_output_bytes(&self, bytes: u64) {
192 self.active_extracted_output_bytes.fetch_add(bytes, Ordering::Relaxed);
193 }
194
195 fn sub_active_extracted_output_bytes(&self, bytes: u64) {
196 sub_bytes(&self.active_extracted_output_bytes, bytes);
197 }
198
199 fn add_active_verified_output_bytes(&self, bytes: u64) {
200 self.active_verified_output_bytes.fetch_add(bytes, Ordering::Relaxed);
201 }
202
203 fn sub_active_verified_output_bytes(&self, bytes: u64) {
204 sub_bytes(&self.active_verified_output_bytes, bytes);
205 }
206
207 pub(crate) fn record_reused_archive(&self, download_bytes: u64, output_bytes: u64) {
209 self.completed_download_bytes.fetch_add(download_bytes, Ordering::Relaxed);
210 self.completed_output_bytes.fetch_add(output_bytes, Ordering::Relaxed);
211 self.archives_done.fetch_add(1, Ordering::Relaxed);
212 }
213
214 pub(crate) fn record_archive_download_complete(&self, bytes: u64) {
216 self.completed_download_bytes.fetch_add(bytes, Ordering::Relaxed);
217 }
218
219 pub(crate) fn record_archive_output_complete(&self, bytes: u64) {
221 self.completed_output_bytes.fetch_add(bytes, Ordering::Relaxed);
222 self.archives_done.fetch_add(1, Ordering::Relaxed);
223 }
224
225 pub(crate) fn logical_downloaded_bytes(&self) -> u64 {
227 (self.completed_download_bytes.load(Ordering::Relaxed) +
228 self.active_download_bytes.load(Ordering::Relaxed))
229 .min(self.total_download_bytes)
230 }
231
232 pub(crate) fn verified_output_bytes(&self) -> u64 {
234 self.completed_output_bytes.load(Ordering::Relaxed).min(self.total_output_bytes)
235 }
236
237 pub(crate) fn extracting_output_bytes(&self) -> u64 {
239 (self.completed_output_bytes.load(Ordering::Relaxed) +
240 self.active_extracted_output_bytes.load(Ordering::Relaxed))
241 .min(self.total_output_bytes)
242 }
243
244 pub(crate) fn verifying_output_bytes(&self) -> u64 {
246 (self.completed_output_bytes.load(Ordering::Relaxed) +
247 self.active_verified_output_bytes.load(Ordering::Relaxed))
248 .min(self.total_output_bytes)
249 }
250
251 fn restart_phase(slot: &Mutex<Option<PhaseStart>>, baseline_bytes: u64) {
252 *slot.lock().unwrap() = Some(PhaseStart { started_at: Instant::now(), baseline_bytes });
253 }
254
255 fn phase_eta(
256 slot: &Mutex<Option<PhaseStart>>,
257 current_bytes: u64,
258 total_bytes: u64,
259 ) -> Option<Duration> {
260 let phase = *slot.lock().unwrap();
261 let phase = phase?;
262 let done = current_bytes.saturating_sub(phase.baseline_bytes);
263 let total = total_bytes.saturating_sub(phase.baseline_bytes);
264 eta_from_progress(phase.started_at.elapsed(), done, total)
265 }
266
267 fn extraction_eta(&self, current_bytes: u64) -> Option<Duration> {
268 Self::phase_eta(&self.extraction_phase, current_bytes, self.total_output_bytes)
269 }
270
271 fn verification_eta(&self, current_bytes: u64) -> Option<Duration> {
272 Self::phase_eta(&self.verification_phase, current_bytes, self.total_output_bytes)
273 }
274
275 pub(crate) fn download_started(&self) {
277 self.active_downloads.fetch_add(1, Ordering::Relaxed);
278 }
279
280 pub(crate) fn download_finished(&self) {
282 sub_bytes(&self.active_downloads, 1);
283 }
284
285 pub(crate) fn request_started(&self) {
287 self.active_download_requests.fetch_add(1, Ordering::Relaxed);
288 }
289
290 pub(crate) fn request_finished(&self) {
292 sub_bytes(&self.active_download_requests, 1);
293 }
294
295 pub(crate) fn extraction_started(&self) {
297 if self.active_extractions.fetch_add(1, Ordering::Relaxed) == 0 {
298 Self::restart_phase(
299 &self.extraction_phase,
300 self.completed_output_bytes.load(Ordering::Relaxed),
301 );
302 }
303 }
304
305 pub(crate) fn extraction_finished(&self) {
307 sub_bytes(&self.active_extractions, 1);
308 }
309
310 pub(crate) fn verification_started(&self) {
312 if self.active_verifications.fetch_add(1, Ordering::Relaxed) == 0 {
313 Self::restart_phase(
314 &self.verification_phase,
315 self.completed_output_bytes.load(Ordering::Relaxed),
316 );
317 }
318 }
319
320 pub(crate) fn verification_finished(&self) {
322 sub_bytes(&self.active_verifications, 1);
323 }
324}
325
326fn sub_bytes(counter: &AtomicU64, bytes: u64) {
327 let _ = counter.try_update(Ordering::Relaxed, Ordering::Relaxed, |current| {
328 Some(current.saturating_sub(bytes))
329 });
330}
331
332fn eta_from_progress(elapsed: Duration, done: u64, total: u64) -> Option<Duration> {
333 if done == 0 || done >= total {
334 return None;
335 }
336
337 let secs = elapsed.as_secs_f64();
338 if secs <= 0.0 {
339 return None;
340 }
341
342 let speed = done as f64 / secs;
343 if speed <= 0.0 {
344 return None;
345 }
346
347 Some(Duration::from_secs_f64((total - done) as f64 / speed))
348}
349
350fn format_percent(done: u64, total: u64) -> String {
351 if total == 0 {
352 return "100.0%".to_string();
353 }
354
355 format!("{:.1}%", (done as f64 / total as f64) * 100.0)
356}
357
358fn format_eta(eta: Option<Duration>) -> String {
359 eta.map(DownloadProgress::format_duration).unwrap_or_else(|| "unknown".to_string())
360}
361
362pub(crate) struct DownloadRequestLimiter {
367 limit: usize,
369 active: Mutex<usize>,
371 notify: Condvar,
373}
374
375impl DownloadRequestLimiter {
376 pub(crate) fn new(limit: usize) -> Arc<Self> {
378 Arc::new(Self { limit: limit.max(1), active: Mutex::new(0), notify: Condvar::new() })
379 }
380
381 pub(crate) fn max_concurrency(&self) -> usize {
383 self.limit
384 }
385
386 pub(crate) fn acquire<'a>(
387 &'a self,
388 progress: Option<&'a Arc<SharedProgress>>,
389 cancel_token: &CancellationToken,
390 ) -> Result<DownloadRequestPermit<'a>> {
391 let mut active = self.active.lock().unwrap();
392 loop {
393 if cancel_token.is_cancelled() {
394 return Err(eyre::eyre!("Download cancelled"));
395 }
396
397 if *active < self.limit {
398 *active += 1;
399 if let Some(progress) = progress {
400 progress.request_started();
401 }
402 return Ok(DownloadRequestPermit { limiter: self, progress });
403 }
404
405 let (next_active, _) =
408 self.notify.wait_timeout(active, Duration::from_millis(100)).unwrap();
409 active = next_active;
410 }
411 }
412}
413
414pub(crate) struct DownloadRequestPermit<'a> {
419 limiter: &'a DownloadRequestLimiter,
421 progress: Option<&'a Arc<SharedProgress>>,
423}
424
425impl Drop for DownloadRequestPermit<'_> {
426 fn drop(&mut self) {
428 let mut active = self.limiter.active.lock().unwrap();
429 *active = active.saturating_sub(1);
430 drop(active);
431 self.limiter.notify.notify_one();
432
433 if let Some(progress) = self.progress {
434 progress.request_finished();
435 }
436 }
437}
438
439pub(crate) struct ArchiveDownloadProgress<'a> {
441 progress: Option<&'a Arc<SharedProgress>>,
442 downloaded: u64,
443 completed: bool,
444}
445
446impl<'a> ArchiveDownloadProgress<'a> {
447 pub(crate) fn new(progress: Option<&'a Arc<SharedProgress>>) -> Self {
449 if let Some(progress) = progress {
450 progress.download_started();
451 }
452 Self { progress, downloaded: 0, completed: false }
453 }
454
455 pub(crate) fn record_downloaded(&mut self, bytes: u64) {
457 self.downloaded += bytes;
458 if let Some(progress) = self.progress {
459 progress.add_active_download_bytes(bytes);
460 }
461 }
462
463 pub(crate) fn has_tracked_bytes(&self) -> bool {
465 self.downloaded > 0
466 }
467
468 pub(crate) fn complete(&mut self, total_bytes: u64) {
470 if self.completed {
471 return;
472 }
473 if let Some(progress) = self.progress {
474 progress.sub_active_download_bytes(self.downloaded);
475 progress.record_archive_download_complete(total_bytes);
476 }
477 self.downloaded = 0;
478 self.completed = true;
479 }
480}
481
482impl Drop for ArchiveDownloadProgress<'_> {
483 fn drop(&mut self) {
484 if let Some(progress) = self.progress {
485 progress.sub_active_download_bytes(self.downloaded);
486 progress.download_finished();
487 }
488 }
489}
490
491pub(crate) struct ArchiveExtractionProgress {
493 progress: Option<Arc<SharedProgress>>,
494 extracted: Arc<AtomicU64>,
495 finished: bool,
496}
497
498#[derive(Clone)]
500pub(crate) struct ArchiveExtractionProgressHandle {
501 progress: Arc<SharedProgress>,
502 extracted: Arc<AtomicU64>,
503}
504
505impl ArchiveExtractionProgress {
506 pub(crate) fn new(progress: Option<&Arc<SharedProgress>>) -> Self {
508 if let Some(progress) = progress {
509 progress.extraction_started();
510 }
511 Self {
512 progress: progress.cloned(),
513 extracted: Arc::new(AtomicU64::new(0)),
514 finished: false,
515 }
516 }
517
518 pub(crate) fn handle(&self) -> Option<ArchiveExtractionProgressHandle> {
520 Some(ArchiveExtractionProgressHandle {
521 progress: Arc::clone(self.progress.as_ref()?),
522 extracted: Arc::clone(&self.extracted),
523 })
524 }
525
526 pub(crate) fn record_extracted(&mut self, bytes: u64) {
528 if let Some(handle) = self.handle() {
529 handle.record_extracted(bytes);
530 }
531 }
532
533 pub(crate) fn finish(&mut self) {
535 if self.finished {
536 return;
537 }
538 if let Some(progress) = &self.progress {
539 progress.sub_active_extracted_output_bytes(self.extracted.swap(0, Ordering::Relaxed));
540 }
541 self.finished = true;
542 }
543}
544
545impl Drop for ArchiveExtractionProgress {
546 fn drop(&mut self) {
547 if let Some(progress) = &self.progress {
548 progress.sub_active_extracted_output_bytes(self.extracted.swap(0, Ordering::Relaxed));
549 progress.extraction_finished();
550 }
551 }
552}
553
554impl ArchiveExtractionProgressHandle {
555 pub(crate) fn record_extracted(&self, bytes: u64) {
557 self.extracted.fetch_add(bytes, Ordering::Relaxed);
558 self.progress.add_active_extracted_output_bytes(bytes);
559 }
560}
561
562pub(crate) struct ArchiveVerificationProgress<'a> {
566 progress: Option<&'a Arc<SharedProgress>>,
567 verified: AtomicU64,
568 completed: bool,
569}
570
571impl<'a> ArchiveVerificationProgress<'a> {
572 pub(crate) fn new(progress: Option<&'a Arc<SharedProgress>>) -> Self {
574 if let Some(progress) = progress {
575 progress.verification_started();
576 }
577 Self { progress, verified: AtomicU64::new(0), completed: false }
578 }
579
580 pub(crate) fn record_verified(&self, bytes: u64) {
582 self.verified.fetch_add(bytes, Ordering::Relaxed);
583 if let Some(progress) = self.progress {
584 progress.add_active_verified_output_bytes(bytes);
585 }
586 }
587
588 pub(crate) fn complete(&mut self, total_bytes: u64) {
590 if self.completed {
591 return;
592 }
593 let verified = std::mem::take(self.verified.get_mut());
594 if let Some(progress) = self.progress {
595 progress.sub_active_verified_output_bytes(verified);
596 progress.record_archive_output_complete(total_bytes);
597 }
598 self.completed = true;
599 }
600}
601
602impl Drop for ArchiveVerificationProgress<'_> {
603 fn drop(&mut self) {
604 if let Some(progress) = self.progress {
605 progress.sub_active_verified_output_bytes(*self.verified.get_mut());
606 progress.verification_finished();
607 }
608 }
609}
610
611pub(crate) struct ProgressReader<R> {
613 reader: R,
615 progress: DownloadProgress,
617 cancel_token: CancellationToken,
619}
620
621impl<R: Read> ProgressReader<R> {
622 pub(crate) fn new(reader: R, total_size: u64, cancel_token: CancellationToken) -> Self {
624 Self { reader, progress: DownloadProgress::new(total_size), cancel_token }
625 }
626}
627
628impl<R: Read> Read for ProgressReader<R> {
629 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
631 if self.cancel_token.is_cancelled() {
632 return Err(io::Error::new(io::ErrorKind::Interrupted, "download cancelled"));
633 }
634 let bytes = self.reader.read(buf)?;
635 if bytes > 0 &&
636 let Err(error) = self.progress.update(bytes as u64)
637 {
638 return Err(io::Error::other(error));
639 }
640 Ok(bytes)
641 }
642}
643
644pub(crate) struct SharedProgressWriter<'a, W> {
647 pub(crate) inner: W,
649 pub(crate) progress: Arc<SharedProgress>,
651 pub(crate) on_written: Option<&'a mut dyn FnMut(u64)>,
653}
654
655impl<W: Write> Write for SharedProgressWriter<'_, W> {
656 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
658 if self.progress.is_cancelled() {
659 return Err(io::Error::new(io::ErrorKind::Interrupted, "download cancelled"));
660 }
661 let n = self.inner.write(buf)?;
662 self.progress.record_session_fetched_bytes(n as u64);
663 if let Some(on_written) = self.on_written.as_deref_mut() {
664 on_written(n as u64);
665 }
666 Ok(n)
667 }
668
669 fn flush(&mut self) -> io::Result<()> {
671 self.inner.flush()
672 }
673}
674
675pub(crate) struct SharedProgressReader<R> {
678 pub(crate) inner: R,
680 pub(crate) progress: Arc<SharedProgress>,
682}
683
684impl<R: Read> Read for SharedProgressReader<R> {
685 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
687 if self.progress.is_cancelled() {
688 return Err(io::Error::new(io::ErrorKind::Interrupted, "download cancelled"));
689 }
690 let n = self.inner.read(buf)?;
691 self.progress.record_session_fetched_bytes(n as u64);
692 Ok(n)
693 }
694}
695
696pub(crate) fn spawn_progress_display(progress: Arc<SharedProgress>) -> tokio::task::JoinHandle<()> {
699 tokio::spawn(async move {
700 let mut interval = tokio::time::interval(Duration::from_secs(3));
701 interval.tick().await;
702 loop {
703 tokio::select! {
704 _ = interval.tick() => {}
705 _ = progress.done.notified() => break,
706 }
707
708 let download_total = progress.total_download_bytes;
709 let output_total = progress.total_output_bytes;
710 if download_total == 0 && output_total == 0 {
711 continue;
712 }
713
714 let done = progress.archives_done.load(Ordering::Relaxed);
715 let all = progress.total_archives;
716 let active_downloads = progress.active_downloads.load(Ordering::Relaxed);
717 let active_requests = progress.active_download_requests.load(Ordering::Relaxed);
718 let active_extractions = progress.active_extractions.load(Ordering::Relaxed);
719 let active_verifications = progress.active_verifications.load(Ordering::Relaxed);
720 let downloaded = progress.logical_downloaded_bytes();
721 let extracted = progress.extracting_output_bytes();
722 let verified = progress.verifying_output_bytes();
723 let elapsed = DownloadProgress::format_duration(progress.started_at.elapsed());
724 let download_total_display = DownloadProgress::format_size(download_total);
725 let output_total_display = DownloadProgress::format_size(output_total);
726 let downloaded_display = DownloadProgress::format_size(downloaded);
727 let extracted_display = DownloadProgress::format_size(extracted);
728 let active_download_phase = active_downloads > 0 || active_requests > 0;
729
730 if active_download_phase {
731 info!(target: "reth::cli",
732 archives = format_args!("{done}/{all}"),
733 progress = %format_percent(downloaded, download_total),
734 elapsed = %elapsed,
735 eta = %format_eta(eta_from_progress(progress.started_at.elapsed(), downloaded, download_total)),
736 bytes = format_args!("{downloaded_display}/{download_total_display}"),
737 "Downloading snapshot archives"
738 );
739 } else if active_extractions > 0 {
740 info!(target: "reth::cli",
741 archives = format_args!("{done}/{all}"),
742 progress = %format_percent(extracted, output_total),
743 elapsed = %elapsed,
744 eta = %format_eta(progress.extraction_eta(extracted)),
745 bytes = format_args!("{extracted_display}/{output_total_display}"),
746 "Extracting snapshot archives"
747 );
748 } else if active_verifications > 0 {
749 info!(target: "reth::cli",
750 archives = format_args!("{done}/{all}"),
751 progress = %format_percent(verified, output_total),
752 elapsed = %elapsed,
753 eta = %format_eta(progress.verification_eta(verified)),
754 bytes = format_args!("{}/{output_total_display}", DownloadProgress::format_size(verified)),
755 "Verifying snapshot archives"
756 );
757 } else {
758 continue;
759 }
760 }
761
762 let completed = progress.verified_output_bytes();
763 let completed_display = DownloadProgress::format_size(completed);
764 let output_total = DownloadProgress::format_size(progress.total_output_bytes);
765 info!(target: "reth::cli",
766 archives = format_args!("{}/{}", progress.total_archives, progress.total_archives),
767 progress = "100.0%",
768 elapsed = %DownloadProgress::format_duration(progress.started_at.elapsed()),
769 eta = "0s",
770 bytes = format_args!("{completed_display}/{output_total}"),
771 "Snapshot archive processing complete"
772 );
773 })
774}
775
776#[cfg(test)]
777mod tests {
778 use super::*;
779 use std::sync::atomic::Ordering;
780
781 #[test]
782 fn shared_progress_separates_session_fetch_from_logical_progress() {
783 let progress = SharedProgress::new(10, 20, 1, CancellationToken::new());
784
785 progress.record_session_fetched_bytes(10);
786 progress.record_session_fetched_bytes(10);
787 progress.record_archive_download_complete(10);
788 progress.record_archive_output_complete(20);
789
790 assert_eq!(progress.session_fetched_bytes.load(Ordering::Relaxed), 20);
791 assert_eq!(progress.logical_downloaded_bytes(), 10);
792 assert_eq!(progress.verified_output_bytes(), 20);
793 assert_eq!(progress.archives_done.load(Ordering::Relaxed), 1);
794 }
795
796 #[test]
797 fn archive_download_progress_rolls_back_unfinished_attempts() {
798 let progress = SharedProgress::new(10, 20, 1, CancellationToken::new());
799
800 {
801 let mut download = ArchiveDownloadProgress::new(Some(&progress));
802 download.record_downloaded(4);
803 assert_eq!(progress.logical_downloaded_bytes(), 4);
804 }
805
806 assert_eq!(progress.logical_downloaded_bytes(), 0);
807 assert_eq!(progress.active_downloads.load(Ordering::Relaxed), 0);
808 }
809
810 #[test]
811 fn extraction_phase_baseline_restarts_after_idle() {
812 let progress = SharedProgress::new(10, 100, 1, CancellationToken::new());
813
814 progress.extraction_started();
815 assert_eq!(progress.extraction_phase.lock().unwrap().as_ref().unwrap().baseline_bytes, 0);
816
817 progress.completed_output_bytes.store(25, Ordering::Relaxed);
818 progress.extraction_started();
819 assert_eq!(progress.extraction_phase.lock().unwrap().as_ref().unwrap().baseline_bytes, 0);
820
821 progress.extraction_finished();
822 progress.extraction_finished();
823 progress.extraction_started();
824 assert_eq!(progress.extraction_phase.lock().unwrap().as_ref().unwrap().baseline_bytes, 25);
825 }
826
827 #[test]
828 fn verification_phase_baseline_restarts_after_idle() {
829 let progress = SharedProgress::new(10, 100, 1, CancellationToken::new());
830
831 progress.verification_started();
832 assert_eq!(progress.verification_phase.lock().unwrap().as_ref().unwrap().baseline_bytes, 0);
833
834 progress.completed_output_bytes.store(40, Ordering::Relaxed);
835 progress.verification_started();
836 assert_eq!(progress.verification_phase.lock().unwrap().as_ref().unwrap().baseline_bytes, 0);
837
838 progress.verification_finished();
839 progress.verification_finished();
840 progress.verification_started();
841 assert_eq!(
842 progress.verification_phase.lock().unwrap().as_ref().unwrap().baseline_bytes,
843 40
844 );
845 }
846
847 #[tokio::test]
848 async fn progress_display_exits_when_done() {
849 let progress = SharedProgress::new(10, 20, 1, CancellationToken::new());
850 let handle = spawn_progress_display(Arc::clone(&progress));
851
852 progress.done.notify_one();
853
854 tokio::time::timeout(Duration::from_secs(1), handle).await.unwrap().unwrap();
856 }
857}