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> {
564 progress: Option<&'a Arc<SharedProgress>>,
565 verified: u64,
566 completed: bool,
567}
568
569impl<'a> ArchiveVerificationProgress<'a> {
570 pub(crate) fn new(progress: Option<&'a Arc<SharedProgress>>) -> Self {
572 if let Some(progress) = progress {
573 progress.verification_started();
574 }
575 Self { progress, verified: 0, completed: false }
576 }
577
578 pub(crate) fn record_verified(&mut self, bytes: u64) {
580 self.verified += bytes;
581 if let Some(progress) = self.progress {
582 progress.add_active_verified_output_bytes(bytes);
583 }
584 }
585
586 pub(crate) fn complete(&mut self, total_bytes: u64) {
588 if self.completed {
589 return;
590 }
591 if let Some(progress) = self.progress {
592 progress.sub_active_verified_output_bytes(self.verified);
593 progress.record_archive_output_complete(total_bytes);
594 }
595 self.verified = 0;
596 self.completed = true;
597 }
598}
599
600impl Drop for ArchiveVerificationProgress<'_> {
601 fn drop(&mut self) {
602 if let Some(progress) = self.progress {
603 progress.sub_active_verified_output_bytes(self.verified);
604 progress.verification_finished();
605 }
606 }
607}
608
609pub(crate) struct ProgressReader<R> {
611 reader: R,
613 progress: DownloadProgress,
615 cancel_token: CancellationToken,
617}
618
619impl<R: Read> ProgressReader<R> {
620 pub(crate) fn new(reader: R, total_size: u64, cancel_token: CancellationToken) -> Self {
622 Self { reader, progress: DownloadProgress::new(total_size), cancel_token }
623 }
624}
625
626impl<R: Read> Read for ProgressReader<R> {
627 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
629 if self.cancel_token.is_cancelled() {
630 return Err(io::Error::new(io::ErrorKind::Interrupted, "download cancelled"));
631 }
632 let bytes = self.reader.read(buf)?;
633 if bytes > 0 &&
634 let Err(error) = self.progress.update(bytes as u64)
635 {
636 return Err(io::Error::other(error));
637 }
638 Ok(bytes)
639 }
640}
641
642pub(crate) struct SharedProgressWriter<'a, W> {
645 pub(crate) inner: W,
647 pub(crate) progress: Arc<SharedProgress>,
649 pub(crate) on_written: Option<&'a mut dyn FnMut(u64)>,
651}
652
653impl<W: Write> Write for SharedProgressWriter<'_, W> {
654 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
656 if self.progress.is_cancelled() {
657 return Err(io::Error::new(io::ErrorKind::Interrupted, "download cancelled"));
658 }
659 let n = self.inner.write(buf)?;
660 self.progress.record_session_fetched_bytes(n as u64);
661 if let Some(on_written) = self.on_written.as_deref_mut() {
662 on_written(n as u64);
663 }
664 Ok(n)
665 }
666
667 fn flush(&mut self) -> io::Result<()> {
669 self.inner.flush()
670 }
671}
672
673pub(crate) struct SharedProgressReader<R> {
676 pub(crate) inner: R,
678 pub(crate) progress: Arc<SharedProgress>,
680}
681
682impl<R: Read> Read for SharedProgressReader<R> {
683 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
685 if self.progress.is_cancelled() {
686 return Err(io::Error::new(io::ErrorKind::Interrupted, "download cancelled"));
687 }
688 let n = self.inner.read(buf)?;
689 self.progress.record_session_fetched_bytes(n as u64);
690 Ok(n)
691 }
692}
693
694pub(crate) fn spawn_progress_display(progress: Arc<SharedProgress>) -> tokio::task::JoinHandle<()> {
697 tokio::spawn(async move {
698 let mut interval = tokio::time::interval(Duration::from_secs(3));
699 interval.tick().await;
700 loop {
701 tokio::select! {
702 _ = interval.tick() => {}
703 _ = progress.done.notified() => break,
704 }
705
706 let download_total = progress.total_download_bytes;
707 let output_total = progress.total_output_bytes;
708 if download_total == 0 && output_total == 0 {
709 continue;
710 }
711
712 let done = progress.archives_done.load(Ordering::Relaxed);
713 let all = progress.total_archives;
714 let active_downloads = progress.active_downloads.load(Ordering::Relaxed);
715 let active_requests = progress.active_download_requests.load(Ordering::Relaxed);
716 let active_extractions = progress.active_extractions.load(Ordering::Relaxed);
717 let active_verifications = progress.active_verifications.load(Ordering::Relaxed);
718 let downloaded = progress.logical_downloaded_bytes();
719 let extracted = progress.extracting_output_bytes();
720 let verified = progress.verifying_output_bytes();
721 let elapsed = DownloadProgress::format_duration(progress.started_at.elapsed());
722 let download_total_display = DownloadProgress::format_size(download_total);
723 let output_total_display = DownloadProgress::format_size(output_total);
724 let downloaded_display = DownloadProgress::format_size(downloaded);
725 let extracted_display = DownloadProgress::format_size(extracted);
726 let active_download_phase = active_downloads > 0 || active_requests > 0;
727
728 if active_download_phase {
729 info!(target: "reth::cli",
730 archives = format_args!("{done}/{all}"),
731 progress = %format_percent(downloaded, download_total),
732 elapsed = %elapsed,
733 eta = %format_eta(eta_from_progress(progress.started_at.elapsed(), downloaded, download_total)),
734 bytes = format_args!("{downloaded_display}/{download_total_display}"),
735 "Downloading snapshot archives"
736 );
737 } else if active_extractions > 0 {
738 info!(target: "reth::cli",
739 archives = format_args!("{done}/{all}"),
740 progress = %format_percent(extracted, output_total),
741 elapsed = %elapsed,
742 eta = %format_eta(progress.extraction_eta(extracted)),
743 bytes = format_args!("{extracted_display}/{output_total_display}"),
744 "Extracting snapshot archives"
745 );
746 } else if active_verifications > 0 {
747 info!(target: "reth::cli",
748 archives = format_args!("{done}/{all}"),
749 progress = %format_percent(verified, output_total),
750 elapsed = %elapsed,
751 eta = %format_eta(progress.verification_eta(verified)),
752 bytes = format_args!("{}/{output_total_display}", DownloadProgress::format_size(verified)),
753 "Verifying snapshot archives"
754 );
755 } else {
756 continue;
757 }
758 }
759
760 let completed = progress.verified_output_bytes();
761 let completed_display = DownloadProgress::format_size(completed);
762 let output_total = DownloadProgress::format_size(progress.total_output_bytes);
763 info!(target: "reth::cli",
764 archives = format_args!("{}/{}", progress.total_archives, progress.total_archives),
765 progress = "100.0%",
766 elapsed = %DownloadProgress::format_duration(progress.started_at.elapsed()),
767 eta = "0s",
768 bytes = format_args!("{completed_display}/{output_total}"),
769 "Snapshot archive processing complete"
770 );
771 })
772}
773
774#[cfg(test)]
775mod tests {
776 use super::*;
777 use std::sync::atomic::Ordering;
778
779 #[test]
780 fn shared_progress_separates_session_fetch_from_logical_progress() {
781 let progress = SharedProgress::new(10, 20, 1, CancellationToken::new());
782
783 progress.record_session_fetched_bytes(10);
784 progress.record_session_fetched_bytes(10);
785 progress.record_archive_download_complete(10);
786 progress.record_archive_output_complete(20);
787
788 assert_eq!(progress.session_fetched_bytes.load(Ordering::Relaxed), 20);
789 assert_eq!(progress.logical_downloaded_bytes(), 10);
790 assert_eq!(progress.verified_output_bytes(), 20);
791 assert_eq!(progress.archives_done.load(Ordering::Relaxed), 1);
792 }
793
794 #[test]
795 fn archive_download_progress_rolls_back_unfinished_attempts() {
796 let progress = SharedProgress::new(10, 20, 1, CancellationToken::new());
797
798 {
799 let mut download = ArchiveDownloadProgress::new(Some(&progress));
800 download.record_downloaded(4);
801 assert_eq!(progress.logical_downloaded_bytes(), 4);
802 }
803
804 assert_eq!(progress.logical_downloaded_bytes(), 0);
805 assert_eq!(progress.active_downloads.load(Ordering::Relaxed), 0);
806 }
807
808 #[test]
809 fn extraction_phase_baseline_restarts_after_idle() {
810 let progress = SharedProgress::new(10, 100, 1, CancellationToken::new());
811
812 progress.extraction_started();
813 assert_eq!(progress.extraction_phase.lock().unwrap().as_ref().unwrap().baseline_bytes, 0);
814
815 progress.completed_output_bytes.store(25, Ordering::Relaxed);
816 progress.extraction_started();
817 assert_eq!(progress.extraction_phase.lock().unwrap().as_ref().unwrap().baseline_bytes, 0);
818
819 progress.extraction_finished();
820 progress.extraction_finished();
821 progress.extraction_started();
822 assert_eq!(progress.extraction_phase.lock().unwrap().as_ref().unwrap().baseline_bytes, 25);
823 }
824
825 #[test]
826 fn verification_phase_baseline_restarts_after_idle() {
827 let progress = SharedProgress::new(10, 100, 1, CancellationToken::new());
828
829 progress.verification_started();
830 assert_eq!(progress.verification_phase.lock().unwrap().as_ref().unwrap().baseline_bytes, 0);
831
832 progress.completed_output_bytes.store(40, Ordering::Relaxed);
833 progress.verification_started();
834 assert_eq!(progress.verification_phase.lock().unwrap().as_ref().unwrap().baseline_bytes, 0);
835
836 progress.verification_finished();
837 progress.verification_finished();
838 progress.verification_started();
839 assert_eq!(
840 progress.verification_phase.lock().unwrap().as_ref().unwrap().baseline_bytes,
841 40
842 );
843 }
844
845 #[tokio::test]
846 async fn progress_display_exits_when_done() {
847 let progress = SharedProgress::new(10, 20, 1, CancellationToken::new());
848 let handle = spawn_progress_display(Arc::clone(&progress));
849
850 progress.done.notify_one();
851
852 tokio::time::timeout(Duration::from_secs(1), handle).await.unwrap().unwrap();
854 }
855}