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,
39 error::Error,
40 fmt::{self, Display},
41 io,
42 path::{Path, PathBuf},
43 sync::{Mutex, MutexGuard},
44};
45
46/// Global mutex for accessing environment variables. Technically we could break
47/// this out into a map with one mutex per variable, but that adds a ton of
48/// complexity for very little value.
49static ENV_MUTEX: Mutex<()> = Mutex::new(());
50/// Global mutex for modifying the current working directory
51static CURRENT_DIR_MUTEX: Mutex<()> = Mutex::new(());
52
53/// Lock the environment and set each given variable to its corresponding
54/// value. If the environment is already locked, this will block until the lock
55/// can be acquired. The returned guard will keep the environment locked so the
56/// calling test has exclusive access to it. Upon being dropped, the old
57/// environment values will be restored and then the environment will be
58/// unlocked.
59///
60/// ## Note
61/// There is a single mutex per process that locks the *entire*
62/// environment. This means multiple usages of by `lock_env` cannot run
63/// concurrently, even if they don't modify any of the same environment
64/// variables. Keep your critical sections as short as possible to prevent
65/// slowdowns.
66pub fn lock_env<'a>(
67 variables: impl IntoIterator<Item = (&'a str, Option<impl AsRef<str>>)>,
68) -> EnvGuard<'a> {
69 // We can ignore poison errors, because the Drop impl for EnvGuard restores
70 // the environment on panic
71 let guard = ENV_MUTEX.lock().unwrap_or_else(|error| error.into_inner());
72
73 let previous_values = variables
74 .into_iter()
75 .map(|(variable, new_value)| {
76 let previous_value = env::var(variable).ok();
77
78 if let Some(value) = new_value {
79 env::set_var(variable, value.as_ref());
80 } else {
81 env::remove_var(variable);
82 }
83
84 (variable, previous_value)
85 })
86 .collect();
87
88 EnvGuard {
89 previous_values,
90 guard,
91 }
92}
93
94/// A guard used to indicate that the current process environment is locked.
95/// Returned by [lock_env]. This will restore and unlock the environment on
96/// drop.
97pub struct EnvGuard<'a> {
98 previous_values: Vec<(&'a str, Option<String>)>,
99 #[allow(unused)]
100 guard: MutexGuard<'static, ()>,
101}
102
103impl<'a> Drop for EnvGuard<'a> {
104 fn drop(&mut self) {
105 // Restore each env var
106 for (variable, value) in &self.previous_values {
107 if let Some(value) = value {
108 env::set_var(variable, value);
109 } else {
110 env::remove_var(variable);
111 }
112 }
113 }
114}
115
116/// Set the working directory for the current process. The working directory is
117/// a form of global mutable state, so this use a mutex to ensure that only one
118/// mutation can be made at a time. This returns a guard that, when dropped,
119/// will revert the working directory to its previous value and release the lock
120/// on it.
121///
122/// ## Errors
123///
124/// Return an error if either [current_dir](env::current_dir) or
125/// [set_current_dir](env::set_current_dir) fails. See those two functions for
126/// failure conditions. In either case, the current directory will *not* be
127/// modified and its mutex will remain unlocked.
128pub fn lock_current_dir(
129 dir: impl AsRef<Path>,
130) -> Result<CurrentDirGuard, CurrentDirError> {
131 // We can ignore poison errors, because the Drop impl for EnvGuard restores
132 // the environment on panic
133 let guard = CURRENT_DIR_MUTEX
134 .lock()
135 .unwrap_or_else(|error| error.into_inner());
136 // Acquire the lock before checking the current value to make sure it isn't
137 // modified by other tests
138 let previous_dir = env::current_dir().map_err(CurrentDirError::Get)?;
139 env::set_current_dir(dir).map_err(CurrentDirError::Set)?;
140 Ok(CurrentDirGuard {
141 previous_dir,
142 guard,
143 })
144}
145
146/// A guard used to indicate that the current working directory is locked.
147/// Returned by [lock_current_dir]. This will restore and unlock the working
148/// directory on drop.
149pub struct CurrentDirGuard {
150 previous_dir: PathBuf,
151 #[allow(unused)]
152 guard: MutexGuard<'static, ()>,
153}
154
155impl Drop for CurrentDirGuard {
156 fn drop(&mut self) {
157 let _ = env::set_current_dir(&self.previous_dir);
158 }
159}
160
161/// Context for an error that can occur while locking the current directory.
162/// Both the get and the set can fail; this error tells you which one failed.
163/// Use [Error::source] to get the underlying error.
164#[derive(Debug)]
165pub enum CurrentDirError {
166 /// Error while getting the current directory. The current directory must
167 /// be fetched before it's modified so we know what value to revert to.
168 Get(io::Error),
169 /// Error while setting the current directory
170 Set(io::Error),
171}
172
173impl Display for CurrentDirError {
174 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
175 match self {
176 Self::Get(_) => {
177 write!(f, "getting current directory")
178 }
179 Self::Set(_) => {
180 write!(f, "setting current directory")
181 }
182 }
183 }
184}
185
186impl Error for CurrentDirError {
187 fn source(&self) -> Option<&(dyn Error + 'static)> {
188 match self {
189 Self::Get(err) => Some(err),
190 Self::Set(err) => Some(err),
191 }
192 }
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198 use std::panic;
199
200 // NOTE: Because these tests specifically modify environment variables
201 // *outside* the env lock, they each need to use a different variable. If
202 // only someone make a library that would avoid that...
203
204 /// Set a value for a variable that doesn't exist yet
205 #[test]
206 fn set_missing_var() {
207 let var = "ENV_LOCK_TEST_VARIABLE_SET_MISSING";
208 assert!(env::var(var).is_err());
209
210 let guard = lock_env([(var, Some("hello!"))]);
211 assert_eq!(env::var(var).unwrap(), "hello!");
212 drop(guard);
213
214 assert!(env::var(var).is_err());
215 }
216
217 /// Override the value for a preexisting variable
218 #[test]
219 fn set_existing_var() {
220 let var = "ENV_LOCK_TEST_VARIABLE_SET_EXISTING";
221 env::set_var(var, "existing");
222 assert_eq!(env::var(var).unwrap(), "existing");
223
224 let guard = lock_env([(var, Some("hello!"))]);
225 assert_eq!(env::var(var).unwrap(), "hello!");
226 drop(guard);
227
228 assert_eq!(env::var(var).unwrap(), "existing");
229 }
230
231 /// Remove the value for a preexisting variable
232 #[test]
233 fn clear_existing_var() {
234 let var = "ENV_LOCK_TEST_VARIABLE_CLEAR_EXISTING";
235 env::set_var(var, "existing");
236 assert_eq!(env::var(var).unwrap(), "existing");
237
238 let guard = lock_env([(var, None::<&str>)]);
239 assert!(env::var(var).is_err());
240 drop(guard);
241
242 assert_eq!(env::var(var).unwrap(), "existing");
243 }
244
245 /// Environment should be restored correctly if a panic occurs while it's
246 /// held. This is important behavior because tests have a tendency to panic
247 #[test]
248 fn env_reset_on_panic() {
249 let var = "ENV_LOCK_TEST_VARIABLE_RESET_ON_PANIC";
250 env::set_var(var, "default");
251 panic::catch_unwind(|| {
252 let _guard = lock_env([(var, Some("panicked!"))]);
253 assert_eq!(env::var(var).unwrap(), "panicked!");
254 panic!("oh no!");
255 })
256 .unwrap_err();
257
258 // Previous state was restored
259 assert_eq!(env::var(var).unwrap(), "default");
260
261 // Should be able to reacquire the lock no problem
262 let _guard = lock_env([(var, Some("very calm"))]);
263 assert_eq!(env::var(var).unwrap(), "very calm");
264 }
265
266 /// Current dir should be restored correctly if a panic occurs while it's
267 /// held. This is important behavior because tests have a tendency to panic
268 #[test]
269 fn current_dir_reset_on_panic() {
270 let current_dir = env::current_dir().unwrap();
271 let new_dir = current_dir.parent().unwrap();
272 panic::catch_unwind(|| {
273 let _guard = lock_current_dir(new_dir).unwrap();
274 assert_eq!(env::current_dir().unwrap(), new_dir);
275 panic!("oh no!");
276 })
277 .unwrap_err();
278
279 // Previous state was restored
280 assert_eq!(env::current_dir().unwrap(), current_dir);
281
282 // Should be able to reacquire the lock no problem
283 let _guard = lock_current_dir(new_dir);
284 assert_eq!(env::current_dir().unwrap(), new_dir);
285 }
286}