#![forbid(unsafe_code)]
#![deny(clippy::all)]
use std::{
env,
error::Error,
fmt::{self, Display},
io,
path::{Path, PathBuf},
sync::{Mutex, MutexGuard},
};
static ENV_MUTEX: Mutex<()> = Mutex::new(());
static CURRENT_DIR_MUTEX: Mutex<()> = Mutex::new(());
pub fn lock_env<'a>(
variables: impl IntoIterator<Item = (&'a str, Option<impl AsRef<str>>)>,
) -> EnvGuard<'a> {
let guard = ENV_MUTEX.lock().unwrap_or_else(|error| error.into_inner());
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,
}
}
#[must_use = "Environment is unlocked when guard is dropped"]
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) {
for (variable, value) in &self.previous_values {
if let Some(value) = value {
env::set_var(variable, value);
} else {
env::remove_var(variable);
}
}
}
}
pub fn lock_current_dir(
dir: impl AsRef<Path>,
) -> Result<CurrentDirGuard, CurrentDirError> {
let guard = CURRENT_DIR_MUTEX
.lock()
.unwrap_or_else(|error| error.into_inner());
let previous_dir = env::current_dir().map_err(CurrentDirError::Get)?;
env::set_current_dir(dir).map_err(CurrentDirError::Set)?;
Ok(CurrentDirGuard {
previous_dir,
guard,
})
}
pub struct CurrentDirGuard {
previous_dir: PathBuf,
#[allow(unused)]
guard: MutexGuard<'static, ()>,
}
impl Drop for CurrentDirGuard {
fn drop(&mut self) {
let _ = env::set_current_dir(&self.previous_dir);
}
}
#[derive(Debug)]
pub enum CurrentDirError {
Get(io::Error),
Set(io::Error),
}
impl Display for CurrentDirError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::Get(_) => {
write!(f, "getting current directory")
}
Self::Set(_) => {
write!(f, "setting current directory")
}
}
}
}
impl Error for CurrentDirError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::Get(err) => Some(err),
Self::Set(err) => Some(err),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::panic;
#[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());
}
#[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");
}
#[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");
}
#[test]
fn env_reset_on_panic() {
let var = "ENV_LOCK_TEST_VARIABLE_RESET_ON_PANIC";
env::set_var(var, "default");
panic::catch_unwind(|| {
let _guard = lock_env([(var, Some("panicked!"))]);
assert_eq!(env::var(var).unwrap(), "panicked!");
panic!("oh no!");
})
.unwrap_err();
assert_eq!(env::var(var).unwrap(), "default");
let _guard = lock_env([(var, Some("very calm"))]);
assert_eq!(env::var(var).unwrap(), "very calm");
}
#[test]
fn current_dir_reset_on_panic() {
let current_dir = env::current_dir().unwrap();
let new_dir = current_dir.parent().unwrap();
panic::catch_unwind(|| {
let _guard = lock_current_dir(new_dir).unwrap();
assert_eq!(env::current_dir().unwrap(), new_dir);
panic!("oh no!");
})
.unwrap_err();
assert_eq!(env::current_dir().unwrap(), current_dir);
let _guard = lock_current_dir(new_dir);
assert_eq!(env::current_dir().unwrap(), new_dir);
}
}