Skip to main content

term_test_support/
env.rs

1//! Centralized RAII environment-variable mutation for tests.
2//!
3//! All test-side environment manipulation belongs here — ad-hoc guards and
4//! inline `unsafe { std::env::set_var }` blocks scattered across test files
5//! create uncoordinated global state mutations. Use [`EnvVarGuard`] instead.
6//!
7//! Safety contract: `std::env::{set_var, remove_var}` mutate process-global
8//! state and are thread-unsafe on some platforms. Callers MUST serialize
9//! multi-threaded environment mutation (`#[serial(...)]`) or run inside a
10//! process-isolated integration binary.
11
12use std::ffi::OsString;
13
14/// Mutates an environment variable for the duration of the guard,
15/// restoring the original state on drop.
16///
17/// - [`EnvVarGuard::set`]: ensures the variable is SET to a value
18/// - [`EnvVarGuard::removed`]: ensures the variable is UNSET
19pub struct EnvVarGuard {
20    key: OsString,
21    previous_value: Option<OsString>,
22}
23
24impl EnvVarGuard {
25    /// Sets `key` to `value`, remembering the previous state.
26    ///
27    /// # Safety contract
28    /// Caller must ensure multi-threaded environment mutation is guarded
29    /// via serial execution (`#[serial]`) or process-isolated integration
30    /// tests.
31    pub fn set<K, V>(key: K, value: V) -> Self
32    where
33        K: AsRef<std::ffi::OsStr>,
34        V: AsRef<std::ffi::OsStr>,
35    {
36        let key = key.as_ref().to_os_string();
37        let previous_value = std::env::var_os(&key);
38
39        // SAFETY: see type-level safety contract.
40        unsafe {
41            std::env::set_var(&key, value);
42        }
43
44        Self {
45            key,
46            previous_value,
47        }
48    }
49
50    /// Removes `key` from the environment, remembering the previous state.
51    ///
52    /// # Safety contract
53    /// Caller must ensure multi-threaded environment mutation is guarded
54    /// via serial execution (`#[serial]`) or process-isolated integration
55    /// tests.
56    pub fn removed<K>(key: K) -> Self
57    where
58        K: AsRef<std::ffi::OsStr>,
59    {
60        let key = key.as_ref().to_os_string();
61        let previous_value = std::env::var_os(&key);
62
63        // SAFETY: see type-level safety contract.
64        unsafe {
65            std::env::remove_var(&key);
66        }
67
68        Self {
69            key,
70            previous_value,
71        }
72    }
73}
74
75impl Drop for EnvVarGuard {
76    fn drop(&mut self) {
77        // SAFETY: see type-level safety contract.
78        unsafe {
79            match &self.previous_value {
80                Some(value) => std::env::set_var(&self.key, value),
81                None => std::env::remove_var(&self.key),
82            }
83        }
84    }
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90
91    /// Unique per-test key: parallel tests mutate distinct keys, and each
92    /// test serializes anyway for maximum platform portability.
93    ///
94    /// SAFETY contract for the raw env calls below mirrors [`EnvVarGuard`]:
95    /// single-threaded mutation points, serialized via `serial_test`.
96    fn unique_key(tag: &str) -> String {
97        format!("term-wm-envguard-{tag}-{}", std::process::id())
98    }
99
100    #[test]
101    #[serial_test::serial]
102    fn set_installs_value_and_drop_restores_original() {
103        let key = unique_key("set-restore");
104        // SAFETY: serialized via #[serial_test::serial]; unique key.
105        unsafe { std::env::set_var(&key, "original") };
106
107        {
108            let _guard = EnvVarGuard::set(&key, "temporary");
109            assert_eq!(
110                std::env::var_os(&key).as_deref(),
111                Some(std::ffi::OsStr::new("temporary"))
112            );
113        }
114
115        assert_eq!(
116            std::env::var_os(&key).as_deref(),
117            Some(std::ffi::OsStr::new("original"))
118        );
119        // SAFETY: serialized; unique key.
120        unsafe { std::env::remove_var(&key) };
121    }
122
123    #[test]
124    #[serial_test::serial]
125    fn removed_unsets_and_stays_unset_after_drop_when_originally_absent() {
126        let key = unique_key("removed-absent");
127        // SAFETY: serialized via #[serial_test::serial]; unique key.
128        unsafe { std::env::remove_var(&key) };
129
130        {
131            let _guard = EnvVarGuard::removed(&key);
132            assert!(std::env::var_os(&key).is_none());
133        }
134
135        assert!(std::env::var_os(&key).is_none(), "must stay unset");
136    }
137
138    #[test]
139    #[serial_test::serial]
140    fn removed_restores_previous_value_after_drop() {
141        let key = unique_key("removed-prev");
142        // SAFETY: serialized via #[serial_test::serial]; unique key.
143        unsafe { std::env::set_var(&key, "keepme") };
144
145        {
146            let _guard = EnvVarGuard::removed(&key);
147            assert!(std::env::var_os(&key).is_none());
148        }
149
150        assert_eq!(
151            std::env::var_os(&key).as_deref(),
152            Some(std::ffi::OsStr::new("keepme"))
153        );
154        // SAFETY: serialized; unique key.
155        unsafe { std::env::remove_var(&key) }
156    }
157}