#![allow(dead_code)]
use std::time::{Duration, SystemTime, UNIX_EPOCH};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum IngestAction {
FileCopied,
ChecksumVerified,
MetadataExtracted,
ChecksumError(String),
IoError(String),
ValidationError(String),
Skipped(String),
}
impl IngestAction {
#[must_use]
pub fn is_error(&self) -> bool {
matches!(
self,
Self::ChecksumError(_) | Self::IoError(_) | Self::ValidationError(_)
)
}
#[must_use]
pub fn is_success(&self) -> bool {
!self.is_error()
}
#[must_use]
pub fn label(&self) -> &'static str {
match self {
Self::FileCopied => "FILE_COPIED",
Self::ChecksumVerified => "CHECKSUM_OK",
Self::MetadataExtracted => "METADATA_OK",
Self::ChecksumError(_) => "CHECKSUM_ERR",
Self::IoError(_) => "IO_ERR",
Self::ValidationError(_) => "VALIDATE_ERR",
Self::Skipped(_) => "SKIPPED",
}
}
}
#[derive(Debug, Clone)]
pub struct IngestLogEntry {
pub asset: String,
pub action: IngestAction,
pub timestamp_secs: u64,
pub note: Option<String>,
}
impl IngestLogEntry {
#[must_use]
pub fn new(asset: impl Into<String>, action: IngestAction, timestamp_secs: u64) -> Self {
Self {
asset: asset.into(),
action,
timestamp_secs,
note: None,
}
}
#[must_use]
pub fn now(asset: impl Into<String>, action: IngestAction) -> Self {
let ts = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or(Duration::ZERO)
.as_secs();
Self::new(asset, action, ts)
}
#[must_use]
pub fn with_note(mut self, note: impl Into<String>) -> Self {
self.note = Some(note.into());
self
}
#[must_use]
pub fn is_recent(&self, reference_secs: u64, window_secs: u64) -> bool {
let age = reference_secs.saturating_sub(self.timestamp_secs);
age <= window_secs
}
}
#[derive(Debug, Default, Clone)]
pub struct IngestLog {
entries: Vec<IngestLogEntry>,
}
impl IngestLog {
#[must_use]
pub fn new() -> Self {
Self {
entries: Vec::new(),
}
}
pub fn record(&mut self, entry: IngestLogEntry) {
self.entries.push(entry);
}
#[must_use]
pub fn all(&self) -> &[IngestLogEntry] {
&self.entries
}
#[must_use]
pub fn errors(&self) -> Vec<&IngestLogEntry> {
self.entries
.iter()
.filter(|e| e.action.is_error())
.collect()
}
#[must_use]
pub fn recent_entries(&self, reference_secs: u64, window_secs: u64) -> Vec<&IngestLogEntry> {
self.entries
.iter()
.filter(|e| e.is_recent(reference_secs, window_secs))
.collect()
}
#[must_use]
pub fn len(&self) -> usize {
self.entries.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
#[must_use]
pub fn error_count(&self) -> usize {
self.errors().len()
}
#[must_use]
pub fn success_count(&self) -> usize {
self.entries
.iter()
.filter(|e| e.action.is_success())
.collect::<Vec<_>>()
.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn file_copied_is_not_error() {
assert!(!IngestAction::FileCopied.is_error());
}
#[test]
fn checksum_error_is_error() {
assert!(IngestAction::ChecksumError("mismatch".into()).is_error());
}
#[test]
fn io_error_is_error() {
assert!(IngestAction::IoError("disk full".into()).is_error());
}
#[test]
fn validation_error_is_error() {
assert!(IngestAction::ValidationError("bad header".into()).is_error());
}
#[test]
fn skipped_is_not_error() {
assert!(!IngestAction::Skipped("duplicate".into()).is_error());
}
#[test]
fn label_nonempty_for_all_variants() {
let variants = [
IngestAction::FileCopied,
IngestAction::ChecksumVerified,
IngestAction::MetadataExtracted,
IngestAction::ChecksumError("e".into()),
IngestAction::IoError("e".into()),
IngestAction::ValidationError("e".into()),
IngestAction::Skipped("s".into()),
];
for v in &variants {
assert!(!v.label().is_empty());
}
}
#[test]
fn is_recent_within_window() {
let entry = IngestLogEntry::new("asset.mxf", IngestAction::FileCopied, 1000);
assert!(entry.is_recent(1050, 60));
}
#[test]
fn is_recent_outside_window() {
let entry = IngestLogEntry::new("asset.mxf", IngestAction::FileCopied, 900);
assert!(!entry.is_recent(1000, 60));
}
#[test]
fn entry_with_note_stores_note() {
let entry =
IngestLogEntry::new("a.mxf", IngestAction::FileCopied, 0).with_note("test note");
assert_eq!(entry.note.as_deref(), Some("test note"));
}
#[test]
fn empty_log() {
let log = IngestLog::new();
assert!(log.is_empty());
assert_eq!(log.len(), 0);
}
#[test]
fn record_increments_len() {
let mut log = IngestLog::new();
log.record(IngestLogEntry::new("a.mxf", IngestAction::FileCopied, 0));
assert_eq!(log.len(), 1);
}
#[test]
fn errors_returns_only_error_entries() {
let mut log = IngestLog::new();
log.record(IngestLogEntry::new("a.mxf", IngestAction::FileCopied, 0));
log.record(IngestLogEntry::new(
"b.mxf",
IngestAction::IoError("fail".into()),
0,
));
assert_eq!(log.errors().len(), 1);
}
#[test]
fn error_count_correct() {
let mut log = IngestLog::new();
log.record(IngestLogEntry::new(
"a.mxf",
IngestAction::ChecksumError("x".into()),
0,
));
log.record(IngestLogEntry::new("b.mxf", IngestAction::FileCopied, 0));
assert_eq!(log.error_count(), 1);
}
#[test]
fn success_count_correct() {
let mut log = IngestLog::new();
log.record(IngestLogEntry::new("a.mxf", IngestAction::FileCopied, 0));
log.record(IngestLogEntry::new(
"b.mxf",
IngestAction::ChecksumVerified,
0,
));
log.record(IngestLogEntry::new(
"c.mxf",
IngestAction::IoError("e".into()),
0,
));
assert_eq!(log.success_count(), 2);
}
#[test]
fn recent_entries_filters_by_window() {
let mut log = IngestLog::new();
log.record(IngestLogEntry::new(
"old.mxf",
IngestAction::FileCopied,
100,
));
log.record(IngestLogEntry::new(
"new.mxf",
IngestAction::FileCopied,
950,
));
let recent = log.recent_entries(1000, 60);
assert_eq!(recent.len(), 1);
assert_eq!(recent[0].asset, "new.mxf");
}
#[test]
fn all_returns_all_entries() {
let mut log = IngestLog::new();
log.record(IngestLogEntry::new("a.mxf", IngestAction::FileCopied, 0));
log.record(IngestLogEntry::new("b.mxf", IngestAction::FileCopied, 0));
assert_eq!(log.all().len(), 2);
}
}