1use serde::{Deserialize, Serialize};
7use std::cell::RefCell;
8use std::sync::{Arc, Mutex};
9use std::time::Instant;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
13pub enum LogLevel {
14 Trace,
16 Debug,
18 Info,
20 Warn,
22 Error,
24}
25
26impl std::fmt::Display for LogLevel {
27 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28 match self {
29 LogLevel::Trace => write!(f, "TRACE"),
30 LogLevel::Debug => write!(f, "DEBUG"),
31 LogLevel::Info => write!(f, "INFO"),
32 LogLevel::Warn => write!(f, "WARN"),
33 LogLevel::Error => write!(f, "ERROR"),
34 }
35 }
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct LogEntry {
41 pub level: LogLevel,
43 pub message: String,
45 pub target: String,
47 pub timestamp_ms: u64,
49 pub fields: std::collections::HashMap<String, serde_json::Value>,
51}
52
53impl LogEntry {
54 pub fn new(level: LogLevel, message: impl Into<String>) -> Self {
56 Self {
57 level,
58 message: message.into(),
59 target: String::new(),
60 timestamp_ms: 0,
61 fields: std::collections::HashMap::new(),
62 }
63 }
64
65 pub fn with_target(mut self, target: impl Into<String>) -> Self {
67 self.target = target.into();
68 self
69 }
70
71 pub fn with_timestamp_ms(mut self, timestamp_ms: u64) -> Self {
73 self.timestamp_ms = timestamp_ms;
74 self
75 }
76
77 pub fn with_field(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
79 self.fields.insert(key.into(), value);
80 self
81 }
82}
83
84#[derive(Clone)]
88pub struct LogCollector {
89 entries: Arc<Mutex<Vec<LogEntry>>>,
90 start_time: Arc<Mutex<Option<Instant>>>,
91 min_level: LogLevel,
92}
93
94impl LogCollector {
95 pub fn new(min_level: LogLevel) -> Self {
97 Self {
98 entries: Arc::new(Mutex::new(Vec::new())),
99 start_time: Arc::new(Mutex::new(None)),
100 min_level,
101 }
102 }
103
104 pub fn start(&self) {
106 {
107 let mut start = self.start_time.lock().unwrap();
108 *start = Some(Instant::now());
109 }
110 self.entries.lock().unwrap().clear();
111 }
112
113 pub fn log(&self, level: LogLevel, message: impl Into<String>) {
115 if level < self.min_level {
116 return;
117 }
118
119 let timestamp_ms = self
120 .start_time
121 .lock()
122 .unwrap()
123 .map(|start| start.elapsed().as_millis().min(u128::from(u64::MAX)) as u64)
124 .unwrap_or(0);
125
126 let entry = LogEntry::new(level, message).with_timestamp_ms(timestamp_ms);
127
128 self.entries.lock().unwrap().push(entry);
129 }
130
131 pub fn log_with_target(&self, level: LogLevel, target: &str, message: impl Into<String>) {
133 if level < self.min_level {
134 return;
135 }
136
137 let timestamp_ms = self
138 .start_time
139 .lock()
140 .unwrap()
141 .map(|start| start.elapsed().as_millis().min(u128::from(u64::MAX)) as u64)
142 .unwrap_or(0);
143
144 let entry = LogEntry::new(level, message)
145 .with_target(target)
146 .with_timestamp_ms(timestamp_ms);
147
148 self.entries.lock().unwrap().push(entry);
149 }
150
151 pub fn drain(&self) -> Vec<LogEntry> {
153 std::mem::take(&mut *self.entries.lock().unwrap())
154 }
155
156 pub fn len(&self) -> usize {
158 self.entries.lock().unwrap().len()
159 }
160
161 pub fn is_empty(&self) -> bool {
163 self.entries.lock().unwrap().is_empty()
164 }
165
166 pub fn trace(&self, message: impl Into<String>) {
168 self.log(LogLevel::Trace, message);
169 }
170
171 pub fn debug(&self, message: impl Into<String>) {
173 self.log(LogLevel::Debug, message);
174 }
175
176 pub fn info(&self, message: impl Into<String>) {
178 self.log(LogLevel::Info, message);
179 }
180
181 pub fn warn(&self, message: impl Into<String>) {
183 self.log(LogLevel::Warn, message);
184 }
185
186 pub fn error(&self, message: impl Into<String>) {
188 self.log(LogLevel::Error, message);
189 }
190}
191
192impl Default for LogCollector {
193 fn default() -> Self {
194 Self::new(LogLevel::Info)
195 }
196}
197
198impl std::fmt::Debug for LogCollector {
199 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
200 f.debug_struct("LogCollector")
201 .field("entries_count", &self.len())
202 .field("min_level", &self.min_level)
203 .finish()
204 }
205}
206
207#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
213pub enum TestEventKind {
214 Phase,
216 Assertion,
218 RuntimeEvent,
220 Warning,
222 Checkpoint,
224}
225
226#[derive(Debug, Clone, Serialize, Deserialize)]
228pub struct TestEvent {
229 pub kind: TestEventKind,
231 pub name: String,
233 pub timestamp_ms: u64,
235 pub details: serde_json::Value,
237}
238
239impl TestEvent {
240 pub fn new(
242 kind: TestEventKind,
243 name: impl Into<String>,
244 timestamp_ms: u64,
245 details: serde_json::Value,
246 ) -> Self {
247 Self {
248 kind,
249 name: name.into(),
250 timestamp_ms,
251 details,
252 }
253 }
254}
255
256#[derive(Debug)]
257struct ConformanceTestLogState {
258 test_name: String,
259 spec_section: String,
260 start_time: Instant,
261 events: Vec<TestEvent>,
262}
263
264#[derive(Clone)]
266pub struct ConformanceTestLogger {
267 inner: Arc<Mutex<ConformanceTestLogState>>,
268}
269
270impl ConformanceTestLogger {
271 pub fn new(test_name: impl Into<String>, spec_section: impl Into<String>) -> Self {
273 Self {
274 inner: Arc::new(Mutex::new(ConformanceTestLogState {
275 test_name: test_name.into(),
276 spec_section: spec_section.into(),
277 start_time: Instant::now(),
278 events: Vec::new(),
279 })),
280 }
281 }
282
283 pub fn phase(&self, name: &'static str) {
285 self.record(TestEventKind::Phase, name, serde_json::Value::Null);
286 }
287
288 #[track_caller]
290 pub fn assert_with_context(&self, condition: bool, description: &str) {
291 let location = std::panic::Location::caller().to_string();
292 let details = serde_json::json!({
293 "passed": condition,
294 "location": location,
295 });
296 self.record(TestEventKind::Assertion, description, details);
297 assert!(condition, "Conformance assertion failed: {}", description);
298 }
299
300 pub fn runtime_event(&self, description: &str, details: serde_json::Value) {
302 self.record(TestEventKind::RuntimeEvent, description, details);
303 }
304
305 pub fn warning(&self, message: &str) {
307 self.record(TestEventKind::Warning, message, serde_json::Value::Null);
308 }
309
310 pub fn checkpoint(&self, name: &str, data: serde_json::Value) {
312 self.record(TestEventKind::Checkpoint, name, data);
313 }
314
315 pub fn events(&self) -> Vec<TestEvent> {
317 self.inner
318 .lock()
319 .expect("conformance log lock poisoned")
320 .events
321 .clone()
322 }
323
324 pub fn test_name(&self) -> String {
326 self.inner
327 .lock()
328 .expect("conformance log lock poisoned")
329 .test_name
330 .clone()
331 }
332
333 pub fn spec_section(&self) -> String {
335 self.inner
336 .lock()
337 .expect("conformance log lock poisoned")
338 .spec_section
339 .clone()
340 }
341
342 fn record(&self, kind: TestEventKind, name: &str, details: serde_json::Value) {
343 let mut guard = self.inner.lock().expect("conformance log lock poisoned");
344 let timestamp_ms = guard
345 .start_time
346 .elapsed()
347 .as_millis()
348 .min(u128::from(u64::MAX)) as u64;
349 guard
350 .events
351 .push(TestEvent::new(kind, name, timestamp_ms, details));
352 }
353}
354
355thread_local! {
356 static CURRENT_TEST_LOGGER: RefCell<Option<ConformanceTestLogger>> =
357 const { RefCell::new(None) };
358}
359
360pub fn with_test_logger<T>(logger: &ConformanceTestLogger, f: impl FnOnce() -> T) -> T {
362 struct Guard {
363 prev: Option<ConformanceTestLogger>,
364 }
365
366 impl Drop for Guard {
367 fn drop(&mut self) {
368 let prev = self.prev.take();
369 CURRENT_TEST_LOGGER.with(|slot| {
370 *slot.borrow_mut() = prev;
371 });
372 }
373 }
374
375 let prev = CURRENT_TEST_LOGGER.with(|slot| slot.replace(Some(logger.clone())));
376 let _guard = Guard { prev };
377 f()
378}
379
380pub fn record_checkpoint(name: &str, data: serde_json::Value) {
382 CURRENT_TEST_LOGGER.with(|slot| {
383 if let Some(logger) = slot.borrow().as_ref() {
384 logger.checkpoint(name, data);
385 }
386 });
387}
388
389#[derive(Debug, Clone)]
391pub struct LogConfig {
392 pub min_level: LogLevel,
394 pub show_timestamps: bool,
396 pub show_targets: bool,
398 pub use_colors: bool,
400}
401
402impl Default for LogConfig {
403 fn default() -> Self {
404 Self {
405 min_level: LogLevel::Info,
406 show_timestamps: true,
407 show_targets: true,
408 use_colors: false,
409 }
410 }
411}
412
413impl LogConfig {
414 pub fn new() -> Self {
416 Self::default()
417 }
418
419 pub fn with_min_level(mut self, level: LogLevel) -> Self {
421 self.min_level = level;
422 self
423 }
424
425 pub fn with_timestamps(mut self, show: bool) -> Self {
427 self.show_timestamps = show;
428 self
429 }
430
431 pub fn with_targets(mut self, show: bool) -> Self {
433 self.show_targets = show;
434 self
435 }
436
437 pub fn with_colors(mut self, use_colors: bool) -> Self {
439 self.use_colors = use_colors;
440 self
441 }
442}
443
444pub fn format_entry(entry: &LogEntry, config: &LogConfig) -> String {
446 let mut parts = Vec::new();
447
448 if config.show_timestamps {
449 parts.push(format!("[{:>8}ms]", entry.timestamp_ms));
450 }
451
452 parts.push(format!("{:5}", entry.level));
453
454 if config.show_targets && !entry.target.is_empty() {
455 parts.push(format!("[{}]", entry.target));
456 }
457
458 parts.push(entry.message.clone());
459
460 if !entry.fields.is_empty() {
461 let fields: Vec<String> = entry
462 .fields
463 .iter()
464 .map(|(k, v)| format!("{}={}", k, v))
465 .collect();
466 parts.push(format!("{{{}}}", fields.join(", ")));
467 }
468
469 parts.join(" ")
470}
471
472pub fn print_logs(entries: &[LogEntry], config: &LogConfig) {
474 for entry in entries {
475 if entry.level >= config.min_level {
476 println!("{}", format_entry(entry, config));
477 }
478 }
479}
480
481#[cfg(test)]
482mod tests {
483 use super::*;
484
485 #[test]
486 fn log_level_ordering() {
487 assert!(LogLevel::Trace < LogLevel::Debug);
488 assert!(LogLevel::Debug < LogLevel::Info);
489 assert!(LogLevel::Info < LogLevel::Warn);
490 assert!(LogLevel::Warn < LogLevel::Error);
491 }
492
493 #[test]
494 fn log_collector_basic() {
495 let collector = LogCollector::new(LogLevel::Debug);
496 collector.start();
497
498 collector.trace("trace message"); collector.debug("debug message");
500 collector.info("info message");
501
502 let entries = collector.drain();
503 assert_eq!(entries.len(), 2);
504 assert_eq!(entries[0].message, "debug message");
505 assert_eq!(entries[1].message, "info message");
506 }
507
508 #[test]
509 fn log_collector_with_target() {
510 let collector = LogCollector::new(LogLevel::Info);
511 collector.start();
512
513 collector.log_with_target(LogLevel::Info, "test::module", "test message");
514
515 let entries = collector.drain();
516 assert_eq!(entries.len(), 1);
517 assert_eq!(entries[0].target, "test::module");
518 }
519
520 #[test]
521 fn log_entry_builder() {
522 let entry = LogEntry::new(LogLevel::Info, "message")
523 .with_target("target")
524 .with_timestamp_ms(100)
525 .with_field("key", serde_json::json!("value"));
526
527 assert_eq!(entry.level, LogLevel::Info);
528 assert_eq!(entry.message, "message");
529 assert_eq!(entry.target, "target");
530 assert_eq!(entry.timestamp_ms, 100);
531 assert_eq!(entry.fields.get("key"), Some(&serde_json::json!("value")));
532 }
533
534 #[test]
535 fn format_entry_basic() {
536 let entry = LogEntry::new(LogLevel::Info, "test message").with_timestamp_ms(42);
537
538 let config = LogConfig::new().with_timestamps(true).with_targets(false);
539
540 let formatted = format_entry(&entry, &config);
541 assert!(formatted.contains("42ms"));
542 assert!(formatted.contains("INFO"));
543 assert!(formatted.contains("test message"));
544 }
545
546 #[test]
547 fn log_collector_drain_clears() {
548 let collector = LogCollector::new(LogLevel::Info);
549 collector.start();
550
551 collector.info("message 1");
552 let entries = collector.drain();
553 assert_eq!(entries.len(), 1);
554
555 collector.info("message 2");
556 let entries = collector.drain();
557 assert_eq!(entries.len(), 1);
558 assert_eq!(entries[0].message, "message 2");
559 }
560
561 #[test]
562 fn log_config_builder() {
563 let config = LogConfig::new()
564 .with_min_level(LogLevel::Debug)
565 .with_timestamps(false)
566 .with_targets(true)
567 .with_colors(true);
568
569 assert_eq!(config.min_level, LogLevel::Debug);
570 assert!(!config.show_timestamps);
571 assert!(config.show_targets);
572 assert!(config.use_colors);
573 }
574}