use std::collections::BTreeMap;
use std::path::Path;
use crate::model::log::{LogEvent, Severity};
use crate::SondaError;
use super::LogGenerator;
pub struct LogReplayGenerator {
lines: Vec<String>,
}
impl LogReplayGenerator {
pub fn from_file(path: &Path) -> Result<Self, SondaError> {
let content = std::fs::read_to_string(path).map_err(SondaError::Sink)?;
let lines: Vec<String> = content
.lines()
.map(|l| l.to_string())
.filter(|l| !l.trim().is_empty())
.collect();
if lines.is_empty() {
return Err(SondaError::Config(format!(
"replay file {:?} contains no lines",
path
)));
}
Ok(Self { lines })
}
pub fn from_lines(lines: Vec<String>) -> Result<Self, SondaError> {
if lines.is_empty() {
return Err(SondaError::Config(
"replay generator requires at least one line".into(),
));
}
Ok(Self { lines })
}
}
impl LogGenerator for LogReplayGenerator {
fn generate(&self, tick: u64) -> LogEvent {
let line = &self.lines[(tick as usize) % self.lines.len()];
LogEvent::new(Severity::Info, line.clone(), BTreeMap::new())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
fn five_line_generator() -> LogReplayGenerator {
let lines: Vec<String> = (0..5).map(|i| format!("line-{i}")).collect();
LogReplayGenerator::from_lines(lines).expect("five lines should succeed")
}
#[test]
fn tick_zero_returns_first_line() {
let gen = five_line_generator();
assert_eq!(gen.generate(0).message, "line-0");
}
#[test]
fn tick_four_returns_fifth_line() {
let gen = five_line_generator();
assert_eq!(gen.generate(4).message, "line-4");
}
#[test]
fn tick_five_wraps_to_line_zero() {
let gen = five_line_generator();
assert_eq!(gen.generate(5).message, "line-0");
}
#[test]
fn tick_six_wraps_to_line_one() {
let gen = five_line_generator();
assert_eq!(gen.generate(6).message, "line-1");
}
#[test]
fn tick_ten_wraps_to_line_zero_again() {
let gen = five_line_generator();
assert_eq!(gen.generate(10).message, "line-0");
}
#[test]
fn severity_is_always_info() {
let gen = five_line_generator();
for tick in 0..15 {
assert_eq!(
gen.generate(tick).severity,
Severity::Info,
"severity at tick {tick} must be Info"
);
}
}
#[test]
fn fields_are_always_empty() {
let gen = five_line_generator();
for tick in 0..15 {
assert!(
gen.generate(tick).fields.is_empty(),
"fields at tick {tick} must be empty"
);
}
}
#[test]
fn from_lines_empty_returns_error() {
let result = LogReplayGenerator::from_lines(vec![]);
assert!(result.is_err(), "from_lines([]) must return Err, not Ok");
}
#[test]
fn from_file_truly_empty_file_returns_error() {
let tmp = NamedTempFile::new().expect("create temp file");
let result = LogReplayGenerator::from_file(tmp.path());
assert!(
result.is_err(),
"from_file with zero-byte file must return Err"
);
}
#[test]
fn from_file_only_empty_lines_returns_error() {
let mut tmp = NamedTempFile::new().expect("create temp file");
writeln!(tmp, "").expect("write empty line");
writeln!(tmp, "").expect("write empty line");
let result = LogReplayGenerator::from_file(tmp.path());
assert!(
result.is_err(),
"from_file with only empty lines must return Err"
);
}
#[test]
fn from_file_missing_file_returns_error() {
let result = LogReplayGenerator::from_file(std::path::Path::new(
"/nonexistent/path/that/does/not/exist.log",
));
assert!(result.is_err(), "missing file must return Err");
}
#[test]
fn from_file_five_lines_cycles_correctly() {
let mut tmp = NamedTempFile::new().expect("create temp file");
for i in 0..5 {
writeln!(tmp, "file-line-{i}").expect("write line");
}
let gen = LogReplayGenerator::from_file(tmp.path()).expect("five-line file should succeed");
for tick in 0..5u64 {
assert_eq!(
gen.generate(tick).message,
format!("file-line-{tick}"),
"tick {tick} should return file-line-{tick}"
);
}
assert_eq!(
gen.generate(5).message,
"file-line-0",
"tick 5 should wrap to file-line-0"
);
}
#[test]
fn from_file_skips_blank_lines() {
let mut tmp = NamedTempFile::new().expect("create temp file");
writeln!(tmp, "alpha").expect("write");
writeln!(tmp).expect("write blank");
writeln!(tmp, "beta").expect("write");
writeln!(tmp).expect("write blank");
writeln!(tmp, "gamma").expect("write");
let gen = LogReplayGenerator::from_file(tmp.path()).expect("non-empty file");
assert_eq!(gen.generate(0).message, "alpha");
assert_eq!(gen.generate(1).message, "beta");
assert_eq!(gen.generate(2).message, "gamma");
assert_eq!(gen.generate(3).message, "alpha");
}
#[test]
fn large_tick_does_not_panic() {
let gen = five_line_generator();
let _ = gen.generate(u64::MAX);
let _ = gen.generate(u64::MAX - 1);
}
#[test]
fn same_tick_always_returns_same_message() {
let gen = five_line_generator();
for tick in 0..20 {
assert_eq!(
gen.generate(tick).message,
gen.generate(tick).message,
"generate(tick) must be deterministic"
);
}
}
fn assert_send_sync<T: Send + Sync>() {}
#[test]
fn log_replay_generator_is_send_and_sync() {
assert_send_sync::<LogReplayGenerator>();
}
}