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
/// Process-global lock for tests that mutate environment variables.
///
/// Rust unit tests run in parallel within the same process, so concurrent
/// `env::set_var` / `env::remove_var` calls race against each other.
/// All env-mutating tests must acquire this lock before touching env vars.
///
/// See <https://github.com/always-further/nono/issues/567> for the plan to
/// eliminate env var mutation from tests entirely.
#[allow(dead_code)]
pub static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
/// Restores a set of environment variables when dropped.
pub struct EnvVarGuard {
original: Vec<(&'static str, Option<String>)>,
}
#[allow(clippy::disallowed_methods)] // This IS the safe wrapper around env var mutation.
impl EnvVarGuard {
/// Set multiple env vars, capturing originals for restore on drop.
#[must_use]
pub fn set_all(vars: &[(&'static str, &str)]) -> Self {
let original = vars
.iter()
.map(|(key, _)| (*key, std::env::var(key).ok()))
.collect::<Vec<_>>();
for (key, value) in vars {
// SAFETY: all callers hold ENV_LOCK, preventing concurrent env mutation.
unsafe { std::env::set_var(key, value) };
}
Self { original }
}
/// Remove an env var mid-test (e.g. to test fallback behaviour).
///
/// Only keys passed to [`set_all`](Self::set_all) can be removed — the
/// guard restores their original values on drop. Panics if `key` is not
/// managed by this guard, since the removal would not be reverted.
pub fn remove(&self, key: &str) {
assert!(
self.original.iter().any(|(k, _)| *k == key),
"EnvVarGuard::remove called with unmanaged key: '{key}'. \
Only keys passed to set_all can be removed."
);
// SAFETY: callers hold ENV_LOCK.
unsafe { std::env::remove_var(key) };
}
}
#[allow(clippy::disallowed_methods)] // Restoring env vars is the other half of the safe wrapper.
impl Drop for EnvVarGuard {
fn drop(&mut self) {
for (key, value) in self.original.iter().rev() {
// SAFETY: drop runs while ENV_LOCK is still held by the test.
match value {
Some(value) => unsafe { std::env::set_var(key, value) },
None => unsafe { std::env::remove_var(key) },
}
}
}
}