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}