Skip to main content

rust_apt/
progress.rs

1//! Contains Progress struct for updating the package list.
2use std::fmt::Write as _;
3use std::io::{Write, stdout};
4use std::os::fd::RawFd;
5use std::pin::Pin;
6
7use cxx::{ExternType, UniquePtr};
8#[doc(inline)]
9pub use raw::{ReleaseInfoChange, ReleaseInfoChanges};
10
11use crate::config::Config;
12use crate::error::raw::pending_error;
13use crate::raw::{AcqTextStatus, ItemDesc, ItemState, PkgAcquire, acquire_status};
14use crate::util::{
15	NumSys, get_apt_progress_string, terminal_height, terminal_width, time_str, unit_str,
16};
17
18fn progress_fraction(current: u64, total: u64) -> f64 {
19	if total == 0 {
20		return 0.0;
21	}
22
23	(current as f64 / total as f64).clamp(0.0, 1.0)
24}
25
26fn terminal_content_width() -> usize { terminal_width().saturating_sub(1) }
27
28/// Customize the output shown during file downloads.
29pub trait DynAcquireProgress {
30	/// Called on c++ to set the pulse interval.
31	fn pulse_interval(&self) -> usize;
32
33	/// Called when a repository changes information from its Release file.
34	///
35	/// Returning [`true`] accepts every change. The default accepts changes
36	/// that APT considers informational or that were allowed through APT
37	/// configuration, and rejects changes that require explicit confirmation.
38	fn release_info_changes(&mut self, info: ReleaseInfoChanges) -> bool {
39		info.changes.iter().all(|change| change.default_action)
40	}
41
42	/// Called when an item is confirmed to be up-to-date.
43	fn hit(&mut self, item: &ItemDesc);
44
45	/// Called when an Item has started to download
46	fn fetch(&mut self, item: &ItemDesc);
47
48	/// Called when an Item fails to download
49	fn fail(&mut self, item: &ItemDesc);
50
51	/// Called periodically to provide the overall progress information
52	fn pulse(&mut self, status: &AcqTextStatus, owner: &PkgAcquire);
53
54	/// Called when an item is successfully and completely fetched.
55	fn done(&mut self, item: &ItemDesc);
56
57	/// Called when progress has started
58	fn start(&mut self);
59
60	/// Called when progress has finished
61	fn stop(&mut self, status: &AcqTextStatus);
62}
63
64/// Customize the output of operation progress on things like opening the cache.
65pub trait DynOperationProgress {
66	fn update(&mut self, operation: String, percent: f32);
67	fn done(&mut self);
68}
69
70/// Customize the output of installation progress.
71pub trait DynInstallProgress {
72	fn status_changed(
73		&mut self,
74		pkgname: String,
75		steps_done: u64,
76		total_steps: u64,
77		action: String,
78	);
79	fn error(&mut self, pkgname: String, steps_done: u64, total_steps: u64, error: String);
80}
81
82/// A struct aligning with `apt`'s AcquireStatus.
83///
84/// This struct takes a struct with impl AcquireProgress
85/// It sets itself as the callback from C++ AcqTextStatus
86/// which will then call the functions on this struct.
87/// This struct will then forward those calls to your struct via
88/// trait methods.
89pub struct AcquireProgress<'a> {
90	status: UniquePtr<AcqTextStatus>,
91	inner: Box<dyn DynAcquireProgress + 'a>,
92}
93
94impl<'a> AcquireProgress<'a> {
95	/// Create a new AcquireProgress Struct from a struct that implements
96	/// AcquireProgress trait.
97	pub fn new(inner: impl DynAcquireProgress + 'a) -> Self {
98		Self {
99			status: unsafe { acquire_status() },
100			inner: Box::new(inner),
101		}
102	}
103
104	/// Create a new AcquireProgress Struct with the default `apt`
105	/// implementation.
106	pub fn apt() -> Self { Self::new(AptAcquireProgress::new()) }
107
108	/// Create a new AcquireProgress Struct that outputs nothing.
109	pub fn quiet() -> Self { Self::new(AptAcquireProgress::disable()) }
110
111	/// Sets AcquireProgress as the AcqTextStatus callback and
112	/// returns a Pinned mutable reference to AcqTextStatus.
113	pub fn mut_status(&mut self) -> Pin<&mut AcqTextStatus> {
114		unsafe {
115			// Create raw mutable pointer to ourself
116			let raw_ptr = &mut *(self as *mut AcquireProgress);
117			// Pin AcqTextStatus in place so it is not moved in memory
118			// Segfault can occur if it is moved
119			let mut status = self.status.pin_mut();
120
121			// Set our raw pointer we created as the callback for C++
122			// AcqTextStatus. AcqTextStatus will then be fed into libapt who
123			// will call its methods providing information. AcqTextStatus
124			// then uses this pointer to send that information back to rust
125			// on this struct. This struct will then send it through the
126			// trait methods on the `inner` object.
127			status.as_mut().set_callback(raw_ptr);
128			status
129		}
130	}
131
132	/// Called on c++ to set the pulse interval.
133	pub(crate) fn pulse_interval(&mut self) -> usize { self.inner.pulse_interval() }
134
135	/// Forward repository information changes to the configured progress
136	/// handler.
137	///
138	/// Returning `true` tells libapt to accept the changes and continue
139	/// updating. Returning `false` rejects them and allows libapt to produce
140	/// its normal error.
141	pub(crate) fn release_info_changes(&mut self, info: ReleaseInfoChanges) -> bool {
142		self.inner.release_info_changes(info)
143	}
144
145	/// Called when an item is confirmed to be up-to-date.
146	pub(crate) fn hit(&mut self, item: &ItemDesc) { self.inner.hit(item) }
147
148	/// Called when an Item has started to download
149	pub(crate) fn fetch(&mut self, item: &ItemDesc) { self.inner.fetch(item) }
150
151	/// Called when an Item fails to download
152	pub(crate) fn fail(&mut self, item: &ItemDesc) { self.inner.fail(item) }
153
154	/// Called periodically to provide the overall progress information
155	pub(crate) fn pulse(&mut self, owner: &PkgAcquire) { self.inner.pulse(&self.status, owner) }
156
157	/// Called when progress has started
158	pub(crate) fn start(&mut self) { self.inner.start() }
159
160	/// Called when an item is successfully and completely fetched.
161	pub(crate) fn done(&mut self, item: &ItemDesc) { self.inner.done(item) }
162
163	/// Called when progress has finished
164	pub(crate) fn stop(&mut self) { self.inner.stop(&self.status) }
165}
166
167impl Default for AcquireProgress<'_> {
168	fn default() -> Self { Self::apt() }
169}
170
171/// Impl for sending AcquireProgress across the barrier.
172unsafe impl ExternType for AcquireProgress<'_> {
173	type Id = cxx::type_id!("AcquireProgress");
174	type Kind = cxx::kind::Trivial;
175}
176
177/// Allows lengthy operations to communicate their progress.
178///
179/// The [`Default`] and only implementation of this is
180/// [`self::OperationProgress::quiet`].
181pub struct OperationProgress<'a> {
182	inner: Box<dyn DynOperationProgress + 'a>,
183}
184
185impl<'a> OperationProgress<'a> {
186	/// Create a new OpProgress Struct from a struct that implements
187	/// AcquireProgress trait.
188	pub fn new(inner: impl DynOperationProgress + 'static) -> Self {
189		Self {
190			inner: Box::new(inner),
191		}
192	}
193
194	/// Returns a OperationProgress that outputs no data
195	///
196	/// Generally I have not found much use for displaying OpProgress
197	pub fn quiet() -> Self { Self::new(NoOpProgress {}) }
198
199	/// Called when an operation has been updated.
200	fn update(&mut self, operation: String, percent: f32) { self.inner.update(operation, percent) }
201
202	/// Called when an operation has finished.
203	fn done(&mut self) { self.inner.done() }
204
205	pub fn pin(&mut self) -> Pin<&mut OperationProgress<'a>> { Pin::new(self) }
206}
207
208impl Default for OperationProgress<'_> {
209	fn default() -> Self { Self::quiet() }
210}
211
212/// Impl for sending AcquireProgress across the barrier.
213unsafe impl ExternType for OperationProgress<'_> {
214	type Id = cxx::type_id!("OperationProgress");
215	type Kind = cxx::kind::Trivial;
216}
217
218/// Enum for displaying Progress of Package Installation.
219///
220/// The [`Default`] implementation mirrors apt's.
221pub enum InstallProgress<'a> {
222	Fancy(InstallProgressFancy<'a>),
223	Fd(RawFd),
224}
225
226impl InstallProgress<'_> {
227	/// Create a new OpProgress Struct from a struct that implements
228	/// AcquireProgress trait.
229	pub fn new(inner: impl DynInstallProgress + 'static) -> Self {
230		Self::Fancy(InstallProgressFancy::new(inner))
231	}
232
233	/// Send dpkg status messages to an File Descriptor.
234	/// This required more work to implement but is the most flexible.
235	pub fn fd(fd: RawFd) -> Self { Self::Fd(fd) }
236
237	/// Returns InstallProgress that mimics apt's fancy progress
238	pub fn apt() -> Self { Self::new(AptInstallProgress::new()) }
239}
240
241impl Default for InstallProgress<'_> {
242	fn default() -> Self { Self::apt() }
243}
244
245/// Struct for displaying Progress of Package Installation.
246///
247/// The [`Default`] implementation mirrors apt's.
248pub struct InstallProgressFancy<'a> {
249	inner: Box<dyn DynInstallProgress + 'a>,
250}
251
252impl<'a> InstallProgressFancy<'a> {
253	/// Create a new OpProgress Struct from a struct that implements
254	/// AcquireProgress trait.
255	pub fn new(inner: impl DynInstallProgress + 'static) -> Self {
256		Self {
257			inner: Box::new(inner),
258		}
259	}
260
261	/// Returns InstallProgress that mimics apt's fancy progress
262	pub fn apt() -> Self { Self::new(AptInstallProgress::new()) }
263
264	fn status_changed(
265		&mut self,
266		pkgname: String,
267		steps_done: u64,
268		total_steps: u64,
269		action: String,
270	) {
271		self.inner
272			.status_changed(pkgname, steps_done, total_steps, action)
273	}
274
275	fn error(&mut self, pkgname: String, steps_done: u64, total_steps: u64, error: String) {
276		self.inner.error(pkgname, steps_done, total_steps, error)
277	}
278
279	pub fn pin(&mut self) -> Pin<&mut InstallProgressFancy<'a>> { Pin::new(self) }
280}
281
282impl Default for InstallProgressFancy<'_> {
283	fn default() -> Self { Self::apt() }
284}
285
286/// Impl for sending InstallProgressFancy across the barrier.
287unsafe impl ExternType for InstallProgressFancy<'_> {
288	type Id = cxx::type_id!("InstallProgressFancy");
289	type Kind = cxx::kind::Trivial;
290}
291
292/// Internal struct to pass into [`crate::Cache::resolve`]. The C++ library for
293/// this wants a progress parameter for this, but it doesn't appear to be doing
294/// anything. Furthermore, [the Python-APT implementation doesn't accept a
295/// parameter for their dependency resolution functionality](https://apt-team.pages.debian.net/python-apt/library/apt_pkg.html#apt_pkg.ProblemResolver.resolve),
296/// so we should be safe to remove it here.
297struct NoOpProgress {}
298
299impl DynOperationProgress for NoOpProgress {
300	fn update(&mut self, _operation: String, _percent: f32) {}
301
302	fn done(&mut self) {}
303}
304
305/// AptAcquireProgress is the default struct for the update method on the cache.
306///
307/// This struct mimics the output of `apt update`.
308#[derive(Default, Debug)]
309pub struct AptAcquireProgress {
310	lastline: usize,
311	pulse_interval: usize,
312	disable: bool,
313	config: Config,
314}
315
316impl AptAcquireProgress {
317	/// Returns a new default progress instance.
318	pub fn new() -> Self { Self::default() }
319
320	/// Returns a disabled progress instance. No output will be shown.
321	pub fn disable() -> Self {
322		AptAcquireProgress {
323			disable: true,
324			..Default::default()
325		}
326	}
327
328	/// Helper function to clear the last line.
329	fn clear_last_line(&mut self, term_width: usize) {
330		if self.disable {
331			return;
332		}
333
334		if self.lastline == 0 {
335			return;
336		}
337
338		if self.lastline > term_width {
339			self.lastline = term_width
340		}
341
342		print!("\r{}", " ".repeat(self.lastline));
343		print!("\r");
344		stdout().flush().unwrap();
345	}
346}
347
348impl DynAcquireProgress for AptAcquireProgress {
349	/// Used to send the pulse interval to the apt progress class.
350	///
351	/// Pulse Interval is in microseconds.
352	///
353	/// Example: 1 second = 1000000 microseconds.
354	///
355	/// Apt default is 500000 microseconds or 0.5 seconds.
356	///
357	/// The higher the number, the less frequent pulse updates will be.
358	///
359	/// Pulse Interval set to 0 assumes the apt defaults.
360	fn pulse_interval(&self) -> usize { self.pulse_interval }
361
362	/// Called when an item is confirmed to be up-to-date.
363	///
364	/// Prints out the short description and the expected size.
365	fn hit(&mut self, item: &ItemDesc) {
366		if self.disable {
367			return;
368		}
369
370		self.clear_last_line(terminal_content_width());
371
372		println!("\rHit:{} {}", item.owner().id(), item.description());
373	}
374
375	/// Called when an Item has started to download
376	///
377	/// Prints out the short description and the expected size.
378	fn fetch(&mut self, item: &ItemDesc) {
379		if self.disable {
380			return;
381		}
382
383		self.clear_last_line(terminal_content_width());
384
385		let mut string = format!("\rGet:{} {}", item.owner().id(), item.description());
386
387		let file_size = item.owner().file_size();
388		if file_size != 0 {
389			string.push_str(&format!(" [{}]", unit_str(file_size, NumSys::Decimal)));
390		}
391
392		println!("{string}");
393	}
394
395	/// Called when an item is successfully and completely fetched.
396	///
397	/// We don't print anything here to remain consistent with apt.
398	fn done(&mut self, _item: &ItemDesc) {
399		// self.clear_last_line(terminal_width() - 1);
400
401		// println!("This is done!");
402	}
403
404	/// Called when progress has started.
405	///
406	/// Start does not pass information into the method.
407	///
408	/// We do not print anything here to remain consistent with apt.
409	/// lastline length is set to 0 to ensure consistency when progress begins.
410	fn start(&mut self) { self.lastline = 0; }
411
412	/// Called when progress has finished.
413	///
414	/// Stop does not pass information into the method.
415	///
416	/// prints out the bytes downloaded and the overall average line speed.
417	fn stop(&mut self, owner: &AcqTextStatus) {
418		if self.disable {
419			return;
420		}
421
422		self.clear_last_line(terminal_content_width());
423
424		if pending_error() {
425			return;
426		}
427
428		if owner.fetched_bytes() != 0 {
429			println!(
430				"Fetched {} in {} ({}/s)",
431				unit_str(owner.fetched_bytes(), NumSys::Decimal),
432				time_str(owner.elapsed_time()),
433				unit_str(owner.current_cps(), NumSys::Decimal)
434			);
435		} else {
436			println!("Nothing to fetch.");
437		}
438	}
439
440	/// Called when an Item fails to download.
441	///
442	/// Print out the ErrorText for the Item.
443	fn fail(&mut self, item: &ItemDesc) {
444		if self.disable {
445			return;
446		}
447
448		self.clear_last_line(terminal_content_width());
449
450		let mut show_error = true;
451		let error_text = item.owner().error_text();
452		let desc = format!("{} {}", item.owner().id(), item.description());
453
454		match item.owner().status() {
455			ItemState::StatIdle | ItemState::StatDone => {
456				println!("\rIgn: {desc}");
457				let key = "Acquire::Progress::Ignore::ShowErrorText";
458				if error_text.is_empty() || self.config.bool(key, false) {
459					show_error = false;
460				}
461			},
462			_ => {
463				println!("\rErr: {desc}");
464			},
465		}
466
467		if show_error {
468			println!("\r{error_text}");
469		}
470	}
471
472	/// Called periodically to provide the overall progress information
473	///
474	/// Draws the current progress.
475	/// Each line has an overall percent meter and a per active item status
476	/// meter along with an overall bandwidth and ETA indicator.
477	fn pulse(&mut self, status: &AcqTextStatus, owner: &PkgAcquire) {
478		if self.disable {
479			return;
480		}
481
482		// Minus 1 for the cursor
483		let term_width = terminal_content_width();
484
485		let mut string = String::new();
486		let mut percent_str = format!("\r{:.0}%", status.percent());
487		let mut eta_str = String::new();
488
489		// Set the ETA string if there is a rate of download
490		let current_cps = status.current_cps();
491		if current_cps != 0 {
492			let _ = write!(
493				eta_str,
494				" {} {}",
495				// Current rate of download
496				unit_str(current_cps, NumSys::Decimal),
497				// ETA String
498				time_str(status.total_bytes().saturating_sub(status.current_bytes()) / current_cps,)
499			);
500		}
501
502		for worker in owner.workers().iter() {
503			let mut work_string = String::new();
504			work_string.push_str(" [");
505
506			let Ok(item) = worker.item() else {
507				if !worker.status().is_empty() {
508					work_string.push_str(&worker.status());
509					work_string.push(']');
510				}
511				continue;
512			};
513
514			let id = item.owner().id();
515			if id != 0 {
516				let _ = write!(work_string, " {id} ");
517			}
518			work_string.push_str(&item.short_desc());
519
520			let sub = item.owner().active_subprocess();
521			if !sub.is_empty() {
522				work_string.push(' ');
523				work_string.push_str(&sub);
524			}
525
526			work_string.push(' ');
527			work_string.push_str(&unit_str(worker.current_size(), NumSys::Decimal));
528
529			if worker.total_size() > 0 && !item.owner().complete() {
530				let _ = write!(
531					work_string,
532					"/{} {}%",
533					unit_str(worker.total_size(), NumSys::Decimal),
534					(progress_fraction(worker.current_size(), worker.total_size()) * 100.0) as u64
535				);
536			}
537
538			work_string.push(']');
539
540			if (string.len() + work_string.len() + percent_str.len() + eta_str.len()) > term_width {
541				break;
542			}
543
544			string.push_str(&work_string);
545		}
546
547		// Display at least something if there is no worker strings
548		if string.is_empty() {
549			string = " [Working]".to_string()
550		}
551
552		// Push the worker strings on the percent string
553		percent_str.push_str(&string);
554
555		// Fill the remaining space in the terminal if eta exists
556		if !eta_str.is_empty() {
557			let fill_size = percent_str.len() + eta_str.len();
558			if fill_size < term_width {
559				percent_str.push_str(&" ".repeat(term_width - fill_size))
560			}
561		}
562
563		// Push the final eta to the end of the filled string
564		percent_str.push_str(&eta_str);
565
566		// Print and flush stdout
567		print!("{percent_str}");
568		stdout().flush().unwrap();
569
570		if self.lastline > percent_str.len() {
571			self.clear_last_line(term_width);
572		}
573
574		self.lastline = percent_str.len();
575	}
576}
577
578/// Default struct to handle the output of a transaction.
579pub struct AptInstallProgress {
580	config: Config,
581}
582
583impl AptInstallProgress {
584	pub fn new() -> Self {
585		Self {
586			config: Config::new(),
587		}
588	}
589}
590
591impl Default for AptInstallProgress {
592	fn default() -> Self { Self::new() }
593}
594
595impl DynInstallProgress for AptInstallProgress {
596	fn status_changed(
597		&mut self,
598		_pkgname: String,
599		steps_done: u64,
600		total_steps: u64,
601		_action: String,
602	) {
603		// Get the terminal's width and height.
604		let term_height = terminal_height();
605		let term_width = terminal_width();
606
607		// Save the current cursor position.
608		print!("\x1b7");
609
610		// Go to the progress reporting line.
611		print!("\x1b[{term_height};0f");
612		std::io::stdout().flush().unwrap();
613
614		// Convert the float to a percentage string.
615		let percent = progress_fraction(steps_done, total_steps) as f32;
616		let mut percent_str = (percent * 100.0).round().to_string();
617
618		let percent_padding = match percent_str.len() {
619			1 => "  ",
620			2 => " ",
621			3 => "",
622			_ => unreachable!(),
623		};
624
625		percent_str = percent_padding.to_owned() + &percent_str;
626
627		// Get colors for progress reporting.
628		// NOTE: The APT implementation confusingly has 'Progress-fg' for
629		// 'bg_color', and the same the other way around.
630		let bg_color = self
631			.config
632			.find("Dpkg::Progress-Fancy::Progress-fg", "\x1b[42m");
633		let fg_color = self
634			.config
635			.find("Dpkg::Progress-Fancy::Progress-bg", "\x1b[30m");
636		const BG_COLOR_RESET: &str = "\x1b[49m";
637		const FG_COLOR_RESET: &str = "\x1b[39m";
638
639		print!("{bg_color}{fg_color}Progress: [{percent_str}%]{BG_COLOR_RESET}{FG_COLOR_RESET} ");
640
641		// The length of "Progress: [100%] ".
642		const PROGRESS_STR_LEN: usize = 17;
643
644		// Print the progress bar.
645		// We should safely be able to convert the `usize`.try_into() into the
646		// `u32` needed by `get_apt_progress_string`, as usize ints only take
647		// up 8 bytes on a 64-bit processor.
648		if let Ok(progress_width) = u32::try_from(term_width.saturating_sub(PROGRESS_STR_LEN)) {
649			if progress_width > 0 {
650				print!("{}", get_apt_progress_string(percent, progress_width));
651			}
652		}
653		std::io::stdout().flush().unwrap();
654
655		// If this is the last change, remove the progress reporting bar.
656		// if steps_done == total_steps {
657		// print!("{}", " ".repeat(term_width));
658		// print!("\x1b[0;{}r", term_height);
659		// }
660		// Finally, go back to the previous cursor position.
661		print!("\x1b8");
662		std::io::stdout().flush().unwrap();
663	}
664
665	// TODO: Need to figure out when to use this.
666	fn error(&mut self, _pkgname: String, _steps_done: u64, _total_steps: u64, _error: String) {}
667}
668
669#[allow(clippy::needless_lifetimes)]
670#[cxx::bridge]
671pub(crate) mod raw {
672	/// Repository metadata associated with a set of Release file changes.
673	#[derive(Debug)]
674	struct ReleaseInfoChanges {
675		/// Repository URI reported by APT.
676		pub uri: String,
677		/// Distribution or suite requested from the repository.
678		pub dist: String,
679		/// Metadata fields that changed.
680		pub changes: Vec<ReleaseInfoChange>,
681	}
682
683	/// A change to repository metadata from its Release file.
684	#[derive(Debug)]
685	struct ReleaseInfoChange {
686		/// Type of change, such as `Origin`, `Codename`, or `Version`.
687		pub field: String,
688		/// Previous value.
689		pub old_value: String,
690		/// New value.
691		pub new_value: String,
692		/// Localized description supplied by APT.
693		pub message: String,
694		/// Whether APT configuration permits this change without confirmation.
695		pub default_action: bool,
696	}
697
698	extern "Rust" {
699		type AcquireProgress<'a>;
700		type OperationProgress<'a>;
701		type InstallProgressFancy<'a>;
702
703		/// Called when an operation has been updated.
704		fn update(self: &mut OperationProgress, operation: String, percent: f32);
705
706		/// Called when an operation has finished.
707		fn done(self: &mut OperationProgress);
708
709		/// Called when the install status has changed.
710		fn status_changed(
711			self: &mut InstallProgressFancy,
712			pkgname: String,
713			steps_done: u64,
714			total_steps: u64,
715			action: String,
716		);
717
718		// TODO: What kind of errors can be returned here?
719		// Research and update higher level structs as well
720		// TODO: Create custom errors when we have better information
721		fn error(
722			self: &mut InstallProgressFancy,
723			pkgname: String,
724			steps_done: u64,
725			total_steps: u64,
726			error: String,
727		);
728
729		/// Called on c++ to set the pulse interval.
730		fn pulse_interval(self: &mut AcquireProgress) -> usize;
731
732		/// Called when a repository changes information from its Release file.
733		fn release_info_changes(self: &mut AcquireProgress, info: ReleaseInfoChanges) -> bool;
734
735		/// Called when an item is confirmed to be up-to-date.
736		fn hit(self: &mut AcquireProgress, item: &ItemDesc);
737
738		/// Called when an Item has started to download
739		fn fetch(self: &mut AcquireProgress, item: &ItemDesc);
740
741		/// Called when an Item fails to download
742		fn fail(self: &mut AcquireProgress, item: &ItemDesc);
743
744		/// Called periodically to provide the overall progress information
745		fn pulse(self: &mut AcquireProgress, owner: &PkgAcquire);
746
747		/// Called when an item is successfully and completely fetched.
748		fn done(self: &mut AcquireProgress, item: &ItemDesc);
749
750		/// Called when progress has started
751		fn start(self: &mut AcquireProgress);
752
753		/// Called when progress has finished
754		fn stop(self: &mut AcquireProgress);
755	}
756
757	extern "C++" {
758		type ItemDesc = crate::acquire::raw::ItemDesc;
759		type PkgAcquire = crate::acquire::raw::PkgAcquire;
760		include!("rust-apt/apt-pkg-c/types.h");
761	}
762}
763
764#[cfg(test)]
765mod tests {
766	use super::{DynAcquireProgress, ReleaseInfoChange, ReleaseInfoChanges, progress_fraction};
767	use crate::raw::{AcqTextStatus, ItemDesc, PkgAcquire};
768
769	struct Progress;
770
771	impl DynAcquireProgress for Progress {
772		fn pulse_interval(&self) -> usize { 0 }
773
774		fn hit(&mut self, _: &ItemDesc) {}
775
776		fn fetch(&mut self, _: &ItemDesc) {}
777
778		fn fail(&mut self, _: &ItemDesc) {}
779
780		fn pulse(&mut self, _: &AcqTextStatus, _: &PkgAcquire) {}
781
782		fn done(&mut self, _: &ItemDesc) {}
783
784		fn start(&mut self) {}
785
786		fn stop(&mut self, _: &AcqTextStatus) {}
787	}
788
789	fn change(default_action: bool) -> ReleaseInfoChange {
790		ReleaseInfoChange {
791			field: "Origin".into(),
792			old_value: "Earth".into(),
793			new_value: "Mars".into(),
794			message: "Repository changed its Origin".into(),
795			default_action,
796		}
797	}
798
799	fn info(changes: Vec<ReleaseInfoChange>) -> ReleaseInfoChanges {
800		ReleaseInfoChanges {
801			uri: "https://deb.example.invalid".into(),
802			dist: "stable".into(),
803			changes,
804		}
805	}
806
807	#[test]
808	fn release_info_changes_use_apt_default_action() {
809		let mut progress = Progress;
810
811		assert!(progress.release_info_changes(info(vec![change(true)])));
812		assert!(!progress.release_info_changes(info(vec![change(true), change(false)])));
813	}
814
815	#[test]
816	fn progress_fractions_are_bounded() {
817		assert_eq!(progress_fraction(1, 0), 0.0);
818		assert_eq!(progress_fraction(50, 100), 0.5);
819		assert_eq!(progress_fraction(200, 100), 1.0);
820		assert_eq!(progress_fraction(u64::MAX, u64::MAX), 1.0);
821	}
822}