#![cfg(test)]
use std::ffi::{OsStr, OsString};
#[must_use = "EnvGuard must be bound to a local; dropping it immediately restores the variable"]
pub struct EnvGuard {
key: OsString,
original: Option<OsString>,
}
impl EnvGuard {
#[allow(dead_code)]
pub fn set(key: impl Into<OsString>, value: impl AsRef<OsStr>) -> Self {
let key = key.into();
let original = std::env::var_os(&key);
unsafe {
std::env::set_var(&key, value);
}
Self { key, original }
}
#[allow(dead_code)]
pub fn remove(key: impl Into<OsString>) -> Self {
let key = key.into();
let original = std::env::var_os(&key);
unsafe {
std::env::remove_var(&key);
}
Self { key, original }
}
}
impl Drop for EnvGuard {
fn drop(&mut self) {
unsafe {
match self.original.take() {
Some(v) => std::env::set_var(&self.key, v),
None => std::env::remove_var(&self.key),
}
}
}
}
mod tests {
use super::EnvGuard;
use serial_test::serial;
const KEY_SET: &str = "BSSH_ENVGUARD_TEST_VAR_SET";
const KEY_REMOVE: &str = "BSSH_ENVGUARD_TEST_VAR_REMOVE";
const KEY_CHAIN: &str = "BSSH_ENVGUARD_TEST_VAR_CHAIN";
const KEY_UNSET: &str = "BSSH_ENVGUARD_TEST_VAR_UNSET";
const KEY_OVERWRITE: &str = "BSSH_ENVGUARD_TEST_VAR_OVERWRITE";
#[test]
#[serial]
fn set_restores_prior_value_on_drop() {
unsafe { std::env::remove_var(KEY_SET) };
{
let _guard = EnvGuard::set(KEY_SET, "temporary");
assert_eq!(std::env::var(KEY_SET).ok().as_deref(), Some("temporary"));
}
assert!(
std::env::var(KEY_SET).is_err(),
"variable should be unset after guard drop"
);
}
#[test]
#[serial]
fn remove_restores_prior_value_on_drop() {
unsafe { std::env::set_var(KEY_REMOVE, "original") };
{
let _guard = EnvGuard::remove(KEY_REMOVE);
assert!(
std::env::var(KEY_REMOVE).is_err(),
"variable should be absent while guard is live"
);
}
assert_eq!(
std::env::var(KEY_REMOVE).ok().as_deref(),
Some("original"),
"variable should be restored after guard drop"
);
unsafe { std::env::remove_var(KEY_REMOVE) };
}
#[test]
#[serial]
fn chained_set_guards_restore_in_lifo_order() {
unsafe { std::env::remove_var(KEY_CHAIN) };
{
let _g1 = EnvGuard::set(KEY_CHAIN, "first");
assert_eq!(std::env::var(KEY_CHAIN).ok().as_deref(), Some("first"));
{
let _g2 = EnvGuard::set(KEY_CHAIN, "second");
assert_eq!(std::env::var(KEY_CHAIN).ok().as_deref(), Some("second"));
}
assert_eq!(
std::env::var(KEY_CHAIN).ok().as_deref(),
Some("first"),
"inner guard should restore to value set by outer guard"
);
}
assert!(
std::env::var(KEY_CHAIN).is_err(),
"outer guard should restore to absent state"
);
}
#[test]
#[serial]
fn remove_on_already_unset_variable_is_noop() {
unsafe { std::env::remove_var(KEY_UNSET) };
{
let _guard = EnvGuard::remove(KEY_UNSET);
assert!(std::env::var(KEY_UNSET).is_err());
}
assert!(
std::env::var(KEY_UNSET).is_err(),
"removing an already-absent variable should leave it absent"
);
}
#[test]
#[serial]
fn set_over_existing_value_restores_original() {
unsafe { std::env::set_var(KEY_OVERWRITE, "before") };
{
let _guard = EnvGuard::set(KEY_OVERWRITE, "during");
assert_eq!(std::env::var(KEY_OVERWRITE).ok().as_deref(), Some("during"));
}
assert_eq!(
std::env::var(KEY_OVERWRITE).ok().as_deref(),
Some("before"),
"guard should restore the pre-existing value, not remove the variable"
);
unsafe { std::env::remove_var(KEY_OVERWRITE) };
}
}