use std::collections::VecDeque;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Line {
pub at_ms: u64,
pub name: String,
pub text: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Group {
pub name: String,
pub at_ms: u64,
pub text: String,
}
#[derive(Debug, Clone)]
pub struct Buffer {
lines: VecDeque<Line>,
capacity: usize,
dropped: u64,
}
impl Buffer {
#[must_use]
pub fn new(capacity: usize) -> Self {
Self {
lines: VecDeque::new(),
capacity,
dropped: 0,
}
}
pub fn push(&mut self, line: Line) {
if self.lines.len() >= self.capacity {
if self.lines.pop_front().is_some() {
self.dropped += 1;
}
}
self.lines.push_back(line);
}
pub fn drain(&mut self, coalesce_ms: u64) -> Vec<Group> {
let mut groups = Vec::new();
while let Some(first) = self.lines.pop_front() {
let mut group = Group {
name: first.name,
at_ms: first.at_ms,
text: first.text,
};
while let Some(next) = self.lines.front() {
let in_window = next.at_ms < group.at_ms + coalesce_ms;
if next.name != group.name || !in_window {
break;
}
let next = self.lines.pop_front().expect("front just matched Some");
group.text.push('\n');
group.text.push_str(&next.text);
}
groups.push(group);
}
groups
}
pub fn take_dropped(&mut self) -> u64 {
core::mem::take(&mut self.dropped)
}
}
#[cfg(test)]
mod tests {
use super::{Buffer, Line};
fn line(at_ms: u64, name: &str, text: &str) -> Line {
Line {
at_ms,
name: name.to_owned(),
text: text.to_owned(),
}
}
#[test]
fn lines_inside_the_window_join_into_one_group() {
let mut buffer = Buffer::new(100);
buffer.push(line(1_000, "web", "first"));
buffer.push(line(1_400, "web", "second"));
let groups = buffer.drain(1_000);
assert_eq!(groups.len(), 1);
assert_eq!(groups[0].text, "first\nsecond");
}
#[test]
fn a_line_past_the_window_starts_a_new_group() {
let mut buffer = Buffer::new(100);
buffer.push(line(1_000, "web", "first"));
buffer.push(line(2_000, "web", "second"));
let groups = buffer.drain(1_000);
assert_eq!(
groups.len(),
2,
"the window is half open: [at, at + coalesce)"
);
}
#[test]
fn drain_returns_every_group_not_one() {
let mut buffer = Buffer::new(100);
for i in 0..5 {
buffer.push(line(i * 5_000, "web", "line"));
}
assert_eq!(buffer.drain(1_000).len(), 5);
assert!(
buffer.drain(1_000).is_empty(),
"drain leaves the buffer empty"
);
}
#[test]
fn two_sheep_in_one_window_do_not_share_a_group() {
let mut buffer = Buffer::new(100);
buffer.push(line(1_000, "web", "from web"));
buffer.push(line(1_100, "api", "from api"));
let groups = buffer.drain(1_000);
assert_eq!(
groups.len(),
2,
"a group carries one sheep's name, so it holds one sheep's lines"
);
}
#[test]
fn over_capacity_the_oldest_lines_go_and_the_count_is_kept() {
let mut buffer = Buffer::new(3);
for i in 0..5 {
buffer.push(line(i * 5_000, "web", "line"));
}
assert_eq!(buffer.take_dropped(), 2);
assert_eq!(buffer.take_dropped(), 0, "taking the count clears it");
assert_eq!(buffer.drain(1_000).len(), 3);
}
#[test]
fn the_first_push_into_a_zero_capacity_buffer_reports_no_drop() {
let mut buffer = Buffer::new(0);
buffer.push(line(0, "web", "line"));
assert_eq!(
buffer.take_dropped(),
0,
"the deque was empty, so pop_front had nothing to evict"
);
}
}