use std::collections::VecDeque;
use std::fmt;
use std::sync::{Mutex, MutexGuard, PoisonError};
use std::time::Duration;
use super::Level;
pub const CONSOLE_TARGET: &str = "console";
pub const CONSOLE_RING_LINES: usize = 1024;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Record {
pub sequence: u64,
pub level: Level,
pub target: String,
pub message: String,
pub elapsed: Duration,
}
#[derive(Debug)]
struct Ring {
records: VecDeque<Record>,
next_sequence: u64,
}
impl Ring {
const fn new() -> Self {
Self {
records: VecDeque::new(),
next_sequence: 0,
}
}
fn push(&mut self, level: Level, target: &str, message: String, elapsed: Duration) {
if self.records.len() >= CONSOLE_RING_LINES {
self.records.pop_front();
}
let sequence = self.next_sequence;
self.next_sequence += 1;
self.records.push_back(Record {
sequence,
level,
target: target.to_owned(),
message,
elapsed,
});
}
fn since(&self, from: u64) -> Vec<Record> {
let at = self
.records
.partition_point(|record| record.sequence < from);
self.records.iter().skip(at).cloned().collect()
}
}
static RING: Mutex<Ring> = Mutex::new(Ring::new());
fn ring() -> MutexGuard<'static, Ring> {
RING.lock().unwrap_or_else(PoisonError::into_inner)
}
pub fn push(level: Level, target: &str, elapsed: Duration, args: fmt::Arguments<'_>) {
let message = args.to_string();
ring().push(level, target, message, elapsed);
}
pub fn print(line: &str) {
super::__emit(Level::Info, CONSOLE_TARGET, format_args!("{line}"));
}
#[must_use]
pub fn snapshot() -> Vec<Record> {
snapshot_since(0)
}
#[must_use]
pub fn snapshot_since(from: u64) -> Vec<Record> {
ring().since(from)
}
#[cfg(test)]
mod tests {
use super::*;
fn fill(ring: &mut Ring, count: usize) {
for i in 0..count {
ring.push(
Level::Info,
"crcbl_core::log::console",
format!("line {i}"),
Duration::from_millis(i as u64),
);
}
}
#[test]
fn past_its_depth_the_oldest_records_are_the_ones_that_go() {
let overflow = 3;
let mut ring = Ring::new();
fill(&mut ring, CONSOLE_RING_LINES + overflow);
let held = ring.since(0);
assert_eq!(held.len(), CONSOLE_RING_LINES, "the bound did not hold");
assert_eq!(
held[0].message,
format!("line {overflow}"),
"the oldest three should have gone, not the newest",
);
assert_eq!(
held.last().expect("the ring is not empty").message,
format!("line {}", CONSOLE_RING_LINES + overflow - 1),
);
}
#[test]
fn sequences_stay_contiguous_across_the_drop() {
let overflow = 3;
let mut ring = Ring::new();
fill(&mut ring, CONSOLE_RING_LINES + overflow);
let held = ring.since(0);
let first = held[0].sequence;
assert_eq!(
first, overflow as u64,
"the sequence must count every record pushed, dropped ones included",
);
for (step, record) in held.iter().enumerate() {
assert_eq!(
record.sequence,
first + step as u64,
"a gap or a repeat at {step}",
);
}
}
#[test]
fn a_cursor_takes_the_newer_records_and_nothing_else() {
let mut ring = Ring::new();
fill(&mut ring, 5);
let all = ring.since(0);
assert_eq!(all.len(), 5);
let newer = ring.since(3);
assert_eq!(
newer.iter().map(|r| r.sequence).collect::<Vec<_>>(),
[3, 4],
"only the records at or after the cursor",
);
assert_eq!(newer[0].message, "line 3");
assert!(
ring.since(5).is_empty(),
"a cursor past the end has nothing to hand back",
);
}
#[test]
fn a_record_keeps_its_level_target_and_elapsed() {
let mut ring = Ring::new();
ring.push(
Level::Warn,
CONSOLE_TARGET,
"something to say".to_owned(),
Duration::from_millis(1250),
);
let held = ring.since(0);
let record = held.first().expect("the record was pushed");
assert_eq!(record.level, Level::Warn);
assert_eq!(record.target, "console");
assert_eq!(record.message, "something to say");
assert_eq!(record.elapsed, Duration::from_millis(1250));
}
#[test]
fn the_process_ring_takes_what_push_is_given() {
let target = "crcbl_core::log::console::the_process_ring_takes_what_push_is_given";
push(
Level::Error,
target,
Duration::from_secs(2),
format_args!("a record from {}", "the shared push"),
);
let mine: Vec<Record> = snapshot()
.into_iter()
.filter(|record| record.target == target)
.collect();
assert_eq!(mine.len(), 1, "{mine:?}");
assert_eq!(mine[0].message, "a record from the shared push");
assert_eq!(mine[0].level, Level::Error);
assert_eq!(mine[0].elapsed, Duration::from_secs(2));
}
}