1use std::fmt::Write as _;
8use std::path::PathBuf;
9use std::sync::{Arc, Mutex};
10use std::time::Instant;
11
12#[derive(Clone)]
13pub struct Evidence {
14 inner: Arc<Mutex<Inner>>,
15}
16
17struct Inner {
18 test: String,
19 started: Instant,
20 entries: Vec<Entry>,
21}
22
23struct Entry {
24 at_us: u128,
25 source: &'static str,
26 line: String,
27}
28
29impl Evidence {
30 pub fn new(test: &str) -> Self {
31 Evidence {
32 inner: Arc::new(Mutex::new(Inner {
33 test: test.to_owned(),
34 started: Instant::now(),
35 entries: Vec::new(),
36 })),
37 }
38 }
39
40 pub fn record(&self, source: &'static str, line: impl Into<String>) {
45 let mut inner = self.inner.lock().unwrap();
46 let at_us = inner.started.elapsed().as_micros();
47 inner.entries.push(Entry { at_us, source, line: line.into() });
48 }
49
50 pub fn is_empty(&self) -> bool {
51 self.inner.lock().unwrap().entries.is_empty()
52 }
53
54 pub fn tail(&self, n: usize) -> String {
56 let inner = self.inner.lock().unwrap();
57 let skip = inner.entries.len().saturating_sub(n);
58 let mut out = String::new();
59 for e in &inner.entries[skip..] {
60 let _ = writeln!(out, "[{:>10.3}ms {}] {}", e.at_us as f64 / 1000.0, e.source, e.line);
61 }
62 out
63 }
64
65 pub fn persist(&self, dir: &std::path::Path) -> std::io::Result<PathBuf> {
67 let inner = self.inner.lock().unwrap();
68 std::fs::create_dir_all(dir)?;
69 let path = dir.join(format!("{}.evidence.log", inner.test.replace(['/', ':'], "_")));
70 let mut out = String::new();
71 for e in &inner.entries {
72 let _ = writeln!(out, "[{:>10.3}ms {}] {}", e.at_us as f64 / 1000.0, e.source, e.line);
73 }
74 std::fs::write(&path, out)?;
75 Ok(path)
76 }
77}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82
83 #[test]
84 fn tail_returns_last_lines() {
85 let ev = Evidence::new("t");
86 for i in 0..10 {
87 ev.record("test", format!("line {i}"));
88 }
89 let tail = ev.tail(3);
90 assert!(tail.contains("line 7") && tail.contains("line 9"));
91 assert!(!tail.contains("line 6"));
92 }
93
94 #[test]
95 fn persist_writes_file() {
96 let ev = Evidence::new("suite/case:1");
97 ev.record("x", "hello");
98 let dir = std::env::temp_dir().join("banc-evidence-test");
99 let path = ev.persist(&dir).unwrap();
100 let content = std::fs::read_to_string(&path).unwrap();
101 assert!(content.contains("hello"));
102 std::fs::remove_file(path).ok();
103 }
104}