use std::io::{self, Write};
use std::sync::atomic::{AtomicU8, Ordering};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Level {
Off = 0,
Warn = 1,
Debug = 2,
}
const _: () = assert!(Level::Off as u8 == 0);
const _: () = assert!(Level::Warn as u8 == 1);
const _: () = assert!(Level::Debug as u8 == 2);
pub const ENV_VAR: &str = "LINESMITH_LOG";
const DEFAULT_LEVEL: Level = Level::Warn;
static LEVEL: AtomicU8 = AtomicU8::new(DEFAULT_LEVEL as u8);
pub fn apply(raw: Option<&str>, warn_sink: &mut dyn Write) {
match decide_init(raw) {
InitDecision::Keep => {}
InitDecision::Set(l) => set_level(l),
InitDecision::Warn(bad) => {
let _ = writeln!(
warn_sink,
"linesmith: {ENV_VAR}={bad:?} unrecognized; using default ({DEFAULT_LEVEL:?})"
);
set_level(DEFAULT_LEVEL);
}
}
}
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum InitDecision<'a> {
Keep,
Set(Level),
Warn(&'a str),
}
pub(crate) fn decide_init(raw: Option<&str>) -> InitDecision<'_> {
match raw {
None => InitDecision::Keep,
Some(s) => match Level::parse(s) {
Some(l) => InitDecision::Set(l),
None => InitDecision::Warn(s),
},
}
}
pub fn set_level(l: Level) {
LEVEL.store(l as u8, Ordering::Relaxed);
}
#[must_use]
pub fn level() -> Level {
from_u8(LEVEL.load(Ordering::Relaxed))
}
fn from_u8(n: u8) -> Level {
match n {
0 => Level::Off,
1 => Level::Warn,
2 => Level::Debug,
_ => {
debug_assert!(false, "logging::LEVEL holds out-of-range byte {n}");
Level::Debug
}
}
}
#[must_use]
pub fn is_enabled(at_least: Level) -> bool {
level() >= at_least
}
pub fn emit(lvl: Level, msg: &str) {
if !is_enabled(lvl) {
return;
}
let tag = match lvl {
Level::Off => return,
Level::Warn => "warn",
Level::Debug => "debug",
};
let _ = writeln!(io::stderr().lock(), "linesmith [{tag}]: {msg}");
}
pub fn emit_error(msg: &str) {
emit_error_to(msg, &mut io::stderr().lock());
}
pub(crate) fn emit_error_to(msg: &str, sink: &mut dyn Write) {
let _ = writeln!(sink, "linesmith [error]: {msg}");
}
impl Level {
#[must_use]
pub fn parse(s: &str) -> Option<Self> {
match s.trim().to_ascii_lowercase().as_str() {
"off" | "none" | "0" => Some(Level::Off),
"warn" | "warning" => Some(Level::Warn),
"debug" | "trace" | "all" => Some(Level::Debug),
_ => None,
}
}
}
#[macro_export]
macro_rules! lsm_warn {
($($arg:tt)*) => {
$crate::logging::emit($crate::logging::Level::Warn, &format!($($arg)*))
};
}
#[macro_export]
macro_rules! lsm_debug {
($($arg:tt)*) => {
if $crate::logging::is_enabled($crate::logging::Level::Debug) {
$crate::logging::emit($crate::logging::Level::Debug, &format!($($arg)*));
}
};
}
#[macro_export]
macro_rules! lsm_error {
($($arg:tt)*) => {
$crate::logging::emit_error(&format!($($arg)*))
};
}
#[cfg(test)]
mod tests {
use super::*;
fn lock() -> std::sync::MutexGuard<'static, ()> {
use std::sync::{Mutex, OnceLock};
static M: OnceLock<Mutex<()>> = OnceLock::new();
M.get_or_init(|| Mutex::new(()))
.lock()
.unwrap_or_else(|p| p.into_inner())
}
#[test]
fn default_level_is_warn() {
let _g = lock();
set_level(DEFAULT_LEVEL);
assert_eq!(level(), Level::Warn);
assert!(is_enabled(Level::Warn));
assert!(!is_enabled(Level::Debug));
}
#[test]
fn debug_enables_every_lower_level() {
let _g = lock();
set_level(Level::Debug);
assert!(is_enabled(Level::Warn));
assert!(is_enabled(Level::Debug));
set_level(DEFAULT_LEVEL);
}
#[test]
fn off_suppresses_every_level() {
let _g = lock();
set_level(Level::Off);
assert!(!is_enabled(Level::Warn));
assert!(!is_enabled(Level::Debug));
set_level(DEFAULT_LEVEL);
}
#[test]
fn parse_accepts_common_aliases() {
assert_eq!(Level::parse("warn"), Some(Level::Warn));
assert_eq!(Level::parse("WARN"), Some(Level::Warn));
assert_eq!(Level::parse(" warn "), Some(Level::Warn));
assert_eq!(Level::parse("warning"), Some(Level::Warn));
assert_eq!(Level::parse("debug"), Some(Level::Debug));
assert_eq!(Level::parse("trace"), Some(Level::Debug));
assert_eq!(Level::parse("all"), Some(Level::Debug));
assert_eq!(Level::parse("off"), Some(Level::Off));
assert_eq!(Level::parse("none"), Some(Level::Off));
assert_eq!(Level::parse("0"), Some(Level::Off));
}
#[test]
fn parse_rejects_error_and_info_aliases() {
assert_eq!(Level::parse("error"), None);
assert_eq!(Level::parse("info"), None);
}
#[test]
fn parse_rejects_garbage() {
assert_eq!(Level::parse("verbose"), None);
assert_eq!(Level::parse(""), None);
assert_eq!(Level::parse("debug2"), None);
}
#[test]
fn decide_init_keeps_default_when_env_unset() {
assert_eq!(decide_init(None), InitDecision::Keep);
}
#[test]
fn decide_init_parses_recognized_levels() {
assert_eq!(decide_init(Some("debug")), InitDecision::Set(Level::Debug));
assert_eq!(decide_init(Some("warn")), InitDecision::Set(Level::Warn));
assert_eq!(decide_init(Some("off")), InitDecision::Set(Level::Off));
}
#[test]
fn decide_init_warns_on_garbage() {
assert_eq!(decide_init(Some("loud")), InitDecision::Warn("loud"));
assert_eq!(decide_init(Some("")), InitDecision::Warn(""));
}
#[test]
fn apply_writes_warning_to_injected_sink_and_resets_to_default() {
let _g = lock();
set_level(Level::Off);
let mut sink = Vec::<u8>::new();
apply(Some("loud"), &mut sink);
let written = String::from_utf8(sink).expect("utf8");
assert!(
written.contains("LINESMITH_LOG=\"loud\""),
"expected the unrecognized value echoed, got {written:?}"
);
assert!(written.contains("unrecognized"));
assert_eq!(level(), DEFAULT_LEVEL);
}
#[test]
fn apply_keeps_level_when_env_unset() {
let _g = lock();
set_level(Level::Debug);
let mut sink = Vec::<u8>::new();
apply(None, &mut sink);
assert!(sink.is_empty(), "no-env must not write: {sink:?}");
assert_eq!(level(), Level::Debug);
set_level(DEFAULT_LEVEL);
}
#[test]
fn apply_sets_recognized_level_without_writing() {
let _g = lock();
set_level(Level::Off);
let mut sink = Vec::<u8>::new();
apply(Some("debug"), &mut sink);
assert!(sink.is_empty());
assert_eq!(level(), Level::Debug);
set_level(DEFAULT_LEVEL);
}
#[test]
fn lsm_debug_skips_format_when_suppressed() {
use std::cell::Cell;
use std::fmt;
struct CountingDisplay<'a>(&'a Cell<u32>);
impl fmt::Display for CountingDisplay<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.set(self.0.get() + 1);
f.write_str("x")
}
}
let _g = lock();
let counter = Cell::new(0u32);
set_level(Level::Warn);
lsm_debug!("{}", CountingDisplay(&counter));
assert_eq!(counter.get(), 0, "format! must not run when suppressed");
set_level(Level::Debug);
lsm_debug!("{}", CountingDisplay(&counter));
assert_eq!(counter.get(), 1, "format! must run when enabled");
set_level(DEFAULT_LEVEL);
}
#[test]
fn from_u8_roundtrips_known_bytes() {
assert_eq!(from_u8(0), Level::Off);
assert_eq!(from_u8(1), Level::Warn);
assert_eq!(from_u8(2), Level::Debug);
}
#[test]
#[should_panic(expected = "out-of-range byte")]
fn from_u8_debug_panics_on_out_of_range() {
let _ = from_u8(99);
}
#[test]
fn emit_error_bypasses_off_level() {
let _g = lock();
set_level(Level::Off);
let mut sink = Vec::<u8>::new();
emit_error_to("render panic", &mut sink);
let written = String::from_utf8(sink).expect("utf8");
assert_eq!(written, "linesmith [error]: render panic\n");
set_level(DEFAULT_LEVEL);
}
#[test]
fn emit_error_fires_at_every_level() {
let _g = lock();
for l in [Level::Off, Level::Warn, Level::Debug] {
set_level(l);
let mut sink = Vec::<u8>::new();
emit_error_to("x", &mut sink);
assert!(
!sink.is_empty(),
"emit_error must fire at level {l:?}, got empty sink"
);
}
set_level(DEFAULT_LEVEL);
}
}