Skip to main content

bnto_core/
logging.rs

1// Engine-level logging — trait + types for structured diagnostics.
2//
3// Defines the interface that all engine crates can import. No dependencies
4// on `std::fs` or external crates — WASM-compatible. Concrete adapters
5// (FileLogger for CLI, NoopLogger for browser) implement the trait.
6
7use std::fmt;
8
9/// Severity levels for log entries.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
11pub enum LogLevel {
12    Trace,
13    Debug,
14    Info,
15    Warn,
16    Error,
17}
18
19impl fmt::Display for LogLevel {
20    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
21        match self {
22            Self::Trace => write!(f, "TRACE"),
23            Self::Debug => write!(f, "DEBUG"),
24            Self::Info => write!(f, "INFO"),
25            Self::Warn => write!(f, "WARN"),
26            Self::Error => write!(f, "ERROR"),
27        }
28    }
29}
30
31/// A single log entry with optional timing data.
32pub struct LogEntry {
33    pub level: LogLevel,
34    /// Subsystem that produced this entry ("tui", "engine", "image", etc.).
35    pub target: &'static str,
36    pub message: String,
37    /// Optional elapsed time in microseconds for performance diagnostics.
38    pub elapsed_us: Option<u64>,
39}
40
41/// Logging interface for engine diagnostics.
42///
43/// Follows the same adapter pattern as `ProcessContext`: browser gets
44/// `NoopLogger`, CLI gets `FileLogger`, desktop gets whatever it needs.
45pub trait Logger: Send + Sync {
46    fn log(&self, entry: LogEntry);
47    fn is_enabled(&self, level: LogLevel) -> bool;
48    fn flush(&self);
49}
50
51/// No-op logger for browser (WASM) and test contexts.
52pub struct NoopLogger;
53
54impl Logger for NoopLogger {
55    fn log(&self, _entry: LogEntry) {}
56    fn is_enabled(&self, _level: LogLevel) -> bool {
57        false
58    }
59    fn flush(&self) {}
60}
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65
66    #[test]
67    fn log_level_ordering() {
68        assert!(LogLevel::Trace < LogLevel::Debug);
69        assert!(LogLevel::Debug < LogLevel::Info);
70        assert!(LogLevel::Info < LogLevel::Warn);
71        assert!(LogLevel::Warn < LogLevel::Error);
72    }
73
74    #[test]
75    fn log_level_display() {
76        assert_eq!(LogLevel::Trace.to_string(), "TRACE");
77        assert_eq!(LogLevel::Info.to_string(), "INFO");
78        assert_eq!(LogLevel::Error.to_string(), "ERROR");
79    }
80
81    #[test]
82    fn noop_logger_accepts_entries() {
83        let logger = NoopLogger;
84        logger.log(LogEntry {
85            level: LogLevel::Info,
86            target: "test",
87            message: "hello".into(),
88            elapsed_us: None,
89        });
90        // Should not panic.
91    }
92
93    #[test]
94    fn noop_logger_is_never_enabled() {
95        let logger = NoopLogger;
96        assert!(!logger.is_enabled(LogLevel::Trace));
97        assert!(!logger.is_enabled(LogLevel::Error));
98    }
99
100    #[test]
101    fn noop_logger_flush_is_safe() {
102        let logger = NoopLogger;
103        logger.flush();
104    }
105
106    #[test]
107    fn log_entry_with_timing() {
108        let entry = LogEntry {
109            level: LogLevel::Debug,
110            target: "tui",
111            message: "frame render".into(),
112            elapsed_us: Some(1234),
113        };
114        assert_eq!(entry.elapsed_us, Some(1234));
115        assert_eq!(entry.target, "tui");
116    }
117}