mt-mock-logger 0.1.2

A thread-safe mock logger for multiple tests running in parallel
Documentation
use std::{ collections::HashMap, sync::{ LazyLock, RwLock }, thread::ThreadId };

static MOCK_LOGGER: MockLogger = MockLogger::new();

pub struct MockLoggerGuard;

impl Drop for MockLoggerGuard {
    fn drop(&mut self) {
        MockLogger::remove_logger();
    }
}

pub struct MockLogger {
    #[allow(clippy::type_complexity)]
    mutex: LazyLock<RwLock<HashMap<ThreadId, (Box<dyn log::Log>, log::LevelFilter)>>>,
}

impl MockLogger {
    const fn new() -> Self {
        MockLogger {
            mutex: LazyLock::new(|| {
                let _ = log::set_logger(&MOCK_LOGGER);
                log::set_max_level(log::LevelFilter::Trace);
                RwLock::new(HashMap::new())
            }),
        }
    }

    pub fn set_logger(
        logger: impl log::Log + 'static,
        max_level: log::LevelFilter
    ) -> MockLoggerGuard {
        MOCK_LOGGER.mutex.write()
            .expect("mutex is poisoned")
            .insert(std::thread::current().id(), (Box::new(logger), max_level));

        MockLoggerGuard
    }

    fn remove_logger() {
        MOCK_LOGGER.mutex.write().expect("mutex is poisoned").remove(&std::thread::current().id());
    }
}

impl log::Log for MockLogger {
    fn enabled(&self, metadata: &log::Metadata) -> bool {
        if
            let Some((logger, _)) = self.mutex
                .read()
                .expect("mutex is poisoned")
                .get(&std::thread::current().id())
        {
            return logger.enabled(metadata);
        }

        false
    }

    fn log(&self, record: &log::Record) {
        if
            let Some((logger, max_level)) = self.mutex
                .read()
                .expect("mutex is poisoned")
                .get(&std::thread::current().id())
        {
            if record.level() <= *max_level {
                logger.log(record);
            }
        }
    }

    fn flush(&self) {
        if
            let Some((logger, _)) = self.mutex
                .read()
                .expect("mutex is poisoned")
                .get(&std::thread::current().id())
        {
            logger.flush();
        }
    }
}

#[cfg(test)]
mod tests {
    use mockall::mock;

    use super::*;

    mock! {
        pub MyLogger {}
        impl log::Log for MyLogger {
            fn enabled<'a>(&self, metadata: &log::Metadata<'a>) -> bool;
            fn log<'a>(&self, record: &log::Record<'a>);
            fn flush(&self);
        }
    }

    #[test]
    fn test_logging() {
        let mut my_logger = MockMyLogger::new();
        my_logger
            .expect_log()
            .withf(|r| r.level() == log::LevelFilter::Info && r.args().as_str() == Some("ok"))
            .once()
            .return_const(());

        let _guard = MockLogger::set_logger(my_logger, log::LevelFilter::Info);

        log::info!("ok");
        log::trace!("ok");
    }

    #[test]
    fn test_logging_below_max_level() {
        let mut my_logger = MockMyLogger::new();
        my_logger.expect_log().never().return_const(());

        let _guard = MockLogger::set_logger(my_logger, log::LevelFilter::Info);

        log::trace!("ok");
    }

    #[test]
    fn test_no_logger() {
        log::trace!("ok");
    }
}