relux_runtime/observe/
shell_log.rs1use std::fs::File;
2use std::io::BufWriter;
3use std::io::Write;
4use std::io::{self};
5use std::path::Path;
6use std::time::Instant;
7
8pub struct ShellLogger {
9 stdin_raw: BufWriter<File>,
10 stdin_log: BufWriter<File>,
11 stdout_raw: BufWriter<File>,
12 stdout_log: BufWriter<File>,
13 test_start: Instant,
14 stdin_at_line_start: bool,
15 stdout_at_line_start: bool,
16}
17
18impl ShellLogger {
19 pub fn create(log_dir: &Path, scoped_name: &str, test_start: Instant) -> io::Result<Self> {
20 std::fs::create_dir_all(log_dir)?;
21 let open = |suffix: &str| -> io::Result<BufWriter<File>> {
22 let path = log_dir.join(format!("{scoped_name}.{suffix}"));
23 Ok(BufWriter::new(File::create(path)?))
24 };
25 Ok(Self {
26 stdin_raw: open("stdin.raw")?,
27 stdin_log: open("stdin.log")?,
28 stdout_raw: open("stdout.raw")?,
29 stdout_log: open("stdout.log")?,
30 test_start,
31 stdin_at_line_start: true,
32 stdout_at_line_start: true,
33 })
34 }
35
36 pub fn log_stdin(&mut self, data: &[u8]) {
37 let _ = self.stdin_raw.write_all(data);
38 let _ = self.stdin_raw.flush();
39 self.stdin_at_line_start = write_timestamped(
40 &mut self.stdin_log,
41 data,
42 self.stdin_at_line_start,
43 &self.test_start,
44 );
45 let _ = self.stdin_log.flush();
46 }
47
48 pub fn log_stdout(&mut self, data: &[u8]) {
49 let _ = self.stdout_raw.write_all(data);
50 let _ = self.stdout_raw.flush();
51 self.stdout_at_line_start = write_timestamped(
52 &mut self.stdout_log,
53 data,
54 self.stdout_at_line_start,
55 &self.test_start,
56 );
57 let _ = self.stdout_log.flush();
58 }
59}
60
61fn write_timestamped(
64 w: &mut BufWriter<File>,
65 data: &[u8],
66 at_line_start: bool,
67 test_start: &Instant,
68) -> bool {
69 if data.is_empty() {
70 return at_line_start;
71 }
72
73 let prefix = timestamp_prefix(test_start);
74 let mut pos = 0;
75
76 if at_line_start {
77 let _ = w.write_all(prefix.as_bytes());
78 }
79
80 while pos < data.len() {
81 if let Some(nl) = data[pos..].iter().position(|&b| b == b'\n') {
82 let end = pos + nl + 1;
83 let _ = w.write_all(&data[pos..end]);
84 if end < data.len() {
85 let _ = w.write_all(prefix.as_bytes());
86 }
87 pos = end;
88 } else {
89 let _ = w.write_all(&data[pos..]);
90 pos = data.len();
91 }
92 }
93
94 data.last() == Some(&b'\n')
95}
96
97fn timestamp_prefix(test_start: &Instant) -> String {
98 let elapsed = test_start.elapsed();
99 let secs = elapsed.as_secs();
100 let millis = elapsed.subsec_millis();
101 format!("[+{secs}.{millis:03}s] ")
102}