use super::traits::ProgressSink;
use std::io::Write;
use std::sync::{Arc, Mutex};
#[derive(Clone, Copy, Debug, Default)]
pub struct SilentProgressSink;
impl ProgressSink for SilentProgressSink {
#[inline]
fn report(&self, _stage: &str, _current: usize, _total: usize) {}
#[inline]
fn start_stage(&self, _name: &str) {}
#[inline]
fn complete_stage(&self, _name: &str) {}
#[inline]
fn warn(&self, _message: &str) {}
#[inline]
fn child(&self, _prefix: &str) -> Arc<dyn ProgressSink> {
Arc::new(SilentProgressSink)
}
}
#[derive(Clone, Debug)]
pub struct CliProgressSink {
quiet: bool,
}
impl CliProgressSink {
pub fn new(quiet: bool) -> Self {
Self { quiet }
}
}
impl Default for CliProgressSink {
fn default() -> Self {
Self::new(false)
}
}
impl ProgressSink for CliProgressSink {
fn report(&self, stage: &str, current: usize, total: usize) {
if !self.quiet {
eprint!("\r{}: {}/{}", stage, current + 1, total);
let _ = std::io::stderr().flush();
}
}
fn start_stage(&self, name: &str) {
if !self.quiet {
eprintln!("\n{}", name);
}
}
fn complete_stage(&self, name: &str) {
if !self.quiet {
eprintln!("\n{} complete", name);
}
}
fn warn(&self, message: &str) {
eprintln!("\nWarning: {}", message);
}
fn child(&self, _prefix: &str) -> Arc<dyn ProgressSink> {
Arc::new(CliProgressSink { quiet: self.quiet })
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ProgressEvent {
Report {
stage: String,
current: usize,
total: usize,
},
StartStage {
name: String,
},
CompleteStage {
name: String,
},
Warn {
message: String,
},
}
#[derive(Clone, Debug, Default)]
pub struct RecordingProgressSink {
events: Arc<Mutex<Vec<ProgressEvent>>>,
}
impl RecordingProgressSink {
pub fn new() -> Self {
Self::default()
}
pub fn events(&self) -> Vec<ProgressEvent> {
self.events.lock().unwrap().clone()
}
pub fn stages(&self) -> Vec<String> {
self.events()
.into_iter()
.filter_map(|e| match e {
ProgressEvent::StartStage { name } => Some(name),
_ => None,
})
.collect()
}
pub fn completed_stages(&self) -> Vec<String> {
self.events()
.into_iter()
.filter_map(|e| match e {
ProgressEvent::CompleteStage { name } => Some(name),
_ => None,
})
.collect()
}
pub fn warnings(&self) -> Vec<String> {
self.events()
.into_iter()
.filter_map(|e| match e {
ProgressEvent::Warn { message } => Some(message),
_ => None,
})
.collect()
}
pub fn clear(&self) {
self.events.lock().unwrap().clear();
}
pub fn event_count(&self) -> usize {
self.events.lock().unwrap().len()
}
}
impl ProgressSink for RecordingProgressSink {
fn report(&self, stage: &str, current: usize, total: usize) {
self.events.lock().unwrap().push(ProgressEvent::Report {
stage: stage.to_string(),
current,
total,
});
}
fn start_stage(&self, name: &str) {
self.events.lock().unwrap().push(ProgressEvent::StartStage {
name: name.to_string(),
});
}
fn complete_stage(&self, name: &str) {
self.events
.lock()
.unwrap()
.push(ProgressEvent::CompleteStage {
name: name.to_string(),
});
}
fn warn(&self, message: &str) {
self.events.lock().unwrap().push(ProgressEvent::Warn {
message: message.to_string(),
});
}
fn child(&self, _prefix: &str) -> Arc<dyn ProgressSink> {
Arc::new(self.clone())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_silent_sink_is_no_op() {
let sink = SilentProgressSink;
sink.start_stage("Test");
sink.report("Test", 0, 100);
sink.report("Test", 50, 100);
sink.complete_stage("Test");
sink.warn("Warning");
}
#[test]
fn test_silent_sink_child_returns_silent() {
let sink = SilentProgressSink;
let child = sink.child("prefix");
child.start_stage("Child Stage");
child.complete_stage("Child Stage");
}
#[test]
fn test_silent_sink_is_default() {
let sink: SilentProgressSink = Default::default();
sink.report("Test", 0, 1);
}
#[test]
fn test_cli_sink_quiet_mode() {
let sink = CliProgressSink::new(true);
sink.start_stage("Test");
sink.report("Test", 0, 100);
sink.complete_stage("Test");
}
#[test]
fn test_cli_sink_default_is_verbose() {
let sink = CliProgressSink::default();
assert!(!sink.quiet);
}
#[test]
fn test_cli_sink_child_preserves_quiet() {
let sink = CliProgressSink::new(true);
let _child = sink.child("prefix");
}
#[test]
fn test_recording_sink_records_all_events() {
let recorder = RecordingProgressSink::new();
recorder.start_stage("Stage 1");
recorder.report("Stage 1", 0, 10);
recorder.report("Stage 1", 5, 10);
recorder.complete_stage("Stage 1");
recorder.warn("Test warning");
let events = recorder.events();
assert_eq!(events.len(), 5);
assert!(matches!(
&events[0],
ProgressEvent::StartStage { name } if name == "Stage 1"
));
assert!(matches!(
&events[1],
ProgressEvent::Report { stage, current: 0, total: 10 } if stage == "Stage 1"
));
assert!(matches!(
&events[2],
ProgressEvent::Report { stage, current: 5, total: 10 } if stage == "Stage 1"
));
assert!(matches!(
&events[3],
ProgressEvent::CompleteStage { name } if name == "Stage 1"
));
assert!(matches!(
&events[4],
ProgressEvent::Warn { message } if message == "Test warning"
));
}
#[test]
fn test_recording_sink_stages_helper() {
let recorder = RecordingProgressSink::new();
recorder.start_stage("Stage 1");
recorder.start_stage("Stage 2");
recorder.complete_stage("Stage 1");
recorder.start_stage("Stage 3");
assert_eq!(recorder.stages(), vec!["Stage 1", "Stage 2", "Stage 3"]);
}
#[test]
fn test_recording_sink_completed_stages_helper() {
let recorder = RecordingProgressSink::new();
recorder.start_stage("Stage 1");
recorder.complete_stage("Stage 1");
recorder.start_stage("Stage 2");
recorder.complete_stage("Stage 2");
assert_eq!(recorder.completed_stages(), vec!["Stage 1", "Stage 2"]);
}
#[test]
fn test_recording_sink_warnings_helper() {
let recorder = RecordingProgressSink::new();
recorder.warn("Warning 1");
recorder.warn("Warning 2");
assert_eq!(recorder.warnings(), vec!["Warning 1", "Warning 2"]);
}
#[test]
fn test_recording_sink_clear() {
let recorder = RecordingProgressSink::new();
recorder.start_stage("Stage 1");
recorder.complete_stage("Stage 1");
assert_eq!(recorder.event_count(), 2);
recorder.clear();
assert_eq!(recorder.event_count(), 0);
assert!(recorder.events().is_empty());
}
#[test]
fn test_recording_sink_child_shares_events() {
let recorder = RecordingProgressSink::new();
let child = recorder.child("prefix");
recorder.start_stage("Parent Stage");
child.start_stage("Child Stage");
let events = recorder.events();
assert_eq!(events.len(), 2);
assert_eq!(recorder.stages(), vec!["Parent Stage", "Child Stage"]);
}
#[test]
fn test_recording_sink_is_clone() {
let recorder = RecordingProgressSink::new();
recorder.start_stage("Test");
let cloned = recorder.clone();
cloned.complete_stage("Test");
assert_eq!(recorder.event_count(), 2);
assert_eq!(cloned.event_count(), 2);
}
#[test]
fn test_recording_sink_is_send_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<RecordingProgressSink>();
}
#[test]
fn test_progress_event_equality() {
let event1 = ProgressEvent::Report {
stage: "Test".to_string(),
current: 0,
total: 10,
};
let event2 = ProgressEvent::Report {
stage: "Test".to_string(),
current: 0,
total: 10,
};
let event3 = ProgressEvent::Report {
stage: "Test".to_string(),
current: 1,
total: 10,
};
assert_eq!(event1, event2);
assert_ne!(event1, event3);
}
}