use std::collections::HashMap;
use std::io;
use std::sync::{Arc, Mutex};
use std::thread;
use ticklog::{info, Backpressure, Level, LogSink};
const THREADS: usize = 8;
const RECORDS_PER_THREAD: usize = 1_000;
struct CaptureSink {
lines: Arc<Mutex<Vec<String>>>,
}
impl LogSink for CaptureSink {
fn accept(&mut self, line: &[u8], _level: Level) -> io::Result<()> {
self.lines
.lock()
.unwrap()
.push(String::from_utf8_lossy(line).into_owned());
Ok(())
}
}
fn parse_worker_seq(line: &str) -> Option<(usize, usize)> {
let after = line.split("worker=").nth(1)?;
let mut parts = after.split(" seq=");
let worker = parts.next()?.trim().parse().ok()?;
let seq = parts.next()?.trim().parse().ok()?;
Some((worker, seq))
}
#[test]
fn each_thread_sequence_arrives_ordered_and_complete() {
let lines = Arc::new(Mutex::new(Vec::new()));
let sink = CaptureSink {
lines: Arc::clone(&lines),
};
let guard = ticklog::builder()
.sink(sink)
.max_level(Level::Info)
.backpressure(Backpressure::Block)
.build()
.expect("first build in a fresh process must succeed");
let handles: Vec<_> = (0..THREADS)
.map(|worker| {
thread::spawn(move || {
for seq in 0..RECORDS_PER_THREAD {
info!("worker={} seq={}", worker as u64, seq as u64);
}
})
})
.collect();
for handle in handles {
handle.join().expect("worker thread must not panic");
}
drop(guard);
let captured = lines.lock().unwrap();
let mut per_worker: HashMap<usize, Vec<usize>> = HashMap::new();
for line in captured.iter() {
let (worker, seq) =
parse_worker_seq(line).unwrap_or_else(|| panic!("unparseable line: {line:?}"));
per_worker.entry(worker).or_default().push(seq);
}
assert_eq!(
per_worker.len(),
THREADS,
"every worker must contribute records"
);
let expected: Vec<usize> = (0..RECORDS_PER_THREAD).collect();
for worker in 0..THREADS {
let seqs = per_worker
.get(&worker)
.unwrap_or_else(|| panic!("worker {worker} produced no records"));
assert_eq!(
seqs, &expected,
"worker {worker} records out of order or incomplete"
);
}
}