rust_loguru/
test_utils.rs

1//! Test utilities for Rust-Loguru
2//!
3//! Provides log capture, assertion macros, and mock logger for testing.
4
5use crate::level::LogLevel;
6use crate::record::Record;
7use std::sync::{Arc, Mutex};
8
9/// Captured log record
10#[derive(Debug, Clone)]
11pub struct CapturedRecord {
12    pub level: LogLevel,
13    pub message: String,
14    pub module: Option<String>,
15    pub file: Option<String>,
16    pub line: Option<u32>,
17}
18
19/// Log capture system for tests
20#[derive(Clone, Default)]
21pub struct LogCapture {
22    records: Arc<Mutex<Vec<CapturedRecord>>>,
23}
24
25impl LogCapture {
26    pub fn new() -> Self {
27        Self {
28            records: Arc::new(Mutex::new(Vec::new())),
29        }
30    }
31
32    pub fn handle(&self, record: &Record) {
33        let captured = CapturedRecord {
34            level: record.level(),
35            message: record.message().to_string(),
36            module: Some(record.module().to_string()),
37            file: Some(record.file().to_string()),
38            line: Some(record.line()),
39        };
40        self.records.lock().unwrap().push(captured);
41    }
42
43    pub fn clear(&self) {
44        self.records.lock().unwrap().clear();
45    }
46
47    pub fn records(&self) -> Vec<CapturedRecord> {
48        self.records.lock().unwrap().clone()
49    }
50
51    pub fn contains_message(&self, msg: &str) -> bool {
52        self.records().iter().any(|r| r.message.contains(msg))
53    }
54
55    pub fn contains_level(&self, level: LogLevel) -> bool {
56        self.records().iter().any(|r| r.level == level)
57    }
58}
59
60/// Mock logger for testing
61pub struct MockLogger {
62    pub capture: LogCapture,
63    pub level: LogLevel,
64}
65
66impl MockLogger {
67    pub fn new(level: LogLevel) -> Self {
68        Self {
69            capture: LogCapture::new(),
70            level,
71        }
72    }
73
74    pub fn log(&self, record: &Record) -> bool {
75        if record.level() >= self.level {
76            self.capture.handle(record);
77            true
78        } else {
79            false
80        }
81    }
82
83    pub fn level(&self) -> LogLevel {
84        self.level
85    }
86
87    pub fn set_level(&mut self, level: LogLevel) {
88        self.level = level;
89    }
90}
91
92/// Assertion macro for log capture
93#[macro_export]
94macro_rules! assert_log_contains {
95    ($capture:expr, $msg:expr) => {
96        assert!(
97            $capture.contains_message($msg),
98            "Expected log to contain message: {}\nCaptured: {:?}",
99            $msg,
100            $capture.records()
101        );
102    };
103}
104
105/// Assertion macro for log level
106#[macro_export]
107macro_rules! assert_log_level {
108    ($capture:expr, $level:expr) => {
109        assert!(
110            $capture.contains_level($level),
111            "Expected log to contain level: {:?}\nCaptured: {:?}",
112            $level,
113            $capture.records()
114        );
115    };
116}
117
118/// Temporarily set log level for a logger in a test
119pub struct TempLogLevel<'a, L: ?Sized> {
120    logger: &'a mut L,
121    old_level: LogLevel,
122    set_level: fn(&mut L, LogLevel),
123}
124
125impl<'a, L: ?Sized> TempLogLevel<'a, L> {
126    pub fn new(
127        logger: &'a mut L,
128        new_level: LogLevel,
129        set_level: fn(&mut L, LogLevel),
130        get_level: fn(&L) -> LogLevel,
131    ) -> Self {
132        let old_level = get_level(logger);
133        set_level(logger, new_level);
134        Self {
135            logger,
136            old_level,
137            set_level,
138        }
139    }
140}
141
142impl<L: ?Sized> Drop for TempLogLevel<'_, L> {
143    fn drop(&mut self) {
144        (self.set_level)(self.logger, self.old_level);
145    }
146}