#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum WorkUnit {
Archives,
Segments,
Nodes,
Revisions,
JournalLines,
Checkpoints,
Files,
Bytes,
IndexEntries,
IndexDocuments,
}
impl WorkUnit {
#[must_use]
pub fn plural_noun(self) -> &'static str {
match self {
Self::Archives => "archives",
Self::Segments => "segments",
Self::Nodes => "nodes",
Self::Revisions => "revisions",
Self::JournalLines => "journal lines",
Self::Checkpoints => "checkpoints",
Self::Files => "files",
Self::Bytes => "bytes",
Self::IndexEntries => "index entries",
Self::IndexDocuments => "index documents",
}
}
#[must_use]
pub fn singular_noun(self) -> &'static str {
match self {
Self::Archives => "archive",
Self::Segments => "segment",
Self::Nodes => "node",
Self::Revisions => "revision",
Self::JournalLines => "journal line",
Self::Checkpoints => "checkpoint",
Self::Files => "file",
Self::Bytes => "byte",
Self::IndexEntries => "index entry",
Self::IndexDocuments => "index document",
}
}
#[must_use]
pub fn noun_for(self, count: u64) -> &'static str {
if count == 1 {
self.singular_noun()
} else {
self.plural_noun()
}
}
}
impl std::fmt::Display for WorkUnit {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str(self.plural_noun())
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Step<'description> {
description: &'description str,
unit: WorkUnit,
total: Option<u64>,
}
impl<'description> Step<'description> {
#[must_use]
pub fn new(description: &'description str, unit: WorkUnit) -> Self {
Self {
description,
unit,
total: None,
}
}
#[must_use]
pub fn with_total(mut self, total: u64) -> Self {
self.total = Some(total);
self
}
#[must_use]
pub fn description(&self) -> &'description str {
self.description
}
#[must_use]
pub fn unit(&self) -> WorkUnit {
self.unit
}
#[must_use]
pub fn total(&self) -> Option<u64> {
self.total
}
}
pub trait ProgressObserver {
fn step_began(&mut self, step: &Step<'_>);
fn step_advanced(&mut self, completed: u64);
fn step_total_resolved(&mut self, total: u64) {
let _ = total;
}
fn step_concluded(&mut self, conclusion: &str) {
let _ = conclusion;
}
fn step_ended(&mut self);
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct DiscardedProgress;
impl ProgressObserver for DiscardedProgress {
fn step_began(&mut self, _step: &Step<'_>) {}
fn step_advanced(&mut self, _completed: u64) {}
fn step_ended(&mut self) {}
}
impl<Observer: ProgressObserver + ?Sized> ProgressObserver for &mut Observer {
fn step_began(&mut self, step: &Step<'_>) {
(**self).step_began(step);
}
fn step_advanced(&mut self, completed: u64) {
(**self).step_advanced(completed);
}
fn step_total_resolved(&mut self, total: u64) {
(**self).step_total_resolved(total);
}
fn step_concluded(&mut self, conclusion: &str) {
(**self).step_concluded(conclusion);
}
fn step_ended(&mut self) {
(**self).step_ended();
}
}
pub(crate) fn count(items: usize) -> u64 {
u64::try_from(items).unwrap_or(u64::MAX)
}
pub(crate) fn observe<Value, Error>(
observer: &mut dyn ProgressObserver,
step: &Step<'_>,
work: impl FnOnce(&mut dyn ProgressObserver) -> std::result::Result<Value, Error>,
) -> std::result::Result<Value, Error> {
observer.step_began(step);
let outcome = work(observer);
observer.step_ended();
outcome
}
pub(crate) struct StrideCounter {
completed: u64,
last_reported: u64,
stride: u64,
}
impl StrideCounter {
pub(crate) fn new(stride: u64) -> Self {
Self::resuming(stride, 0)
}
pub(crate) fn resuming(stride: u64, already: u64) -> Self {
Self {
completed: already,
last_reported: already,
stride: stride.max(1),
}
}
pub(crate) fn advance(&mut self, observer: &mut dyn ProgressObserver) {
self.completed += 1;
if self.completed - self.last_reported >= self.stride {
self.last_reported = self.completed;
observer.step_advanced(self.completed);
}
}
pub(crate) fn finish(&mut self, observer: &mut dyn ProgressObserver) {
if self.completed != self.last_reported {
self.last_reported = self.completed;
observer.step_advanced(self.completed);
}
}
pub(crate) fn completed(&self) -> u64 {
self.completed
}
}
#[cfg(test)]
mod tests {
use super::{DiscardedProgress, ProgressObserver, Step, StrideCounter, WorkUnit, observe};
#[derive(Default)]
struct RecordingObserver {
calls: Vec<String>,
}
impl ProgressObserver for RecordingObserver {
fn step_began(&mut self, step: &Step<'_>) {
self.calls.push(match step.total() {
Some(total) => format!("began {} of {total} {}", step.description(), step.unit()),
None => format!("began {} of {}", step.description(), step.unit()),
});
}
fn step_advanced(&mut self, completed: u64) {
self.calls.push(format!("advanced {completed}"));
}
fn step_total_resolved(&mut self, total: u64) {
self.calls.push(format!("total {total}"));
}
fn step_ended(&mut self) {
self.calls.push("ended".to_owned());
}
}
#[test]
fn a_step_carries_its_description_unit_and_total() {
let step = Step::new("scanning archives", WorkUnit::Archives).with_total(48);
assert_eq!(step.description(), "scanning archives");
assert_eq!(step.unit(), WorkUnit::Archives);
assert_eq!(step.total(), Some(48));
assert_eq!(Step::new("tracing", WorkUnit::Segments).total(), None);
}
#[test]
fn units_name_themselves_in_the_plural() {
assert_eq!(WorkUnit::Archives.plural_noun(), "archives");
assert_eq!(WorkUnit::JournalLines.to_string(), "journal lines");
}
#[test]
fn the_discarding_observer_accepts_every_call() {
let mut observer = DiscardedProgress;
observer.step_began(&Step::new("working", WorkUnit::Files));
observer.step_advanced(7);
observer.step_total_resolved(9);
observer.step_ended();
}
#[test]
fn bracketing_ends_the_step_after_a_failure() {
let mut observer = RecordingObserver::default();
let outcome: std::result::Result<(), &str> = observe(
&mut observer,
&Step::new("failing", WorkUnit::Files),
|_observer| Err("no"),
);
assert_eq!(outcome, Err("no"));
assert_eq!(observer.calls, ["began failing of files", "ended"]);
}
#[test]
fn a_borrowed_observer_forwards_every_call() {
let mut observer = RecordingObserver::default();
let borrowed: &mut dyn ProgressObserver = &mut observer;
borrowed.step_began(&Step::new("copying", WorkUnit::Nodes).with_total(2));
borrowed.step_advanced(1);
borrowed.step_total_resolved(3);
borrowed.step_ended();
assert_eq!(
observer.calls,
["began copying of 2 nodes", "advanced 1", "total 3", "ended"]
);
}
#[test]
fn the_stride_counter_reports_only_on_the_stride() {
let mut observer = RecordingObserver::default();
let mut counter = StrideCounter::new(4);
for _ in 0..9 {
counter.advance(&mut observer);
}
assert_eq!(counter.completed(), 9);
assert_eq!(observer.calls, ["advanced 4", "advanced 8"]);
}
#[test]
fn a_zero_stride_reports_every_item() {
let mut observer = RecordingObserver::default();
let mut counter = StrideCounter::new(0);
counter.advance(&mut observer);
counter.advance(&mut observer);
assert_eq!(observer.calls, ["advanced 1", "advanced 2"]);
}
}