Skip to main content

oximedia_transcode/
encoding_log.rs

1#![allow(dead_code)]
2//! Structured log for encoding sessions.
3
4use std::time::{Duration, SystemTime};
5
6/// Categories of events that can occur during an encoding session.
7#[derive(Debug, Clone, PartialEq, Eq, Hash)]
8pub enum EncodingEvent {
9    /// A non-fatal advisory message.
10    Warning(String),
11    /// A fatal error that stopped encoding.
12    Error(String),
13    /// A milestone reporting percentage completion.
14    Progress(u8),
15    /// A phase boundary (e.g. "pass 1 complete").
16    Phase(String),
17    /// An informational note.
18    Info(String),
19}
20
21impl EncodingEvent {
22    /// Returns `true` for error-level events.
23    #[must_use]
24    pub fn is_error(&self) -> bool {
25        matches!(self, Self::Error(_))
26    }
27
28    /// Returns `true` for warning-level events.
29    #[must_use]
30    pub fn is_warning(&self) -> bool {
31        matches!(self, Self::Warning(_))
32    }
33
34    /// Returns `true` for progress milestone events.
35    #[must_use]
36    pub fn is_progress(&self) -> bool {
37        matches!(self, Self::Progress(_))
38    }
39
40    /// Extract the human-readable message, if any.
41    #[must_use]
42    pub fn message(&self) -> Option<&str> {
43        match self {
44            Self::Warning(m) | Self::Error(m) | Self::Phase(m) | Self::Info(m) => Some(m),
45            Self::Progress(_) => None,
46        }
47    }
48}
49
50/// A single entry in the encoding log.
51#[derive(Debug, Clone)]
52pub struct EncodingLogEntry {
53    /// The event that was logged.
54    pub event: EncodingEvent,
55    /// Wall-clock time when the event occurred.
56    pub timestamp: SystemTime,
57    /// Session-relative elapsed time at the moment of the event.
58    pub elapsed: Duration,
59}
60
61impl EncodingLogEntry {
62    /// Create a new log entry.
63    #[must_use]
64    pub fn new(event: EncodingEvent, timestamp: SystemTime, elapsed: Duration) -> Self {
65        Self {
66            event,
67            timestamp,
68            elapsed,
69        }
70    }
71
72    /// Returns `true` if the entry was recorded less than `window` ago.
73    #[must_use]
74    pub fn is_recent(&self, window: Duration) -> bool {
75        self.timestamp
76            .elapsed()
77            .map(|age| age < window)
78            .unwrap_or(false)
79    }
80}
81
82/// A complete log of encoding events for one session.
83#[derive(Debug, Default)]
84pub struct EncodingLog {
85    entries: Vec<EncodingLogEntry>,
86    session_start: Option<SystemTime>,
87}
88
89impl EncodingLog {
90    /// Create an empty log, recording the current time as session start.
91    #[must_use]
92    pub fn new() -> Self {
93        Self {
94            entries: Vec::new(),
95            session_start: Some(SystemTime::now()),
96        }
97    }
98
99    /// Record a new event, automatically computing elapsed time.
100    pub fn record(&mut self, event: EncodingEvent) {
101        let now = SystemTime::now();
102        let elapsed = self
103            .session_start
104            .and_then(|s| now.duration_since(s).ok())
105            .unwrap_or(Duration::ZERO);
106        self.entries
107            .push(EncodingLogEntry::new(event, now, elapsed));
108    }
109
110    /// All error entries.
111    #[must_use]
112    pub fn errors(&self) -> Vec<&EncodingLogEntry> {
113        self.entries.iter().filter(|e| e.event.is_error()).collect()
114    }
115
116    /// All warning entries.
117    #[must_use]
118    pub fn warnings(&self) -> Vec<&EncodingLogEntry> {
119        self.entries
120            .iter()
121            .filter(|e| e.event.is_warning())
122            .collect()
123    }
124
125    /// All progress milestone entries.
126    #[must_use]
127    pub fn progress_events(&self) -> Vec<&EncodingLogEntry> {
128        self.entries
129            .iter()
130            .filter(|e| e.event.is_progress())
131            .collect()
132    }
133
134    /// All entries in insertion order.
135    #[must_use]
136    pub fn all_entries(&self) -> &[EncodingLogEntry] {
137        &self.entries
138    }
139
140    /// Total number of logged entries.
141    #[must_use]
142    pub fn len(&self) -> usize {
143        self.entries.len()
144    }
145
146    /// Returns `true` when the log is empty.
147    #[must_use]
148    pub fn is_empty(&self) -> bool {
149        self.entries.is_empty()
150    }
151
152    /// Returns `true` if any fatal error was recorded.
153    #[must_use]
154    pub fn has_errors(&self) -> bool {
155        self.entries.iter().any(|e| e.event.is_error())
156    }
157
158    /// The most recent progress percentage (0–100), or `None` if not yet reported.
159    #[must_use]
160    pub fn last_progress_pct(&self) -> Option<u8> {
161        self.entries.iter().rev().find_map(|e| {
162            if let EncodingEvent::Progress(p) = e.event {
163                Some(p)
164            } else {
165                None
166            }
167        })
168    }
169}
170
171#[cfg(test)]
172mod tests {
173    use super::*;
174
175    #[test]
176    fn test_encoding_event_is_error_true() {
177        let e = EncodingEvent::Error("oops".into());
178        assert!(e.is_error());
179    }
180
181    #[test]
182    fn test_encoding_event_is_error_false() {
183        assert!(!EncodingEvent::Warning("w".into()).is_error());
184        assert!(!EncodingEvent::Progress(50).is_error());
185    }
186
187    #[test]
188    fn test_encoding_event_is_warning() {
189        assert!(EncodingEvent::Warning("low bitrate".into()).is_warning());
190        assert!(!EncodingEvent::Info("ok".into()).is_warning());
191    }
192
193    #[test]
194    fn test_encoding_event_is_progress() {
195        assert!(EncodingEvent::Progress(75).is_progress());
196        assert!(!EncodingEvent::Error("x".into()).is_progress());
197    }
198
199    #[test]
200    fn test_encoding_event_message() {
201        let e = EncodingEvent::Error("disk full".into());
202        assert_eq!(e.message(), Some("disk full"));
203        let p = EncodingEvent::Progress(50);
204        assert!(p.message().is_none());
205    }
206
207    #[test]
208    fn test_log_record_and_len() {
209        let mut log = EncodingLog::new();
210        log.record(EncodingEvent::Info("start".into()));
211        log.record(EncodingEvent::Progress(25));
212        assert_eq!(log.len(), 2);
213    }
214
215    #[test]
216    fn test_log_errors() {
217        let mut log = EncodingLog::new();
218        log.record(EncodingEvent::Error("bad codec".into()));
219        log.record(EncodingEvent::Warning("slow".into()));
220        assert_eq!(log.errors().len(), 1);
221    }
222
223    #[test]
224    fn test_log_warnings() {
225        let mut log = EncodingLog::new();
226        log.record(EncodingEvent::Warning("W1".into()));
227        log.record(EncodingEvent::Warning("W2".into()));
228        log.record(EncodingEvent::Error("E".into()));
229        assert_eq!(log.warnings().len(), 2);
230    }
231
232    #[test]
233    fn test_log_progress_events() {
234        let mut log = EncodingLog::new();
235        log.record(EncodingEvent::Progress(10));
236        log.record(EncodingEvent::Progress(50));
237        log.record(EncodingEvent::Info("info".into()));
238        assert_eq!(log.progress_events().len(), 2);
239    }
240
241    #[test]
242    fn test_log_has_errors_false() {
243        let mut log = EncodingLog::new();
244        log.record(EncodingEvent::Warning("w".into()));
245        assert!(!log.has_errors());
246    }
247
248    #[test]
249    fn test_log_has_errors_true() {
250        let mut log = EncodingLog::new();
251        log.record(EncodingEvent::Error("fatal".into()));
252        assert!(log.has_errors());
253    }
254
255    #[test]
256    fn test_log_last_progress_pct_none() {
257        let log = EncodingLog::new();
258        assert!(log.last_progress_pct().is_none());
259    }
260
261    #[test]
262    fn test_log_last_progress_pct_some() {
263        let mut log = EncodingLog::new();
264        log.record(EncodingEvent::Progress(25));
265        log.record(EncodingEvent::Progress(75));
266        assert_eq!(log.last_progress_pct(), Some(75));
267    }
268
269    #[test]
270    fn test_log_is_empty() {
271        let log = EncodingLog::new();
272        assert!(log.is_empty());
273    }
274
275    #[test]
276    fn test_entry_elapsed_non_negative() {
277        let mut log = EncodingLog::new();
278        log.record(EncodingEvent::Info("hi".into()));
279        assert!(!log.is_empty());
280        let entry = &log.all_entries()[0];
281        // elapsed should be very small but non-negative
282        assert!(entry.elapsed < Duration::from_secs(5));
283    }
284}