use std::sync::atomic::{AtomicU64, Ordering};
use super::*;
static PROGRESS_SEQ: AtomicU64 = AtomicU64::new(0);
pub(crate) struct ProgressReporter {
out_tx: Sender<Outbound>,
token: String,
ended: bool,
}
impl ProgressReporter {
pub(crate) fn begin(out_tx: Sender<Outbound>, title: &str, message: Option<String>) -> Self {
let n = PROGRESS_SEQ.fetch_add(1, Ordering::Relaxed);
let token = format!("arity/progress/{n}");
let work = WorkDoneProgress::Begin(WorkDoneProgressBegin {
title: title.to_string(),
cancellable: Some(false),
message,
percentage: None,
});
let _ = out_tx.send(Outbound::Progress {
token: token.clone(),
work,
});
Self {
out_tx,
token,
ended: false,
}
}
pub(crate) fn report(&self, message: String, percentage: Option<u32>) {
let work = WorkDoneProgress::Report(WorkDoneProgressReport {
cancellable: Some(false),
message: Some(message),
percentage,
});
let _ = self.out_tx.send(Outbound::Progress {
token: self.token.clone(),
work,
});
}
pub(crate) fn end(mut self, message: Option<String>) {
self.finish(message);
}
fn finish(&mut self, message: Option<String>) {
if self.ended {
return;
}
self.ended = true;
let work = WorkDoneProgress::End(WorkDoneProgressEnd { message });
let _ = self.out_tx.send(Outbound::Progress {
token: self.token.clone(),
work,
});
}
}
impl Drop for ProgressReporter {
fn drop(&mut self) {
self.finish(None);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn progress(ob: Outbound) -> (String, WorkDoneProgress) {
match ob {
Outbound::Progress { token, work } => (token, work),
_ => panic!("expected Outbound::Progress"),
}
}
#[test]
fn begin_report_end_emit_in_order_with_one_token() {
let (tx, rx) = crossbeam_channel::unbounded::<Outbound>();
let reporter = ProgressReporter::begin(tx, "Indexing", Some("2 packages".to_string()));
reporter.report("magrittr".to_string(), Some(50));
reporter.end(Some("done".to_string()));
let (t0, w0) = progress(rx.recv().unwrap());
let (t1, w1) = progress(rx.recv().unwrap());
let (t2, w2) = progress(rx.recv().unwrap());
assert!(rx.try_recv().is_err(), "no extra messages after End");
assert_eq!(t0, t1);
assert_eq!(t1, t2);
assert!(matches!(w0, WorkDoneProgress::Begin(_)));
assert!(matches!(w1, WorkDoneProgress::Report(_)));
assert!(matches!(w2, WorkDoneProgress::End(_)));
}
#[test]
fn drop_without_end_still_emits_exactly_one_end() {
let (tx, rx) = crossbeam_channel::unbounded::<Outbound>();
{
let _reporter = ProgressReporter::begin(tx, "Fetching", None);
}
let (_, begin) = progress(rx.recv().unwrap());
assert!(matches!(begin, WorkDoneProgress::Begin(_)));
let (_, end) = progress(rx.recv().unwrap());
assert!(matches!(end, WorkDoneProgress::End(_)));
assert!(rx.try_recv().is_err(), "drop emits End only once");
}
#[test]
fn explicit_end_suppresses_drop_end() {
let (tx, rx) = crossbeam_channel::unbounded::<Outbound>();
let reporter = ProgressReporter::begin(tx, "Indexing", None);
reporter.end(None); let _ = progress(rx.recv().unwrap()); let (_, end) = progress(rx.recv().unwrap());
assert!(matches!(end, WorkDoneProgress::End(_)));
assert!(rx.try_recv().is_err(), "no double End from the drop");
}
#[test]
fn distinct_reporters_get_distinct_tokens() {
let (tx, rx) = crossbeam_channel::unbounded::<Outbound>();
let a = ProgressReporter::begin(tx.clone(), "A", None);
let b = ProgressReporter::begin(tx, "B", None);
let (ta, _) = progress(rx.recv().unwrap());
let (tb, _) = progress(rx.recv().unwrap());
assert_ne!(ta, tb);
drop(a);
drop(b);
}
}