use std::path::{Path, PathBuf};
use std::sync::Once;
use std::time::{Duration, SystemTime};
use tempfile::TempDir;
pub(crate) const TEST_DIR_PREFIX: &str = "tm-test-";
const STALE_AFTER: Duration = Duration::from_secs(24 * 60 * 60);
static SWEEP_ONCE: Once = Once::new();
fn real_system_tmp() -> PathBuf {
#[cfg(unix)]
{
PathBuf::from("/tmp")
}
#[cfg(not(unix))]
{
std::env::temp_dir()
}
}
fn is_meaningful_home_boundary(home: &Path, candidate: &Path) -> bool {
home.components().count() > 1 && home != candidate
}
fn guard_against_project_tree(root: &Path) {
let Some(home) = std::env::var_os("HOME") else {
return;
};
let home = PathBuf::from(home);
if is_meaningful_home_boundary(&home, root) && root.starts_with(&home) {
panic!(
"hermetic test temp root {root:?} resolves inside $HOME ({home:?}) — \
refusing to risk littering a project tree (see #3382)"
);
}
}
pub(crate) fn hermetic_temp_dir() -> TempDir {
SWEEP_ONCE.call_once(sweep_stale_test_dirs);
let root = real_system_tmp();
guard_against_project_tree(&root);
tempfile::Builder::new()
.prefix(TEST_DIR_PREFIX)
.tempdir_in(&root)
.expect("create hermetic test temp dir")
}
fn sweep_stale_test_dirs() {
let root = real_system_tmp();
let Ok(entries) = std::fs::read_dir(&root) else {
return;
};
let now = SystemTime::now();
for entry in entries.flatten() {
let Some(name) = entry.file_name().to_str().map(str::to_owned) else {
continue;
};
if !name.starts_with(TEST_DIR_PREFIX) {
continue;
}
let is_stale = entry
.metadata()
.and_then(|meta| meta.modified())
.ok()
.and_then(|modified| now.duration_since(modified).ok())
.is_some_and(|age| age > STALE_AFTER);
if is_stale {
let _ = std::fs::remove_dir_all(entry.path());
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::UNIX_EPOCH;
#[test]
fn real_system_tmp_ignores_tmpdir_env() {
let root = real_system_tmp();
#[cfg(unix)]
assert_eq!(root, PathBuf::from("/tmp"));
assert!(root.exists(), "hermetic root must exist: {root:?}");
}
#[serial_test::serial]
#[test]
fn hermetic_temp_dir_is_prefixed_and_outside_home() {
let dir = hermetic_temp_dir();
let name = dir
.path()
.file_name()
.and_then(|n| n.to_str())
.unwrap_or_default();
assert!(
name.starts_with(TEST_DIR_PREFIX),
"expected {name:?} to start with {TEST_DIR_PREFIX:?}"
);
if let Some(home) = std::env::var_os("HOME") {
let home = PathBuf::from(home);
if is_meaningful_home_boundary(&home, &real_system_tmp()) {
assert!(
!dir.path().starts_with(&home),
"hermetic dir must never resolve inside $HOME: {:?}",
dir.path()
);
}
}
}
struct HomeOverride {
prev: Option<std::ffi::OsString>,
}
impl Drop for HomeOverride {
fn drop(&mut self) {
match self.prev.take() {
Some(v) => unsafe { std::env::set_var("HOME", v) },
None => unsafe { std::env::remove_var("HOME") },
}
}
}
fn override_home(value: &str) -> HomeOverride {
let prev = std::env::var_os("HOME");
unsafe { std::env::set_var("HOME", value) };
HomeOverride { prev }
}
#[serial_test::serial]
#[test]
#[should_panic(expected = "resolves inside $HOME")]
fn guard_panics_on_genuine_home_containment() {
let _home = override_home("/Users/test-user");
guard_against_project_tree(&PathBuf::from("/Users/test-user/trusty-mpm-projects"));
}
#[serial_test::serial]
#[test]
fn guard_does_not_panic_when_home_is_tmp() {
let _home = override_home("/tmp");
guard_against_project_tree(&PathBuf::from("/tmp"));
}
#[serial_test::serial]
#[test]
fn guard_does_not_panic_when_home_is_root() {
let _home = override_home("/");
guard_against_project_tree(&PathBuf::from("/tmp"));
}
#[test]
fn sweep_removes_only_stale_prefixed_dirs() {
let root = real_system_tmp();
let stale = root.join(format!(
"{TEST_DIR_PREFIX}sweep-stale-{}",
std::process::id()
));
let fresh = root.join(format!(
"{TEST_DIR_PREFIX}sweep-fresh-{}",
std::process::id()
));
let unrelated = root.join(format!("not-tm-prefixed-{}", std::process::id()));
std::fs::create_dir_all(&stale).unwrap();
std::fs::create_dir_all(&fresh).unwrap();
std::fs::create_dir_all(&unrelated).unwrap();
let two_days_ago = SystemTime::now() - Duration::from_secs(2 * 24 * 60 * 60);
let times = std::fs::FileTimes::new().set_modified(two_days_ago);
std::fs::File::open(&stale)
.unwrap()
.set_times(times)
.unwrap();
sweep_stale_test_dirs();
assert!(!stale.exists(), "stale tm-test- dir should be swept");
assert!(fresh.exists(), "fresh tm-test- dir should survive");
assert!(unrelated.exists(), "non-prefixed dir must never be touched");
let _ = std::fs::remove_dir_all(&fresh);
let _ = std::fs::remove_dir_all(&unrelated);
let _ = std::fs::remove_dir_all(&stale);
}
#[test]
fn stale_after_is_positive_duration() {
assert!(STALE_AFTER > Duration::from_secs(0));
assert!(SystemTime::now().duration_since(UNIX_EPOCH).is_ok());
}
}