mock_logger/
lib.rs

1use log::{Level, LevelFilter, Log};
2use std::cell::{Ref, RefCell};
3use std::fmt;
4use std::sync::Once;
5
6/// Captured log entry.
7pub struct LogEntry {
8    pub level: Level,
9    pub body: String,
10}
11
12impl<'a> From<&'a log::Record<'a>> for LogEntry {
13    fn from(record: &'a log::Record) -> Self {
14        Self {
15            level: record.level(),
16            body: fmt::format(*record.args()),
17        }
18    }
19}
20
21thread_local! {
22    static LOG_ENTRIES: RefCell<Vec<LogEntry>> = RefCell::new(Vec::<LogEntry>::new());
23}
24
25/// Mocked log implementation, for the purpose of validating log output in tests.
26pub struct MockLogger;
27
28impl MockLogger {
29    /// Access all captured log entries.
30    pub fn entries<F>(f: F)
31    where
32        F: FnOnce(Ref<'_, Vec<LogEntry>>),
33    {
34        LOG_ENTRIES.with(|entries| {
35            f(entries.borrow());
36        });
37    }
38
39    /// Map over all captured log entries.
40    pub fn map<F>(f: F)
41    where
42        F: Fn(usize, &LogEntry),
43    {
44        LOG_ENTRIES.with(|entries| {
45            for (i, entry) in entries.borrow().iter().enumerate() {
46                f(i, entry);
47            }
48        });
49    }
50
51    /// Clear captured log entries.
52    pub fn empty() {
53        LOG_ENTRIES.with(|entries| {
54            entries.borrow_mut().truncate(0);
55        });
56    }
57}
58
59impl Log for MockLogger {
60    fn enabled(&self, _metadata: &log::Metadata) -> bool {
61        true
62    }
63
64    fn flush(&self) {}
65
66    fn log(&self, record: &log::Record) {
67        LOG_ENTRIES.with(|entries| {
68            entries.borrow_mut().push(LogEntry::from(record));
69        });
70    }
71}
72
73static INITIALIZED: Once = Once::new();
74static INSTANCE: MockLogger = MockLogger {};
75
76/// Initialize the MockLogger.
77/// If called after the logger has already been initialized this will clear any previously captured log entries.
78pub fn init() {
79    INITIALIZED.call_once(|| {
80        log::set_logger(&INSTANCE).unwrap();
81        log::set_max_level(LevelFilter::Debug);
82    });
83    LOG_ENTRIES.with(|entries| {
84        entries.borrow_mut().truncate(0);
85    });
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91    use log::{debug, error, info, warn};
92
93    #[test]
94    fn test_entries() {
95        init();
96        debug!("one");
97        info!("two");
98        warn!("three");
99        error!("four");
100        MockLogger::entries(|entries| {
101            assert_eq!(entries.len(), 4);
102            assert_eq!(entries[0].level, Level::Debug);
103            assert_eq!(entries[0].body, "one".to_owned());
104            assert_eq!(entries[1].level, Level::Info);
105            assert_eq!(entries[1].body, "two".to_owned());
106            assert_eq!(entries[2].level, Level::Warn);
107            assert_eq!(entries[2].body, "three".to_owned());
108            assert_eq!(entries[3].level, Level::Error);
109            assert_eq!(entries[3].body, "four".to_owned());
110        });
111    }
112
113    #[test]
114    fn test_map() {
115        init();
116        debug!("entry {}", 1);
117        debug!("entry {}", 2);
118        MockLogger::map(|i, entry| match i {
119            0 => {
120                assert_eq!(entry.level, Level::Debug);
121                assert_eq!(entry.body, "entry 1".to_owned());
122            }
123            1 => {
124                assert_eq!(entry.level, Level::Debug);
125                assert_eq!(entry.body, "entry 2".to_owned());
126            }
127            _ => {
128                panic!("MockLogger has too many entries!");
129            }
130        });
131    }
132}