1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
//! Process-global environment guard for lib tests.
//!
//! Lib tests share one process. Every test that mutates `HOME`, an `OO_*`
//! variable or the cwd must hold [`env_guard`] for the whole mutation; otherwise
//! two modules race (one removes `HOME` while another sets it). The guard
//! serializes mutators only. Readers are not covered, so a test that depends on
//! the exact value of one of these must hold the guard too.
//!
//! The guard snapshots the tracked variables and the cwd when acquired and
//! restores them on drop, so a panicking test cannot leak state to the next
//! holder. Restore runs before the lock is released, which makes recovery from a
//! poisoned lock safe.
//!
//! No unit test checks that the lock serializes: a single-threaded test passes
//! trivially. The stress loop at `RUST_TEST_THREADS=64` is the verification.
use std::ffi::OsString;
use std::path::PathBuf;
use std::sync::{Mutex, MutexGuard};
static ENV_LOCK: Mutex<()> = Mutex::new(());
/// Variables that lib tests mutate; restored when an [`EnvGuard`] drops.
const TRACKED_VARS: &[&str] = &[
"HOME",
"OO_CONFIG_DIR",
"OO_DATA_DIR",
"OO_CLAUDE_DIR",
"OO_PI_EXTENSIONS_DIR",
"TMPDIR",
];
/// Holds the shared environment lock; the environment is restored when it drops.
pub struct EnvGuard {
// Declared first, so it drops first: state is restored before the lock is released.
_restore: Restore,
_lock: MutexGuard<'static, ()>,
}
/// Acquires the shared environment guard and snapshots the tracked state.
///
/// Create temp dirs BEFORE taking the guard, so cwd/env are restored before they
/// are deleted: locals drop in reverse declaration order, and a tempdir dropped
/// while the cwd still points into it leaks a deleted cwd to child processes.
pub fn env_guard() -> EnvGuard {
let lock = ENV_LOCK
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
EnvGuard {
_restore: Restore::capture(),
_lock: lock,
}
}
/// Snapshot of the tracked environment and cwd, restored on drop. Holds no lock,
/// so the restore logic can be exercised on its own.
struct Restore {
saved_vars: Vec<(&'static str, Option<OsString>)>,
saved_cwd: Option<PathBuf>,
}
impl Restore {
fn capture() -> Self {
Self {
saved_vars: TRACKED_VARS
.iter()
.map(|&key| (key, std::env::var_os(key)))
.collect(),
saved_cwd: std::env::current_dir().ok(),
}
}
}
impl Drop for Restore {
fn drop(&mut self) {
for (key, value) in &self.saved_vars {
// SAFETY: the shared lock is held by the owning `EnvGuard`, so no other
// lib test is mutating the process environment concurrently.
unsafe {
match value {
Some(v) => std::env::set_var(key, v),
None => std::env::remove_var(key),
}
}
}
if let Some(cwd) = &self.saved_cwd {
if let Err(e) = std::env::set_current_dir(cwd) {
// A leaked cwd is inherited by every later child process; fail the
// test that leaked it instead of letting the damage spread.
let msg = format!("test_env: could not restore cwd {}: {e}", cwd.display());
if std::thread::panicking() {
eprintln!("{msg}");
} else {
panic!("{msg}");
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::{Restore, env_guard};
use std::panic::{AssertUnwindSafe, catch_unwind};
#[test]
fn restore_reverts_tracked_var_when_unwinding() {
// Holds the shared lock so no other test changes OO_CLAUDE_DIR meanwhile.
let _env = env_guard();
let before = std::env::var_os("OO_CLAUDE_DIR");
let result = catch_unwind(AssertUnwindSafe(|| {
let _restore = Restore::capture();
// SAFETY: this test holds the shared lock.
unsafe { std::env::set_var("OO_CLAUDE_DIR", "/nonexistent-env-guard-probe") };
panic!("simulated failure mid-mutation");
}));
assert!(result.is_err(), "the closure must unwind");
assert_eq!(
std::env::var_os("OO_CLAUDE_DIR"),
before,
"an unwinding test must not leak its environment mutation"
);
}
}