1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142
//! Lock environment variables to prevent simultaneous access. Use [lock_env] to
//! set values for whatever environment variables you intend to access in your
//! test. This will return a guard that, when dropped, will revert the
//! environment to its initial state. The guard uses a [Mutex] underneath to
//! ensure that multiple tests within the same process can't access it at the
//! same time.
//!
//! ```
//! use std::env;
//!
//! let var = "ENV_LOCK_TEST_VARIABLE";
//! assert!(env::var(var).is_err());
//!
//! let guard = env_lock::lock_env([(var, Some("hello!"))]);
//! assert_eq!(env::var(var).unwrap(), "hello!");
//! drop(guard);
//!
//! assert!(env::var(var).is_err());
//! ```
#![forbid(unsafe_code)]
#![deny(clippy::all)]
use std::{
env,
sync::{Mutex, MutexGuard},
};
/// Lock the environment and set each given variable to its corresponding
/// value. If the environment is already locked, this will block until the lock
/// can be acquired. The returned guard will keep the environment locked so the
/// calling test has exclusive access to it. Upon being dropped, the old
/// environment values will be restored and then the environment will be
/// unlocked.
///
/// ## Note
/// There is a single mutex per process that locks the *entire*
/// environment. This means multiple usages of by `lock_env` cannot run
/// concurrently, even if they don't modify any of the same environment
/// variables. Keep your critical sections as short as possible to prevent
/// slowdowns.
pub fn lock_env<'a>(
variables: impl IntoIterator<Item = (&'a str, Option<impl AsRef<str>>)>,
) -> EnvGuard<'a> {
/// Global mutex for accessing environment variables. Technically we
/// could break this out into a map with one mutex per variable, but
/// that adds a ton of complexity for very little value.
static ENV_MUTEX: Mutex<()> = Mutex::new(());
let guard = ENV_MUTEX.lock().expect("Environment lock is poisoned");
let previous_values = variables
.into_iter()
.map(|(variable, new_value)| {
let previous_value = env::var(variable).ok();
if let Some(value) = new_value {
env::set_var(variable, value.as_ref());
} else {
env::remove_var(variable);
}
(variable, previous_value)
})
.collect();
EnvGuard {
previous_values,
guard,
}
}
/// A guard used to indicate that the current process environment is locked.
/// Returned by [lock_env]. This will restore and unlock the environment on
/// drop.
pub struct EnvGuard<'a> {
previous_values: Vec<(&'a str, Option<String>)>,
#[allow(unused)]
guard: MutexGuard<'static, ()>,
}
impl<'a> Drop for EnvGuard<'a> {
fn drop(&mut self) {
// Restore each env var
for (variable, value) in &self.previous_values {
if let Some(value) = value {
env::set_var(variable, value);
} else {
env::remove_var(variable);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
// NOTE: Because these tests specifically modify environment variables
// *outside* the env lock, they each need to use a different variable. If
// only someone make a library that would avoid that...
/// Set a value for a variable that doesn't exist yet
#[test]
fn set_missing_var() {
let var = "ENV_LOCK_TEST_VARIABLE_SET_MISSING";
assert!(env::var(var).is_err());
let guard = lock_env([(var, Some("hello!"))]);
assert_eq!(env::var(var).unwrap(), "hello!");
drop(guard);
assert!(env::var(var).is_err());
}
/// Override the value for a preexisting variable
#[test]
fn set_existing_var() {
let var = "ENV_LOCK_TEST_VARIABLE_SET_EXISTING";
env::set_var(var, "existing");
assert_eq!(env::var(var).unwrap(), "existing");
let guard = lock_env([(var, Some("hello!"))]);
assert_eq!(env::var(var).unwrap(), "hello!");
drop(guard);
assert_eq!(env::var(var).unwrap(), "existing");
}
/// Remove the value for a preexisting variable
#[test]
fn clear_existing_var() {
let var = "ENV_LOCK_TEST_VARIABLE_CLEAR_EXISTING";
env::set_var(var, "existing");
assert_eq!(env::var(var).unwrap(), "existing");
let guard = lock_env([(var, None::<&str>)]);
assert!(env::var(var).is_err());
drop(guard);
assert_eq!(env::var(var).unwrap(), "existing");
}
}