use assert_fs::fixture::FileTouch;
use cli_utils::BoxResult;
use fake::uuid::UUIDv5;
use fake::Fake;
use projvar::var;
use regex::Regex;
use uuid::Uuid;
use assert_cmd::prelude::*;
use std::ffi::OsStr;
use std::fs;
use std::path::Path;
use std::path::PathBuf;
use std::sync::LazyLock;
use std::{collections::HashMap, fmt::Display, process::Command};
pub static R_DATE_TIME: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"^[12][0-9]{3}-[01]?[0-9]-[0-3]?[0-9] [012]?[0-9]:[0-5]?[0-9]:[0-5]?[0-9]$")
.unwrap()
});
pub static R_NON_EMPTY: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^.+$").unwrap());
pub static R_BOOL: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^(true|false)$").unwrap());
pub fn random_uuid() -> String {
UUIDv5.fake::<Uuid>().to_string()
}
pub trait StrMatcher: Display {
fn matches(&self, text: &str) -> bool;
}
impl StrMatcher for Regex {
fn matches(&self, text: &str) -> bool {
self.is_match(text)
}
}
impl StrMatcher for &Regex {
fn matches(&self, text: &str) -> bool {
self.is_match(text)
}
}
impl StrMatcher for &str {
fn matches(&self, text: &str) -> bool {
self == &text
}
}
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error(
"For key '{key}' we expected value '{expected_pat}', but actual value is '{actual_val}'."
)]
ValuesDiffer {
key: &'static str,
expected_pat: String,
actual_val: String,
},
#[error("For key '{key}', value '{expected_pat}' was expected, but none was produced.")]
MissingValue {
key: &'static str,
expected_pat: String,
},
#[error("For key '{key}', no value was expected, but '{actual_val}' was produced.")]
Unexpected { key: String, actual_val: String },
}
#[derive(thiserror::Error, Debug)]
#[error("{children:#?}")]
pub struct Errors {
pub children: Vec<Error>,
}
pub fn compare(
expected: &HashMap<&'static str, (Box<&'static dyn StrMatcher>, bool)>,
actual: &mut HashMap<String, String>,
) -> Result<(), Errors> {
let mut errors = vec![];
for (key, (expected_pat, required)) in expected.iter() {
let actual_val = actual.remove(key.to_owned());
match actual_val {
Some(actual_val) => {
if !expected_pat.matches(&actual_val) {
errors.push(Error::ValuesDiffer {
key,
expected_pat: expected_pat.to_string(),
actual_val,
});
}
}
None => {
if *required {
errors.push(Error::MissingValue {
key,
expected_pat: expected_pat.to_string(),
});
}
}
}
}
for (key, actual_val) in actual.iter() {
errors.push(Error::Unexpected {
key: key.to_string(),
actual_val: actual_val.to_string(),
}); }
if errors.is_empty() {
Ok(())
} else {
Err(Errors { children: errors })
}
}
fn projvar_test_internal<I, K, V>(
expected_pats: &HashMap<&'static str, (Box<&'static dyn StrMatcher>, bool)>,
args: &[&str],
cwd: &Path,
envs: I,
debug: bool,
) -> BoxResult<()>
where
I: IntoIterator<Item = (K, V)>,
K: AsRef<OsStr>,
V: AsRef<OsStr>,
{
let tmp_out_file = assert_fs::NamedTempFile::new("projvar.out.env")?;
tmp_out_file.touch()?;
let out_file = if debug {
let out_file = PathBuf::from("/tmp/projvar-test-out.env");
if out_file.exists() {
fs::remove_file(&out_file)?;
}
out_file
} else {
tmp_out_file.path().to_path_buf()
};
let out_file_str = &out_file.display().to_string();
let mut cmd = Command::cargo_bin("projvar")?;
cmd.arg("-O").arg(&out_file_str);
if debug {
cmd.arg("-A").arg("/tmp/pv-dbg-out-all.md");
cmd.arg("-F").arg("trace");
cmd.arg("-F").arg("warnings");
}
cmd.current_dir(cwd);
cmd.args(args);
cmd.env_clear();
cmd.envs(envs);
if debug {
let output = cmd.output()?;
let stdout_utf8 = std::str::from_utf8(&output.stdout)?;
println!("{stdout_utf8}");
} else {
cmd.assert().success();
}
assert!(out_file.exists());
let mut output_reader = cli_utils::create_input_reader(Some(&out_file_str))?;
let mut actual_vars = var::parse_vars_file_reader(&mut output_reader)?;
compare(expected_pats, &mut actual_vars)?;
Ok(())
}
pub fn projvar_test<I, K, V>(
expected_pats: &HashMap<&'static str, (Box<&'static dyn StrMatcher>, bool)>,
args: &[&str],
cwd: &Path,
envs: I,
) -> BoxResult<()>
where
I: IntoIterator<Item = (K, V)>,
K: AsRef<OsStr>,
V: AsRef<OsStr>,
{
projvar_test_internal(expected_pats, args, cwd, envs, false)
}
pub fn projvar_test_clean(
expected_pats: &HashMap<&'static str, (Box<&'static dyn StrMatcher>, bool)>,
args: &[&str],
) -> BoxResult<()> {
let tmp_proj_dir_empty = assert_fs::TempDir::new()?;
projvar_test_internal(
expected_pats,
args,
tmp_proj_dir_empty.path(),
HashMap::<String, String>::new(),
false,
)
}