use once_cell::sync::OnceCell;
use std::collections::HashMap;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex, PoisonError};
use std::{env, fs, io};
use tempfile::{tempdir, TempDir};
use super::*;
type EnvMap = HashMap<String, String>;
static ENV_LOCKER: OnceCell<Arc<Mutex<EnvMap>>> = OnceCell::new();
#[derive(Debug)]
pub struct TestEnv {
temp_dir: TempDir,
work_dir: PathBuf,
env_vars: Vec<KeyVal>,
envfile_contents: Option<String>,
envfile_path: PathBuf,
}
#[derive(Debug, Clone)]
pub struct KeyVal {
key: String,
value: String,
}
pub fn test_in_env<F>(test_env: TestEnv, test: F)
where
F: FnOnce(),
{
let locker = get_env_locker();
let original_env = locker.lock().unwrap_or_else(PoisonError::into_inner);
reset_env(&original_env);
create_env(&test_env);
test();
}
pub fn test_in_default_env<F>(test: F)
where
F: FnOnce(),
{
let test_env = TestEnv::default();
test_in_env(test_env, test);
}
impl TestEnv {
pub fn init() -> Self {
let tempdir = tempdir().expect("create tempdir");
let work_dir = tempdir.path().to_owned();
let envfile_path = work_dir.join(".env");
Self {
temp_dir: tempdir,
work_dir,
env_vars: Default::default(),
envfile_contents: None,
envfile_path,
}
}
pub fn init_with_envfile(contents: impl ToString) -> Self {
let mut test_env = Self::init();
test_env.set_envfile_contents(contents);
test_env
}
pub fn set_envfile_name(&mut self, name: impl AsRef<Path>) -> &mut Self {
self.envfile_path = self.temp_path().join(name);
self
}
pub fn set_envfile_path(&mut self, path: PathBuf) -> &mut Self {
self.envfile_path = path;
self
}
pub fn set_envfile_contents(&mut self, contents: impl ToString) -> &mut Self {
self.envfile_contents = Some(contents.to_string());
self
}
pub fn set_work_dir(&mut self, path: PathBuf) -> &mut Self {
self.work_dir = path;
self
}
pub fn add_env_var(&mut self, key: impl ToString, value: impl ToString) -> &mut Self {
self.env_vars.push(KeyVal {
key: key.to_string(),
value: value.to_string(),
});
self
}
pub fn set_env_vars(&mut self, env_vars: Vec<KeyVal>) -> &mut Self {
self.env_vars = env_vars;
self
}
pub fn set_env_vars_tuple(&mut self, env_vars: &[(&str, &str)]) -> &mut Self {
self.env_vars = env_vars
.iter()
.map(|(key, value)| KeyVal {
key: key.to_string(),
value: value.to_string(),
})
.collect();
self
}
pub fn add_child_dir_all(&self, rel_path: impl AsRef<Path>) -> PathBuf {
let rel_path = rel_path.as_ref();
let child_dir = self.temp_path().join(rel_path);
if let Err(err) = fs::create_dir_all(&child_dir) {
panic!(
"unable to create child directory: `{}` in `{}`: {}",
self.temp_path().display(),
rel_path.display(),
err
);
}
child_dir
}
pub fn temp_path(&self) -> &Path {
self.temp_dir.path()
}
pub fn work_dir(&self) -> &Path {
&self.work_dir
}
pub fn env_vars(&self) -> &[KeyVal] {
&self.env_vars
}
pub fn envfile_contents(&self) -> Option<&str> {
self.envfile_contents.as_deref()
}
pub fn envfile_path(&self) -> &Path {
&self.envfile_path
}
}
impl Default for TestEnv {
fn default() -> Self {
let temp_dir = tempdir().expect("create tempdir");
let work_dir = temp_dir.path().to_owned();
let env_vars = vec![KeyVal {
key: TEST_EXISTING_KEY.into(),
value: TEST_EXISTING_VALUE.into(),
}];
let envfile_contents = Some(create_default_envfile());
let envfile_path = work_dir.join(".env");
Self {
temp_dir,
work_dir,
env_vars,
envfile_contents,
envfile_path,
}
}
}
impl From<(&str, &str)> for KeyVal {
fn from(kv: (&str, &str)) -> Self {
let (key, value) = kv;
Self {
key: key.to_string(),
value: value.to_string(),
}
}
}
impl From<(String, String)> for KeyVal {
fn from(kv: (String, String)) -> Self {
let (key, value) = kv;
Self { key, value }
}
}
fn get_env_locker() -> Arc<Mutex<EnvMap>> {
Arc::clone(ENV_LOCKER.get_or_init(|| {
let map: EnvMap = env::vars().collect();
Arc::new(Mutex::new(map))
}))
}
fn reset_env(original_env: &EnvMap) {
env::vars()
.filter(|(key, _)| !original_env.contains_key(key))
.for_each(|(key, _)| env::remove_var(key));
original_env
.iter()
.for_each(|(key, value)| env::set_var(key, value));
}
fn create_env(test_env: &TestEnv) {
if let Some(contents) = test_env.envfile_contents() {
create_envfile(&test_env.envfile_path, contents);
}
env::set_current_dir(&test_env.work_dir).expect("setting working directory");
for KeyVal { key, value } in &test_env.env_vars {
env::set_var(key, value)
}
}
fn create_envfile(path: &Path, contents: &str) {
if path.exists() {
panic!("envfile `{}` already exists", path.display())
}
fn create_env_file_inner(path: &Path, contents: &str) -> io::Result<()> {
let mut file = fs::File::create(path)?;
file.write_all(contents.as_bytes())?;
file.sync_all()
}
if let Err(err) = create_env_file_inner(path, contents) {
panic!("error creating envfile `{}`: {}", path.display(), err);
}
}