Skip to main content

term_test_support/
log_capture.rs

1//! Per-run diagnostic log capture for test harnesses.
2//!
3//! Spawned daemons discard stdout/stderr by design (a detached daemon must
4//! not rely on drained pipes), which makes flaky failures undiagnosable.
5//! Harnesses use [`apply_test_logging`] to point each spawned process at
6//! `TERM_WM_LOG_FILE` (see `term-wm-config`) under a stable root directory
7//! that CI can archive on failure.
8
9use std::path::PathBuf;
10use std::sync::OnceLock;
11
12/// Env var overriding the capture root (used by CI to pin an archivable
13/// path). Mirrors `TERM_WM_*` naming; defined locally because this crate has
14/// no dependency on `term-wm-config`.
15const TEST_LOG_DIR_ENV_VAR: &str = "TERM_WM_TEST_LOG_DIR";
16
17/// Stable parent directory name under the OS temp dir. CI archives this
18/// whole directory on failure; each test process creates its own uniquely
19/// named subdirectory beneath it.
20const LOG_ROOT_DIR_NAME: &str = "term-wm-test-logs";
21
22/// Verbosity for captured processes. Daemons default to INFO without
23/// `RUST_LOG`; debug level makes startup/binding traces visible.
24const TEST_LOG_FILTER: &str = "debug";
25
26fn capture_root() -> &'static PathBuf {
27    static ROOT: OnceLock<PathBuf> = OnceLock::new();
28    ROOT.get_or_init(|| match std::env::var_os(TEST_LOG_DIR_ENV_VAR) {
29        Some(dir) => PathBuf::from(dir),
30        None => {
31            let unique = format!(
32                "{}-{}",
33                std::process::id(),
34                std::time::SystemTime::now()
35                    .duration_since(std::time::UNIX_EPOCH)
36                    .map(|d| d.as_nanos())
37                    .unwrap_or(0)
38            );
39            std::env::temp_dir().join(LOG_ROOT_DIR_NAME).join(unique)
40        }
41    })
42}
43
44/// The directory this test process writes captured logs into. Created lazily
45/// by [`test_log_file`].
46pub fn test_log_dir() -> &'static std::path::Path {
47    capture_root().as_path()
48}
49
50/// A unique log file path under [`test_log_dir`] for the given label (e.g. a
51/// gateway name slug). Creates the directory on demand.
52pub fn test_log_file(label: &str) -> PathBuf {
53    let dir = capture_root();
54    let _ = std::fs::create_dir_all(dir);
55    let slug: String = label.replace('/', "-");
56    dir.join(format!("{slug}.log"))
57}
58
59/// Configure a spawned test process to write its diagnostics to a unique
60/// file under [`test_log_dir`]: sets `TERM_WM_LOG_FILE` (file logging) and
61/// `RUST_LOG=debug` (verbose enough to see bind/startup traces).
62pub fn apply_test_logging(cmd: &mut std::process::Command, label: &str) {
63    cmd.env("TERM_WM_LOG_FILE", test_log_file(label));
64    cmd.env("RUST_LOG", TEST_LOG_FILTER);
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70
71    #[test]
72    fn log_files_are_unique_per_label_and_under_root() {
73        let a = test_log_file("gw/one");
74        let b = test_log_file("gw-two");
75        assert!(a.starts_with(test_log_dir()));
76        assert!(b.starts_with(test_log_dir()));
77        assert_ne!(a, b);
78        assert!(a.to_string_lossy().ends_with("gw-one.log"));
79        // Directory must have been created.
80        assert!(test_log_dir().is_dir());
81    }
82
83    #[test]
84    fn apply_sets_both_env_vars() {
85        let mut cmd = std::process::Command::new("true");
86        apply_test_logging(&mut cmd, "probe");
87        let file = test_log_file("probe");
88        let env_of = |key: &str| {
89            cmd.get_envs()
90                .find(|(k, _)| k.to_string_lossy() == key)
91                .and_then(|(_, v)| v)
92                .map(std::ffi::OsString::from)
93        };
94        assert_eq!(
95            env_of("TERM_WM_LOG_FILE"),
96            Some(std::ffi::OsString::from(file))
97        );
98        assert_eq!(
99            env_of("RUST_LOG"),
100            Some(std::ffi::OsString::from(TEST_LOG_FILTER))
101        );
102    }
103}