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}