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}