1use std::io::{self, Write};
28use std::sync::atomic::{AtomicU8, Ordering};
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
33pub enum Level {
34 Off = 0,
35 Warn = 1,
38 Debug = 2,
41}
42
43const _: () = assert!(Level::Off as u8 == 0);
47const _: () = assert!(Level::Warn as u8 == 1);
48const _: () = assert!(Level::Debug as u8 == 2);
49
50pub const ENV_VAR: &str = "LINESMITH_LOG";
51
52const DEFAULT_LEVEL: Level = Level::Warn;
53static LEVEL: AtomicU8 = AtomicU8::new(DEFAULT_LEVEL as u8);
54
55pub fn apply(raw: Option<&str>, warn_sink: &mut dyn Write) {
61 match decide_init(raw) {
62 InitDecision::Keep => {}
63 InitDecision::Set(l) => set_level(l),
64 InitDecision::Warn(bad) => {
65 let _ = writeln!(
66 warn_sink,
67 "linesmith: {ENV_VAR}={bad:?} unrecognized; using default ({DEFAULT_LEVEL:?})"
68 );
69 set_level(DEFAULT_LEVEL);
70 }
71 }
72}
73
74#[derive(Debug, PartialEq, Eq)]
78pub(crate) enum InitDecision<'a> {
79 Keep,
81 Set(Level),
83 Warn(&'a str),
85}
86
87pub(crate) fn decide_init(raw: Option<&str>) -> InitDecision<'_> {
88 match raw {
89 None => InitDecision::Keep,
90 Some(s) => match Level::parse(s) {
91 Some(l) => InitDecision::Set(l),
92 None => InitDecision::Warn(s),
93 },
94 }
95}
96
97pub fn set_level(l: Level) {
100 LEVEL.store(l as u8, Ordering::Relaxed);
101}
102
103#[must_use]
104pub fn level() -> Level {
105 from_u8(LEVEL.load(Ordering::Relaxed))
106}
107
108fn from_u8(n: u8) -> Level {
113 match n {
114 0 => Level::Off,
115 1 => Level::Warn,
116 2 => Level::Debug,
117 _ => {
118 debug_assert!(false, "logging::LEVEL holds out-of-range byte {n}");
119 Level::Debug
120 }
121 }
122}
123
124#[must_use]
127pub fn is_enabled(at_least: Level) -> bool {
128 level() >= at_least
129}
130
131pub fn emit(lvl: Level, msg: &str) {
136 if !is_enabled(lvl) {
137 return;
138 }
139 let tag = match lvl {
140 Level::Off => return,
141 Level::Warn => "warn",
142 Level::Debug => "debug",
143 };
144 let _ = writeln!(io::stderr().lock(), "linesmith [{tag}]: {msg}");
145}
146
147pub fn emit_error(msg: &str) {
152 emit_error_to(msg, &mut io::stderr().lock());
153}
154
155pub(crate) fn emit_error_to(msg: &str, sink: &mut dyn Write) {
159 let _ = writeln!(sink, "linesmith [error]: {msg}");
160}
161
162impl Level {
163 #[must_use]
169 pub fn parse(s: &str) -> Option<Self> {
170 match s.trim().to_ascii_lowercase().as_str() {
171 "off" | "none" | "0" => Some(Level::Off),
172 "warn" | "warning" => Some(Level::Warn),
173 "debug" | "trace" | "all" => Some(Level::Debug),
174 _ => None,
175 }
176 }
177}
178
179#[macro_export]
181macro_rules! lsm_warn {
182 ($($arg:tt)*) => {
183 $crate::logging::emit($crate::logging::Level::Warn, &format!($($arg)*))
184 };
185}
186
187#[macro_export]
190macro_rules! lsm_debug {
191 ($($arg:tt)*) => {
192 if $crate::logging::is_enabled($crate::logging::Level::Debug) {
193 $crate::logging::emit($crate::logging::Level::Debug, &format!($($arg)*));
194 }
195 };
196}
197
198#[macro_export]
203macro_rules! lsm_error {
204 ($($arg:tt)*) => {
205 $crate::logging::emit_error(&format!($($arg)*))
206 };
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212
213 fn lock() -> std::sync::MutexGuard<'static, ()> {
216 use std::sync::{Mutex, OnceLock};
217 static M: OnceLock<Mutex<()>> = OnceLock::new();
218 M.get_or_init(|| Mutex::new(()))
219 .lock()
220 .unwrap_or_else(|p| p.into_inner())
221 }
222
223 #[test]
224 fn default_level_is_warn() {
225 let _g = lock();
226 set_level(DEFAULT_LEVEL);
227 assert_eq!(level(), Level::Warn);
228 assert!(is_enabled(Level::Warn));
229 assert!(!is_enabled(Level::Debug));
230 }
231
232 #[test]
233 fn debug_enables_every_lower_level() {
234 let _g = lock();
235 set_level(Level::Debug);
236 assert!(is_enabled(Level::Warn));
237 assert!(is_enabled(Level::Debug));
238 set_level(DEFAULT_LEVEL);
239 }
240
241 #[test]
242 fn off_suppresses_every_level() {
243 let _g = lock();
244 set_level(Level::Off);
245 assert!(!is_enabled(Level::Warn));
246 assert!(!is_enabled(Level::Debug));
247 set_level(DEFAULT_LEVEL);
248 }
249
250 #[test]
251 fn parse_accepts_common_aliases() {
252 assert_eq!(Level::parse("warn"), Some(Level::Warn));
253 assert_eq!(Level::parse("WARN"), Some(Level::Warn));
254 assert_eq!(Level::parse(" warn "), Some(Level::Warn));
255 assert_eq!(Level::parse("warning"), Some(Level::Warn));
256 assert_eq!(Level::parse("debug"), Some(Level::Debug));
257 assert_eq!(Level::parse("trace"), Some(Level::Debug));
258 assert_eq!(Level::parse("all"), Some(Level::Debug));
259 assert_eq!(Level::parse("off"), Some(Level::Off));
260 assert_eq!(Level::parse("none"), Some(Level::Off));
261 assert_eq!(Level::parse("0"), Some(Level::Off));
262 }
263
264 #[test]
265 fn parse_rejects_error_and_info_aliases() {
266 assert_eq!(Level::parse("error"), None);
270 assert_eq!(Level::parse("info"), None);
271 }
272
273 #[test]
274 fn parse_rejects_garbage() {
275 assert_eq!(Level::parse("verbose"), None);
276 assert_eq!(Level::parse(""), None);
277 assert_eq!(Level::parse("debug2"), None);
278 }
279
280 #[test]
281 fn decide_init_keeps_default_when_env_unset() {
282 assert_eq!(decide_init(None), InitDecision::Keep);
283 }
284
285 #[test]
286 fn decide_init_parses_recognized_levels() {
287 assert_eq!(decide_init(Some("debug")), InitDecision::Set(Level::Debug));
288 assert_eq!(decide_init(Some("warn")), InitDecision::Set(Level::Warn));
289 assert_eq!(decide_init(Some("off")), InitDecision::Set(Level::Off));
290 }
291
292 #[test]
293 fn decide_init_warns_on_garbage() {
294 assert_eq!(decide_init(Some("loud")), InitDecision::Warn("loud"));
295 assert_eq!(decide_init(Some("")), InitDecision::Warn(""));
296 }
297
298 #[test]
299 fn apply_writes_warning_to_injected_sink_and_resets_to_default() {
300 let _g = lock();
301 set_level(Level::Off);
302 let mut sink = Vec::<u8>::new();
303 apply(Some("loud"), &mut sink);
304 let written = String::from_utf8(sink).expect("utf8");
305 assert!(
306 written.contains("LINESMITH_LOG=\"loud\""),
307 "expected the unrecognized value echoed, got {written:?}"
308 );
309 assert!(written.contains("unrecognized"));
310 assert_eq!(level(), DEFAULT_LEVEL);
313 }
314
315 #[test]
316 fn apply_keeps_level_when_env_unset() {
317 let _g = lock();
318 set_level(Level::Debug);
319 let mut sink = Vec::<u8>::new();
320 apply(None, &mut sink);
321 assert!(sink.is_empty(), "no-env must not write: {sink:?}");
322 assert_eq!(level(), Level::Debug);
323 set_level(DEFAULT_LEVEL);
324 }
325
326 #[test]
327 fn apply_sets_recognized_level_without_writing() {
328 let _g = lock();
329 set_level(Level::Off);
330 let mut sink = Vec::<u8>::new();
331 apply(Some("debug"), &mut sink);
332 assert!(sink.is_empty());
333 assert_eq!(level(), Level::Debug);
334 set_level(DEFAULT_LEVEL);
335 }
336
337 #[test]
338 fn lsm_debug_skips_format_when_suppressed() {
339 use std::cell::Cell;
343 use std::fmt;
344 struct CountingDisplay<'a>(&'a Cell<u32>);
345 impl fmt::Display for CountingDisplay<'_> {
346 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
347 self.0.set(self.0.get() + 1);
348 f.write_str("x")
349 }
350 }
351
352 let _g = lock();
353 let counter = Cell::new(0u32);
354
355 set_level(Level::Warn);
356 lsm_debug!("{}", CountingDisplay(&counter));
357 assert_eq!(counter.get(), 0, "format! must not run when suppressed");
358
359 set_level(Level::Debug);
360 lsm_debug!("{}", CountingDisplay(&counter));
361 assert_eq!(counter.get(), 1, "format! must run when enabled");
362
363 set_level(DEFAULT_LEVEL);
364 }
365
366 #[test]
367 fn from_u8_roundtrips_known_bytes() {
368 assert_eq!(from_u8(0), Level::Off);
369 assert_eq!(from_u8(1), Level::Warn);
370 assert_eq!(from_u8(2), Level::Debug);
371 }
372
373 #[test]
374 #[should_panic(expected = "out-of-range byte")]
375 fn from_u8_debug_panics_on_out_of_range() {
376 let _ = from_u8(99);
378 }
379
380 #[test]
381 fn emit_error_bypasses_off_level() {
382 let _g = lock();
383 set_level(Level::Off);
384 let mut sink = Vec::<u8>::new();
385 emit_error_to("render panic", &mut sink);
386 let written = String::from_utf8(sink).expect("utf8");
387 assert_eq!(written, "linesmith [error]: render panic\n");
388 set_level(DEFAULT_LEVEL);
389 }
390
391 #[test]
392 fn emit_error_fires_at_every_level() {
393 let _g = lock();
394 for l in [Level::Off, Level::Warn, Level::Debug] {
395 set_level(l);
396 let mut sink = Vec::<u8>::new();
397 emit_error_to("x", &mut sink);
398 assert!(
399 !sink.is_empty(),
400 "emit_error must fire at level {l:?}, got empty sink"
401 );
402 }
403 set_level(DEFAULT_LEVEL);
404 }
405}