//! Minimal stderr logging utilities. use std::{ cell::RefCell, io::{self, Write}, }; /// Logging verbosity level. #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum Verbosity { /// Suppress warnings and informational messages. Quiet, /// Emit warnings but suppress informational messages. Normal, /// Emit warnings and informational messages. Verbose, } /// Lightweight logger used by renderers and generators. #[derive(Debug)] pub struct Logger { verbosity: Verbosity, writer: RefCell, } impl Logger { /// Creates a logger with the given verbosity. pub fn new(verbosity: Verbosity) -> Self { Self { verbosity, writer: RefCell::new(io::stderr()), } } } impl Logger { pub fn with_writer(verbosity: Verbosity, writer: W) -> Self { Self { verbosity, writer: RefCell::new(writer), } } /// Emits a warning unless verbosity is [`Verbosity::Quiet`]. pub fn warn(&self, message: impl AsRef) { if self.verbosity != Verbosity::Quiet { let _ = writeln!(self.writer.borrow_mut(), "Warning: {}", message.as_ref()); } } /// Emits an informational message only when verbosity is [`Verbosity::Verbose`]. pub fn info(&self, message: impl AsRef) { if self.verbosity == Verbosity::Verbose { let _ = writeln!(self.writer.borrow_mut(), "Info: {}", message.as_ref()); } } } #[cfg(test)] mod tests { use std::io::{self, Write}; use super::{Logger, Verbosity}; fn logged_output(logger: &Logger>) -> String { String::from_utf8(logger.writer.borrow().clone()).unwrap() } #[test] fn quiet_suppresses_warnings() { let logger = Logger::with_writer(Verbosity::Quiet, Vec::new()); logger.warn("skipped file"); assert_eq!(logged_output(&logger), ""); } #[test] fn quiet_suppresses_info_messages() { let logger = Logger::with_writer(Verbosity::Quiet, Vec::new()); logger.info("rendering file"); assert_eq!(logged_output(&logger), ""); } #[test] fn normal_writes_warnings() { let logger = Logger::with_writer(Verbosity::Normal, Vec::new()); logger.warn("skipped file"); assert_eq!(logged_output(&logger), "Warning: skipped file\n"); } #[test] fn normal_suppresses_info_messages() { let logger = Logger::with_writer(Verbosity::Normal, Vec::new()); logger.info("rendering file"); assert_eq!(logged_output(&logger), ""); } #[test] fn verbose_writes_warnings() { let logger = Logger::with_writer(Verbosity::Verbose, Vec::new()); logger.warn("skipped file"); assert_eq!(logged_output(&logger), "Warning: skipped file\n"); } #[test] fn verbose_writes_info_messages() { let logger = Logger::with_writer(Verbosity::Verbose, Vec::new()); logger.info("rendering file"); assert_eq!(logged_output(&logger), "Info: rendering file\n"); } #[test] fn verbose_preserves_log_order() { let logger = Logger::with_writer(Verbosity::Verbose, Vec::new()); logger.info("rendering a.rs"); logger.warn("skipping duplicate file: a.rs"); logger.info("rendering b.rs"); assert_eq!( logged_output(&logger), "Info: rendering a.rs\n\ Warning: skipping duplicate file: a.rs\n\ Info: rendering b.rs\n" ); } struct FailingWriter; impl Write for FailingWriter { fn write(&mut self, _buf: &[u8]) -> io::Result { Err(io::Error::other("intentional write failure")) } fn flush(&mut self) -> io::Result<()> { Ok(()) } } #[test] fn logging_ignores_writer_errors() { let logger = Logger::with_writer(Verbosity::Verbose, FailingWriter); logger.warn("this should not panic"); logger.info("this should not panic"); } }