use std::path::{Path, PathBuf};
use std::sync::Once;
use std::time::{Duration, SystemTime};
use tempfile::TempDir;
const DEAD_LOOPBACK_PORT: u16 = 1;
const _: () = assert!(
DEAD_LOOPBACK_PORT < 1024,
"DEAD_LOOPBACK_PORT must stay below 1024 so binding it requires root and the \
OS never hands it out as an ephemeral port (#4306/#4415)"
);
static DEAD_PORT_VERIFIED: Once = Once::new();
pub(crate) fn dead_loopback_url() -> String {
DEAD_PORT_VERIFIED.call_once(|| {
let addr = std::net::SocketAddr::from((std::net::Ipv4Addr::LOCALHOST, DEAD_LOOPBACK_PORT));
match std::net::TcpStream::connect_timeout(&addr, Duration::from_secs(10)) {
Ok(_) => panic!(
"test_support::dead_loopback_url(): something is LISTENING on {addr}, so the \
dead-address fixture's premise is void (#4306/#4415). Stop that listener, or \
pick another privileged, non-ephemeral loopback port for DEAD_LOOPBACK_PORT."
),
Err(e) if e.kind() == std::io::ErrorKind::ConnectionRefused => {}
Err(e) => panic!(
"test_support::dead_loopback_url(): {addr} neither accepted nor REFUSED the \
connect — it failed with {:?} ({e}) (#4306/#4415). The fixture requires a \
prompt ECONNREFUSED; a dropped SYN (TimedOut) would make every daemon-down \
test slow and would hand the error-classification tests the wrong error kind.",
e.kind()
),
}
});
format!("http://127.0.0.1:{DEAD_LOOPBACK_PORT}")
}
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 dead_loopback_url_refuses_every_connect() {
let addr: std::net::SocketAddr = dead_loopback_url()
.trim_start_matches("http://")
.parse()
.expect("the fixture's url must be host:port after the scheme");
for attempt in 0..50 {
let err = std::net::TcpStream::connect_timeout(&addr, Duration::from_secs(10))
.expect_err("nothing may be listening on the dead-address port");
assert_eq!(
err.kind(),
std::io::ErrorKind::ConnectionRefused,
"attempt {attempt} to {addr} must be REFUSED (RST), not dropped: {err:?}"
);
}
}
#[test]
fn dead_loopback_port_is_not_bindable_or_ephemeral() {
let addr = std::net::SocketAddr::from((std::net::Ipv4Addr::LOCALHOST, DEAD_LOOPBACK_PORT));
assert!(
std::net::TcpListener::bind(addr).is_err(),
"an unprivileged bind of {addr} must fail — a bindable dead-address port is \
exactly the invalidated premise #4306 describes"
);
for _ in 0..2_000 {
if let Ok(other) = std::net::TcpListener::bind("127.0.0.1:0") {
assert_ne!(
other.local_addr().expect("local addr").port(),
DEAD_LOOPBACK_PORT,
"the OS handed a competing binder the dead-address port"
);
}
}
}
#[test]
fn stale_after_is_positive_duration() {
assert!(STALE_AFTER > Duration::from_secs(0));
assert!(SystemTime::now().duration_since(UNIX_EPOCH).is_ok());
}
}