1#![forbid(unsafe_code)]
35#![deny(missing_docs)]
36
37use parking_lot::Mutex;
38use rlg::log::Log;
39use rlg::log_level::LogLevel;
40use serde_json::Value;
41use std::sync::Arc;
42
43#[derive(Debug, Clone, Default)]
48pub struct Capture {
49 inner: Arc<Mutex<Vec<Log>>>,
50}
51
52impl Capture {
53 #[must_use]
55 pub fn new() -> Self {
56 Self::default()
57 }
58
59 pub fn push(&self, record: Log) {
61 self.inner.lock().push(record);
62 }
63
64 #[must_use]
66 pub fn records(&self) -> Vec<Log> {
67 self.inner.lock().clone()
68 }
69
70 #[must_use]
72 pub fn len(&self) -> usize {
73 self.inner.lock().len()
74 }
75
76 #[must_use]
78 pub fn is_empty(&self) -> bool {
79 self.inner.lock().is_empty()
80 }
81
82 pub fn clear(&self) {
84 self.inner.lock().clear();
85 }
86}
87
88#[must_use]
91pub fn capture() -> Capture {
92 Capture::new()
93}
94
95pub trait LogExt {
98 fn log_to(self, capture: &Capture);
100}
101
102impl LogExt for Log {
103 fn log_to(self, capture: &Capture) {
104 capture.push(self);
105 }
106}
107
108#[must_use]
114pub fn has_level(capture: &Capture, level: LogLevel) -> bool {
115 capture.inner.lock().iter().any(|r| r.level == level)
116}
117
118#[must_use]
121pub fn description_contains(capture: &Capture, needle: &str) -> bool {
122 capture
123 .inner
124 .lock()
125 .iter()
126 .any(|r| r.description.contains(needle))
127}
128
129pub fn attribute_eq<V>(
132 capture: &Capture,
133 key: &str,
134 expected: V,
135) -> bool
136where
137 V: Into<Value>,
138{
139 let want = expected.into();
140 capture
141 .inner
142 .lock()
143 .iter()
144 .any(|r| r.attributes.get(key) == Some(&want))
145}
146
147#[must_use]
149pub fn has_component(capture: &Capture, component: &str) -> bool {
150 capture
151 .inner
152 .lock()
153 .iter()
154 .any(|r| r.component.as_ref() == component)
155}
156
157#[macro_export]
174macro_rules! assert_logged {
175 ($capture:expr, level == $level:expr) => {{
176 assert!(
177 $crate::has_level(&$capture, $level),
178 "expected a captured record at level {:?}, got: {:?}",
179 $level,
180 $capture.records()
181 );
182 }};
183 ($capture:expr, contains $needle:expr) => {{
184 assert!(
185 $crate::description_contains(&$capture, $needle),
186 "expected a captured record description to contain {:?}, got: {:?}",
187 $needle,
188 $capture.records()
189 );
190 }};
191 ($capture:expr, attribute $key:expr => $expected:expr) => {{
192 assert!(
193 $crate::attribute_eq(&$capture, $key, $expected),
194 "expected a captured record with attribute {:?} == {:?}, got: {:?}",
195 $key,
196 $expected,
197 $capture.records()
198 );
199 }};
200 ($capture:expr, component $component:expr) => {{
201 assert!(
202 $crate::has_component(&$capture, $component),
203 "expected a captured record with component {:?}, got: {:?}",
204 $component,
205 $capture.records()
206 );
207 }};
208 ($capture:expr, len == $n:expr) => {{
209 let n = $capture.len();
210 assert!(
211 n == $n,
212 "expected {} captured records, got {}: {:?}",
213 $n,
214 n,
215 $capture.records()
216 );
217 }};
218}
219
220#[cfg(test)]
221mod tests {
222 use super::*;
223 use rlg::log::Log;
224
225 #[test]
226 fn capture_starts_empty() {
227 let c = capture();
228 assert!(c.is_empty());
229 assert_eq!(c.len(), 0);
230 }
231
232 #[test]
233 fn log_to_appends() {
234 let c = capture();
235 Log::info("hi").component("svc").log_to(&c);
236 Log::warn("oops").log_to(&c);
237 assert_eq!(c.len(), 2);
238 }
239
240 #[test]
241 fn clear_drops_records() {
242 let c = capture();
243 Log::info("x").log_to(&c);
244 c.clear();
245 assert!(c.is_empty());
246 }
247
248 #[test]
249 fn predicates_match_levels() {
250 let c = capture();
251 Log::error("boom").log_to(&c);
252 assert!(has_level(&c, LogLevel::ERROR));
253 assert!(!has_level(&c, LogLevel::INFO));
254 }
255
256 #[test]
257 fn predicates_match_components() {
258 let c = capture();
259 Log::info("x").component("auth").log_to(&c);
260 assert!(has_component(&c, "auth"));
261 assert!(!has_component(&c, "other"));
262 }
263
264 #[test]
265 fn predicates_match_descriptions() {
266 let c = capture();
267 Log::info("authenticated user").log_to(&c);
268 assert!(description_contains(&c, "authenticated"));
269 assert!(!description_contains(&c, "missing"));
270 }
271
272 #[test]
273 fn predicates_match_attributes() {
274 let c = capture();
275 Log::info("x")
276 .with("user_id", 42_u64)
277 .with("region", "eu-west-1")
278 .log_to(&c);
279 assert!(attribute_eq(&c, "user_id", 42_u64));
280 assert!(attribute_eq(&c, "region", "eu-west-1"));
281 assert!(!attribute_eq(&c, "user_id", 99_u64));
282 }
283
284 #[test]
285 fn macro_succeeds_on_match() {
286 let c = capture();
287 Log::info("hello")
288 .component("svc")
289 .with("k", 1_u64)
290 .log_to(&c);
291 assert_logged!(c, level == LogLevel::INFO);
292 assert_logged!(c, contains "hello");
293 assert_logged!(c, component "svc");
294 assert_logged!(c, attribute "k" => 1_u64);
295 assert_logged!(c, len == 1);
296 }
297
298 #[test]
299 #[should_panic(expected = "expected a captured record at level")]
300 fn macro_panics_on_missing_level() {
301 let c = capture();
302 Log::info("x").log_to(&c);
303 assert_logged!(c, level == LogLevel::ERROR);
304 }
305
306 #[test]
307 #[should_panic(expected = "expected 3 captured records")]
308 fn macro_panics_on_wrong_count() {
309 let c = capture();
310 Log::info("x").log_to(&c);
311 assert_logged!(c, len == 3);
312 }
313
314 #[test]
315 fn capture_handle_is_cheap_to_clone() {
316 let a = capture();
317 let b = a.clone();
318 Log::info("x").log_to(&a);
319 assert_eq!(b.len(), 1, "clones share the same buffer");
320 }
321}