env_lock/lib.rs
1//! Lock environment variables to prevent simultaneous access. Use [lock_env] to
2//! set values for whatever environment variables you intend to access in your
3//! test. This will return a guard that, when dropped, will revert the
4//! environment to its initial state. The guard uses a [Mutex] underneath to
5//! ensure that multiple tests within the same process can't access it at the
6//! same time.
7//!
8//! ```
9//! # use std::env;
10//! let var = "ENV_LOCK_TEST_VARIABLE";
11//! assert!(env::var(var).is_err());
12//!
13//! let guard = env_lock::lock_env([(var, Some("hello!"))]);
14//! assert_eq!(env::var(var).unwrap(), "hello!");
15//! drop(guard);
16//!
17//! assert!(env::var(var).is_err());
18//! ```
19//!
20//! You can also lock the current working directory, which is another form of
21//! mutable global state.
22//!
23//! ```
24//! # use std::{env, path::Path};
25//! let old_dir = env::current_dir().unwrap();
26//! let new_dir = old_dir.parent().unwrap();
27//! let guard = env_lock::lock_current_dir(new_dir).unwrap();
28//! assert_eq!(env::current_dir().unwrap(), new_dir);
29//! drop(guard);
30//!
31//! assert_eq!(env::current_dir().unwrap(), old_dir);
32//! ```
33
34#![forbid(unsafe_code)]
35#![deny(clippy::all)]
36
37use std::{
38 env, io,
39 path::{Path, PathBuf},
40 sync::{Mutex, MutexGuard},
41};
42
43/// Global mutex for accessing environment variables. Technically we could break
44/// this out into a map with one mutex per variable, but that adds a ton of
45/// complexity for very little value.
46static ENV_MUTEX: Mutex<()> = Mutex::new(());
47/// Global mutex for modifying the current working directory
48static CURRENT_DIR_MUTEX: Mutex<()> = Mutex::new(());
49
50/// Lock the environment and set each given variable to its corresponding
51/// value. If the environment is already locked, this will block until the lock
52/// can be acquired. The returned guard will keep the environment locked so the
53/// calling test has exclusive access to it. Upon being dropped, the old
54/// environment values will be restored and then the environment will be
55/// unlocked.
56///
57/// ## Note
58/// There is a single mutex per process that locks the *entire*
59/// environment. This means multiple usages of by `lock_env` cannot run
60/// concurrently, even if they don't modify any of the same environment
61/// variables. Keep your critical sections as short as possible to prevent
62/// slowdowns.
63pub fn lock_env<'a>(
64 variables: impl IntoIterator<Item = (&'a str, Option<impl AsRef<str>>)>,
65) -> EnvGuard<'a> {
66 // We can ignore poison errors, because the Drop impl for EnvGuard restores
67 // the environment on panic
68 let guard = ENV_MUTEX.lock().unwrap_or_else(|error| error.into_inner());
69
70 let previous_values = variables
71 .into_iter()
72 .map(|(variable, new_value)| {
73 let previous_value = env::var(variable).ok();
74
75 if let Some(value) = new_value {
76 env::set_var(variable, value.as_ref());
77 } else {
78 env::remove_var(variable);
79 }
80
81 (variable, previous_value)
82 })
83 .collect();
84
85 EnvGuard {
86 previous_values,
87 guard,
88 }
89}
90
91/// A guard used to indicate that the current process environment is locked.
92/// Returned by [lock_env]. This will restore and unlock the environment on
93/// drop.
94pub struct EnvGuard<'a> {
95 previous_values: Vec<(&'a str, Option<String>)>,
96 #[allow(unused)]
97 guard: MutexGuard<'static, ()>,
98}
99
100impl<'a> Drop for EnvGuard<'a> {
101 fn drop(&mut self) {
102 // Restore each env var
103 for (variable, value) in &self.previous_values {
104 if let Some(value) = value {
105 env::set_var(variable, value);
106 } else {
107 env::remove_var(variable);
108 }
109 }
110 }
111}
112
113/// Set the working directory for the current process. The working directory is
114/// a form of global mutable state, so this use a mutex to ensure that only one
115/// mutation can be made at a time. This returns a guard that, when dropped,
116/// will revert the working directory to its previous value and release the lock
117/// on it.
118///
119/// ## Errors
120///
121/// Return an error if either [current_dir](env::current_dir) or
122/// [set_current_dir](env::set_current_dir) fails. See those two functions for
123/// failure conditions. In either case, the current directory will *not* be
124/// modified and its mutex will remain unlocked.
125pub fn lock_current_dir(
126 dir: impl AsRef<Path>,
127) -> Result<CurrentDirGuard, io::Error> {
128 let previous_dir = env::current_dir()?;
129 // We can ignore poison errors, because the Drop impl for EnvGuard restores
130 // the environment on panic
131 let guard = CURRENT_DIR_MUTEX
132 .lock()
133 .unwrap_or_else(|error| error.into_inner());
134 env::set_current_dir(dir)?;
135 Ok(CurrentDirGuard {
136 previous_dir,
137 guard,
138 })
139}
140
141/// A guard used to indicate that the current working directory is locked.
142/// Returned by [lock_current_dir]. This will restore and unlock the working
143/// directory on drop.
144pub struct CurrentDirGuard {
145 previous_dir: PathBuf,
146 #[allow(unused)]
147 guard: MutexGuard<'static, ()>,
148}
149
150impl Drop for CurrentDirGuard {
151 fn drop(&mut self) {
152 let _ = env::set_current_dir(&self.previous_dir);
153 }
154}
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159 use std::panic;
160
161 // NOTE: Because these tests specifically modify environment variables
162 // *outside* the env lock, they each need to use a different variable. If
163 // only someone make a library that would avoid that...
164
165 /// Set a value for a variable that doesn't exist yet
166 #[test]
167 fn set_missing_var() {
168 let var = "ENV_LOCK_TEST_VARIABLE_SET_MISSING";
169 assert!(env::var(var).is_err());
170
171 let guard = lock_env([(var, Some("hello!"))]);
172 assert_eq!(env::var(var).unwrap(), "hello!");
173 drop(guard);
174
175 assert!(env::var(var).is_err());
176 }
177
178 /// Override the value for a preexisting variable
179 #[test]
180 fn set_existing_var() {
181 let var = "ENV_LOCK_TEST_VARIABLE_SET_EXISTING";
182 env::set_var(var, "existing");
183 assert_eq!(env::var(var).unwrap(), "existing");
184
185 let guard = lock_env([(var, Some("hello!"))]);
186 assert_eq!(env::var(var).unwrap(), "hello!");
187 drop(guard);
188
189 assert_eq!(env::var(var).unwrap(), "existing");
190 }
191
192 /// Remove the value for a preexisting variable
193 #[test]
194 fn clear_existing_var() {
195 let var = "ENV_LOCK_TEST_VARIABLE_CLEAR_EXISTING";
196 env::set_var(var, "existing");
197 assert_eq!(env::var(var).unwrap(), "existing");
198
199 let guard = lock_env([(var, None::<&str>)]);
200 assert!(env::var(var).is_err());
201 drop(guard);
202
203 assert_eq!(env::var(var).unwrap(), "existing");
204 }
205
206 /// Environment should be restored correctly if a panic occurs while it's
207 /// held. This is important behavior because tests have a tendency to panic
208 #[test]
209 fn env_reset_on_panic() {
210 let var = "ENV_LOCK_TEST_VARIABLE_RESET_ON_PANIC";
211 env::set_var(var, "default");
212 panic::catch_unwind(|| {
213 let _guard = lock_env([(var, Some("panicked!"))]);
214 assert_eq!(env::var(var).unwrap(), "panicked!");
215 panic!("oh no!");
216 })
217 .unwrap_err();
218
219 // Previous state was restored
220 assert_eq!(env::var(var).unwrap(), "default");
221
222 // Should be able to reacquire the lock no problem
223 let _guard = lock_env([(var, Some("very calm"))]);
224 assert_eq!(env::var(var).unwrap(), "very calm");
225 }
226
227 /// Current dir should be restored correctly if a panic occurs while it's
228 /// held. This is important behavior because tests have a tendency to panic
229 #[test]
230 fn current_dir_reset_on_panic() {
231 let current_dir = env::current_dir().unwrap();
232 let new_dir = current_dir.parent().unwrap();
233 panic::catch_unwind(|| {
234 let _guard = lock_current_dir(new_dir).unwrap();
235 assert_eq!(env::current_dir().unwrap(), new_dir);
236 panic!("oh no!");
237 })
238 .unwrap_err();
239
240 // Previous state was restored
241 assert_eq!(env::current_dir().unwrap(), current_dir);
242
243 // Should be able to reacquire the lock no problem
244 let _guard = lock_current_dir(new_dir);
245 assert_eq!(env::current_dir().unwrap(), new_dir);
246 }
247}