reifydb_core/interface/logging/
mock.rs

1// Copyright (c) reifydb.com 2025
2// This file is licensed under the AGPL-3.0-or-later, see license.md file
3
4//! Mock logger support for testing
5//!
6//! This module provides thread-local mock logger functionality that allows
7//! tests to intercept and redirect log messages without interfering with
8//! the global logger or other tests running in parallel.
9//!
10//! This functionality is only available in debug builds and is compiled out
11//! in release builds for zero runtime overhead in production.
12
13#[cfg(debug_assertions)]
14use std::cell::RefCell;
15
16#[cfg(debug_assertions)]
17use crossbeam_channel::Sender;
18
19#[cfg(debug_assertions)]
20use super::Record;
21
22#[cfg(debug_assertions)]
23thread_local! {
24    /// Thread-local storage for mock logger sender
25    /// When set, log messages will be sent here instead of the global logger
26    static MOCK_LOGGER: RefCell<Option<Sender<Record>>> = RefCell::new(None);
27}
28
29/// Set a mock logger for the current thread
30#[cfg(debug_assertions)]
31pub fn set_mock_logger(sender: Sender<Record>) {
32	MOCK_LOGGER.with(|logger| {
33		*logger.borrow_mut() = Some(sender);
34	});
35}
36
37/// Clear the mock logger for the current thread
38#[cfg(debug_assertions)]
39pub fn clear_mock_logger() {
40	MOCK_LOGGER.with(|logger| {
41		*logger.borrow_mut() = None;
42	});
43}
44
45/// Get the current mock logger sender if one is set
46#[cfg(debug_assertions)]
47pub fn get_mock_logger() -> Option<Sender<Record>> {
48	MOCK_LOGGER.with(|logger| logger.borrow().clone())
49}
50
51/// Check if a mock logger is currently active
52#[cfg(debug_assertions)]
53pub fn is_mock_logger_active() -> bool {
54	MOCK_LOGGER.with(|logger| logger.borrow().is_some())
55}
56
57/// RAII guard that sets a mock logger and clears it when dropped
58#[cfg(debug_assertions)]
59pub struct MockLoggerGuard {
60	/// Previous logger that was set (if any)
61	previous: Option<Sender<Record>>,
62}
63
64#[cfg(debug_assertions)]
65impl MockLoggerGuard {
66	/// Create a new mock logger guard that sets the given sender
67	pub fn new(sender: Sender<Record>) -> Self {
68		let previous = get_mock_logger();
69		set_mock_logger(sender);
70		Self {
71			previous,
72		}
73	}
74}
75
76#[cfg(debug_assertions)]
77impl Drop for MockLoggerGuard {
78	fn drop(&mut self) {
79		// Restore the previous logger (or clear if there wasn't one)
80		if let Some(sender) = self.previous.take() {
81			set_mock_logger(sender);
82		} else {
83			clear_mock_logger();
84		}
85	}
86}
87
88/// Run a function with a mock logger active
89#[cfg(debug_assertions)]
90pub fn with_mock_logger<T>(sender: Sender<Record>, f: impl FnOnce() -> T) -> T {
91	let _guard = MockLoggerGuard::new(sender);
92	f()
93}
94
95#[cfg(test)]
96mod tests {
97	use LogLevel::{Debug, Info};
98	use crossbeam_channel::unbounded;
99
100	use super::*;
101	use crate::interface::logging::{LogLevel, Record};
102
103	#[test]
104	fn test_mock_logger_basic() {
105		let (sender, receiver) = unbounded();
106
107		assert!(!is_mock_logger_active());
108
109		set_mock_logger(sender.clone());
110		assert!(is_mock_logger_active());
111
112		// Should be able to get the logger back
113		let retrieved = get_mock_logger().unwrap();
114		let record = Record::new(Info, "test", "message");
115		retrieved.send(record.clone()).unwrap();
116
117		let received = receiver.try_recv().unwrap();
118		assert_eq!(received.message, "message");
119
120		clear_mock_logger();
121		assert!(!is_mock_logger_active());
122	}
123
124	#[test]
125	fn test_mock_logger_guard() {
126		let (sender1, _) = unbounded();
127		let (sender2, _) = unbounded();
128
129		assert!(!is_mock_logger_active());
130
131		{
132			let _guard = MockLoggerGuard::new(sender1.clone());
133			assert!(is_mock_logger_active());
134
135			// Nested guard
136			{
137				let _guard2 = MockLoggerGuard::new(sender2.clone());
138				assert!(is_mock_logger_active());
139				// Should have sender2 active
140			}
141
142			// Should restore sender1
143			assert!(is_mock_logger_active());
144		}
145
146		// Should be cleared after guard drops
147		assert!(!is_mock_logger_active());
148	}
149
150	#[test]
151	fn test_with_mock_logger() {
152		let (sender, receiver) = unbounded();
153
154		assert!(!is_mock_logger_active());
155
156		let result = with_mock_logger(sender, || {
157			assert!(is_mock_logger_active());
158
159			let logger = get_mock_logger().unwrap();
160			let record = Record::new(Debug, "test", "test message");
161			logger.send(record).unwrap();
162
163			42
164		});
165
166		assert_eq!(result, 42);
167		assert!(!is_mock_logger_active());
168
169		let received = receiver.try_recv().unwrap();
170		assert_eq!(received.message, "test message");
171	}
172
173	#[test]
174	fn test_thread_isolation() {
175		use std::thread;
176
177		let (sender1, receiver1) = unbounded();
178		let (sender2, receiver2) = unbounded();
179
180		let handle1 = thread::spawn(move || {
181			with_mock_logger(sender1, || {
182				let logger = get_mock_logger().unwrap();
183				let record = Record::new(Info, "thread1", "message1");
184				logger.send(record).unwrap();
185			});
186		});
187
188		let handle2 = thread::spawn(move || {
189			with_mock_logger(sender2, || {
190				let logger = get_mock_logger().unwrap();
191				let record = Record::new(Info, "thread2", "message2");
192				logger.send(record).unwrap();
193			});
194		});
195
196		handle1.join().unwrap();
197		handle2.join().unwrap();
198
199		// Each thread should have sent to its own receiver
200		let received1 = receiver1.try_recv().unwrap();
201		assert_eq!(received1.module, "thread1");
202		assert_eq!(received1.message, "message1");
203
204		let received2 = receiver2.try_recv().unwrap();
205		assert_eq!(received2.module, "thread2");
206		assert_eq!(received2.message, "message2");
207	}
208}