use std::sync::Arc;
pub trait ProgressHandler: Send + Sync {
fn on_progress(&self, event: &ProgressEvent);
}
pub struct Progress(Arc<dyn ProgressHandler>);
impl Progress {
pub fn new<H: ProgressHandler + 'static>(handler: H) -> Self {
Self(Arc::new(handler))
}
}
impl Clone for Progress {
fn clone(&self) -> Self {
Self(Arc::clone(&self.0))
}
}
impl std::fmt::Debug for Progress {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Progress").finish_non_exhaustive()
}
}
impl std::ops::Deref for Progress {
type Target = dyn ProgressHandler;
fn deref(&self) -> &Self::Target {
&*self.0
}
}
impl<H: ProgressHandler + 'static> From<H> for Progress {
fn from(handler: H) -> Self {
Self(Arc::new(handler))
}
}
impl<H: ProgressHandler + 'static> From<Arc<H>> for Progress {
fn from(handler: Arc<H>) -> Self {
Self(handler)
}
}
impl From<Arc<dyn ProgressHandler>> for Progress {
fn from(handler: Arc<dyn ProgressHandler>) -> Self {
Self(handler)
}
}
#[derive(Debug, Clone)]
pub enum ProgressEvent {
Upload(UploadEvent),
Download(DownloadEvent),
}
#[derive(Debug, Clone)]
pub enum UploadEvent {
Start {
total_files: usize,
total_bytes: u64,
},
Progress {
bytes_completed: u64,
total_bytes: u64,
bytes_per_sec: Option<f64>,
transfer_bytes_completed: u64,
transfer_bytes: u64,
transfer_bytes_per_sec: Option<f64>,
files: Vec<FileProgress>,
},
Committing,
Complete,
}
#[derive(Debug, Clone)]
pub enum DownloadEvent {
Start {
total_files: usize,
total_bytes: u64,
},
Progress {
files: Vec<FileProgress>,
},
AggregateProgress {
bytes_completed: u64,
total_bytes: u64,
bytes_per_sec: Option<f64>,
},
Complete,
}
#[derive(Debug, Clone)]
pub struct FileProgress {
pub filename: String,
pub bytes_completed: u64,
pub total_bytes: u64,
pub status: FileStatus,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FileStatus {
Started,
InProgress,
Complete,
}
impl From<UploadEvent> for ProgressEvent {
fn from(event: UploadEvent) -> Self {
ProgressEvent::Upload(event)
}
}
impl From<DownloadEvent> for ProgressEvent {
fn from(event: DownloadEvent) -> Self {
ProgressEvent::Download(event)
}
}
pub(crate) trait EmitEvent {
fn emit(&self, event: impl Into<ProgressEvent>);
}
impl<T: ProgressHandler + ?Sized> EmitEvent for T {
fn emit(&self, event: impl Into<ProgressEvent>) {
self.on_progress(&event.into());
}
}
impl EmitEvent for Option<Progress> {
fn emit(&self, event: impl Into<ProgressEvent>) {
if let Some(h) = self {
h.on_progress(&event.into());
}
}
}
#[cfg(test)]
mod tests {
use std::sync::{Arc, Mutex};
use super::*;
struct RecordingHandler {
events: Mutex<Vec<ProgressEvent>>,
}
impl RecordingHandler {
fn new() -> Self {
Self {
events: Mutex::new(Vec::new()),
}
}
fn events(&self) -> Vec<ProgressEvent> {
self.events.lock().unwrap().clone()
}
}
impl ProgressHandler for RecordingHandler {
fn on_progress(&self, event: &ProgressEvent) {
self.events.lock().unwrap().push(event.clone());
}
}
#[test]
fn handler_is_send_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<Arc<RecordingHandler>>();
}
#[test]
fn emit_with_none_is_noop() {
let progress: Option<Progress> = None;
progress.emit(DownloadEvent::Complete);
}
#[test]
fn emit_records_events() {
let handler = Arc::new(RecordingHandler::new());
let progress: Option<Progress> = Some(handler.clone().into());
progress.emit(UploadEvent::Start {
total_files: 2,
total_bytes: 1024,
});
progress.emit(UploadEvent::Progress {
bytes_completed: 512,
total_bytes: 1024,
bytes_per_sec: Some(100.0),
transfer_bytes_completed: 0,
transfer_bytes: 0,
transfer_bytes_per_sec: None,
files: vec![],
});
progress.emit(UploadEvent::Complete);
let events = handler.events();
assert_eq!(events.len(), 3);
assert!(matches!(events[0], ProgressEvent::Upload(UploadEvent::Start { .. })));
assert!(matches!(events[1], ProgressEvent::Upload(UploadEvent::Progress { .. })));
assert!(matches!(events[2], ProgressEvent::Upload(UploadEvent::Complete)));
}
#[test]
fn download_file_lifecycle() {
let handler = Arc::new(RecordingHandler::new());
let progress: Option<Progress> = Some(handler.clone().into());
progress.emit(DownloadEvent::Start {
total_files: 1,
total_bytes: 1000,
});
progress.emit(DownloadEvent::Progress {
files: vec![FileProgress {
filename: "file.bin".to_string(),
bytes_completed: 0,
total_bytes: 1000,
status: FileStatus::Started,
}],
});
progress.emit(DownloadEvent::Progress {
files: vec![FileProgress {
filename: "file.bin".to_string(),
bytes_completed: 500,
total_bytes: 1000,
status: FileStatus::InProgress,
}],
});
progress.emit(DownloadEvent::Progress {
files: vec![FileProgress {
filename: "file.bin".to_string(),
bytes_completed: 1000,
total_bytes: 1000,
status: FileStatus::Complete,
}],
});
progress.emit(DownloadEvent::Complete);
let events = handler.events();
assert_eq!(events.len(), 5);
}
#[test]
fn upload_event_ordering() {
let handler = Arc::new(RecordingHandler::new());
let progress: Option<Progress> = Some(handler.clone().into());
progress.emit(UploadEvent::Start {
total_files: 1,
total_bytes: 100,
});
progress.emit(UploadEvent::Progress {
bytes_completed: 50,
total_bytes: 100,
bytes_per_sec: None,
transfer_bytes_completed: 0,
transfer_bytes: 0,
transfer_bytes_per_sec: None,
files: vec![],
});
progress.emit(UploadEvent::Committing);
progress.emit(UploadEvent::Complete);
let events = handler.events();
assert_eq!(events.len(), 4);
assert!(matches!(events[0], ProgressEvent::Upload(UploadEvent::Start { .. })));
assert!(matches!(events[1], ProgressEvent::Upload(UploadEvent::Progress { .. })));
assert!(matches!(events[2], ProgressEvent::Upload(UploadEvent::Committing)));
assert!(matches!(events[3], ProgressEvent::Upload(UploadEvent::Complete)));
}
#[test]
fn upload_progress_with_per_file_data() {
let handler = Arc::new(RecordingHandler::new());
let progress: Option<Progress> = Some(handler.clone().into());
progress.emit(UploadEvent::Progress {
bytes_completed: 500,
total_bytes: 1000,
bytes_per_sec: Some(100.0),
transfer_bytes_completed: 250,
transfer_bytes: 800,
transfer_bytes_per_sec: Some(50.0),
files: vec![
FileProgress {
filename: "model/weights.bin".to_string(),
bytes_completed: 300,
total_bytes: 600,
status: FileStatus::InProgress,
},
FileProgress {
filename: "config.json".to_string(),
bytes_completed: 200,
total_bytes: 400,
status: FileStatus::InProgress,
},
],
});
let events = handler.events();
assert_eq!(events.len(), 1);
if let ProgressEvent::Upload(UploadEvent::Progress {
files,
transfer_bytes_completed,
transfer_bytes,
transfer_bytes_per_sec,
..
}) = &events[0]
{
assert_eq!(files.len(), 2);
assert_eq!(files[0].filename, "model/weights.bin");
assert_eq!(files[1].filename, "config.json");
assert_eq!(*transfer_bytes_completed, 250);
assert_eq!(*transfer_bytes, 800);
assert_eq!(*transfer_bytes_per_sec, Some(50.0));
} else {
panic!("expected Upload(Progress)");
}
}
}