Skip to main content

relux_runtime/observe/
shell_log.rs

1use 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
61/// Writes data with timestamp prefixes inserted only at the beginning of lines.
62/// Returns whether the stream is at a line start after writing (i.e. data ended with `\n`).
63fn 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}