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.
97#[must_use = "Environment is unlocked when guard is dropped"]
98pub struct EnvGuard<'a> {
99    previous_values: Vec<(&'a str, Option<String>)>,
100    #[allow(unused)]
101    guard: MutexGuard<'static, ()>,
102}
103
104impl<'a> Drop for EnvGuard<'a> {
105    fn drop(&mut self) {
106        // Restore each env var
107        for (variable, value) in &self.previous_values {
108            if let Some(value) = value {
109                env::set_var(variable, value);
110            } else {
111                env::remove_var(variable);
112            }
113        }
114    }
115}
116
117/// Set the working directory for the current process. The working directory is
118/// a form of global mutable state, so this use a mutex to ensure that only one
119/// mutation can be made at a time. This returns a guard that, when dropped,
120/// will revert the working directory to its previous value and release the lock
121/// on it.
122///
123/// ## Errors
124///
125/// Return an error if either [current_dir](env::current_dir) or
126/// [set_current_dir](env::set_current_dir) fails. See those two functions for
127/// failure conditions. In either case, the current directory will *not* be
128/// modified and its mutex will remain unlocked.
129pub fn lock_current_dir(
130    dir: impl AsRef<Path>,
131) -> Result<CurrentDirGuard, CurrentDirError> {
132    // We can ignore poison errors, because the Drop impl for EnvGuard restores
133    // the environment on panic
134    let guard = CURRENT_DIR_MUTEX
135        .lock()
136        .unwrap_or_else(|error| error.into_inner());
137    // Acquire the lock before checking the current value to make sure it isn't
138    // modified by other tests
139    let previous_dir = env::current_dir().map_err(CurrentDirError::Get)?;
140    env::set_current_dir(dir).map_err(CurrentDirError::Set)?;
141    Ok(CurrentDirGuard {
142        previous_dir,
143        guard,
144    })
145}
146
147/// A guard used to indicate that the current working directory is locked.
148/// Returned by [lock_current_dir]. This will restore and unlock the working
149/// directory on drop.
150pub struct CurrentDirGuard {
151    previous_dir: PathBuf,
152    #[allow(unused)]
153    guard: MutexGuard<'static, ()>,
154}
155
156impl Drop for CurrentDirGuard {
157    fn drop(&mut self) {
158        let _ = env::set_current_dir(&self.previous_dir);
159    }
160}
161
162/// Context for an error that can occur while locking the current directory.
163/// Both the get and the set can fail; this error tells you which one failed.
164/// Use [Error::source] to get the underlying error.
165#[derive(Debug)]
166pub enum CurrentDirError {
167    /// Error while getting the current directory. The current directory must
168    /// be fetched before it's modified so we know what value to revert to.
169    Get(io::Error),
170    /// Error while setting the current directory
171    Set(io::Error),
172}
173
174impl Display for CurrentDirError {
175    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
176        match self {
177            Self::Get(_) => {
178                write!(f, "getting current directory")
179            }
180            Self::Set(_) => {
181                write!(f, "setting current directory")
182            }
183        }
184    }
185}
186
187impl Error for CurrentDirError {
188    fn source(&self) -> Option<&(dyn Error + 'static)> {
189        match self {
190            Self::Get(err) => Some(err),
191            Self::Set(err) => Some(err),
192        }
193    }
194}
195
196#[cfg(test)]
197mod tests {
198    use super::*;
199    use std::panic;
200
201    // NOTE: Because these tests specifically modify environment variables
202    // *outside* the env lock, they each need to use a different variable. If
203    // only someone make a library that would avoid that...
204
205    /// Set a value for a variable that doesn't exist yet
206    #[test]
207    fn set_missing_var() {
208        let var = "ENV_LOCK_TEST_VARIABLE_SET_MISSING";
209        assert!(env::var(var).is_err());
210
211        let guard = lock_env([(var, Some("hello!"))]);
212        assert_eq!(env::var(var).unwrap(), "hello!");
213        drop(guard);
214
215        assert!(env::var(var).is_err());
216    }
217
218    /// Override the value for a preexisting variable
219    #[test]
220    fn set_existing_var() {
221        let var = "ENV_LOCK_TEST_VARIABLE_SET_EXISTING";
222        env::set_var(var, "existing");
223        assert_eq!(env::var(var).unwrap(), "existing");
224
225        let guard = lock_env([(var, Some("hello!"))]);
226        assert_eq!(env::var(var).unwrap(), "hello!");
227        drop(guard);
228
229        assert_eq!(env::var(var).unwrap(), "existing");
230    }
231
232    /// Remove the value for a preexisting variable
233    #[test]
234    fn clear_existing_var() {
235        let var = "ENV_LOCK_TEST_VARIABLE_CLEAR_EXISTING";
236        env::set_var(var, "existing");
237        assert_eq!(env::var(var).unwrap(), "existing");
238
239        let guard = lock_env([(var, None::<&str>)]);
240        assert!(env::var(var).is_err());
241        drop(guard);
242
243        assert_eq!(env::var(var).unwrap(), "existing");
244    }
245
246    /// Environment should be restored correctly if a panic occurs while it's
247    /// held. This is important behavior because tests have a tendency to panic
248    #[test]
249    fn env_reset_on_panic() {
250        let var = "ENV_LOCK_TEST_VARIABLE_RESET_ON_PANIC";
251        env::set_var(var, "default");
252        panic::catch_unwind(|| {
253            let _guard = lock_env([(var, Some("panicked!"))]);
254            assert_eq!(env::var(var).unwrap(), "panicked!");
255            panic!("oh no!");
256        })
257        .unwrap_err();
258
259        // Previous state was restored
260        assert_eq!(env::var(var).unwrap(), "default");
261
262        // Should be able to reacquire the lock no problem
263        let _guard = lock_env([(var, Some("very calm"))]);
264        assert_eq!(env::var(var).unwrap(), "very calm");
265    }
266
267    /// Current dir should be restored correctly if a panic occurs while it's
268    /// held. This is important behavior because tests have a tendency to panic
269    #[test]
270    fn current_dir_reset_on_panic() {
271        let current_dir = env::current_dir().unwrap();
272        let new_dir = current_dir.parent().unwrap();
273        panic::catch_unwind(|| {
274            let _guard = lock_current_dir(new_dir).unwrap();
275            assert_eq!(env::current_dir().unwrap(), new_dir);
276            panic!("oh no!");
277        })
278        .unwrap_err();
279
280        // Previous state was restored
281        assert_eq!(env::current_dir().unwrap(), current_dir);
282
283        // Should be able to reacquire the lock no problem
284        let _guard = lock_current_dir(new_dir);
285        assert_eq!(env::current_dir().unwrap(), new_dir);
286    }
287}