use std::collections::BTreeSet;
use std::sync::Mutex;
pub static ENV_MUTEX: Mutex<()> = Mutex::new(());
pub const CONFIG_ENV_VARS: &[&str] = &[
"VIPUNE_DATABASE_PATH",
"VIPUNE_EMBEDDING_MODEL",
"VIPUNE_SIMILARITY_THRESHOLD",
"VIPUNE_RECENCY_WEIGHT",
"VIPUNE_HYBRID",
"VIPUNE_DECAY_REFRESH_DAYS",
"VIPUNE_PROMOTION_THRESHOLD",
"VIPUNE_PRUNE_RETRIEVAL_LIMIT",
"VIPUNE_PRUNE_MIN_AGE_DAYS",
];
pub fn cleanup_env_vars(vars: &[&str]) {
for var in vars {
unsafe {
std::env::remove_var(var);
}
}
}
pub fn cleanup_config_env_vars() {
cleanup_env_vars(CONFIG_ENV_VARS);
}
#[must_use = "the guard is the lock: a discarded guard releases it and skips the exit-clear"]
pub struct ConfigEnvGuard {
_lock: std::sync::MutexGuard<'static, ()>,
}
impl ConfigEnvGuard {
#[must_use = "hold the guard for the whole test body; discarding it releases the lock immediately"]
pub fn acquire() -> Self {
let _lock = lock_env_mutex();
cleanup_config_env_vars();
Self { _lock }
}
}
impl Drop for ConfigEnvGuard {
fn drop(&mut self) {
cleanup_config_env_vars();
}
}
#[must_use = "hold the guard while the environment is mutated; discarding it releases the lock immediately"]
pub fn lock_env_mutex() -> std::sync::MutexGuard<'static, ()> {
ENV_MUTEX
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
#[test]
fn test_config_env_vars_match_env_parser() {
const ENV_PARSER: &str = include_str!("env_parser.rs");
let apply_fns = ENV_PARSER
.lines()
.filter(|line| {
let trimmed = line.trim_start();
trimmed.starts_with("pub fn apply_") && trimmed.contains("_override(")
})
.count();
assert_eq!(
CONFIG_ENV_VARS.len(),
apply_fns,
"`CONFIG_ENV_VARS` is out of step with the `apply_*_override` functions in \
`config/env_parser.rs` — add (or remove) the matching knob"
);
let parsed = vipune_knob_names(ENV_PARSER);
let declared: BTreeSet<String> = CONFIG_ENV_VARS
.iter()
.map(|&name| name.to_string())
.collect();
assert_eq!(
parsed, declared,
"`CONFIG_ENV_VARS` and the `VIPUNE_*` names spelled in `config/env_parser.rs` \
differ — left are the names the parser reads, right the names these helpers clear"
);
}
fn vipune_knob_names(source: &str) -> BTreeSet<String> {
const PREFIX: &[u8] = b"VIPUNE_";
let bytes = source.as_bytes();
let mut names = BTreeSet::new();
for (at, _) in bytes
.windows(PREFIX.len())
.enumerate()
.filter(|(_, window)| *window == PREFIX)
{
let mut end = at + PREFIX.len();
while end < bytes.len() && (bytes[end].is_ascii_uppercase() || bytes[end] == b'_') {
end += 1;
}
names.insert(String::from_utf8_lossy(&bytes[at..end]).into_owned());
}
names
}
#[test]
fn test_cleanup_env_vars() {
let _guard = lock_env_mutex();
unsafe {
std::env::set_var("VIPUNE_TEST_VAR", "test_value");
}
cleanup_env_vars(&["VIPUNE_TEST_VAR"]);
assert!(std::env::var("VIPUNE_TEST_VAR").is_err());
}