use std::{
fs,
ops::Deref,
path::{Path, PathBuf},
sync::atomic::{AtomicU64, Ordering},
};
static TEMP_COUNTER: AtomicU64 = AtomicU64::new(0);
pub struct TestTempDir {
path: PathBuf,
kept: bool,
}
impl TestTempDir {
pub fn new(label: &str) -> Self {
let nonce = TEMP_COUNTER.fetch_add(1, Ordering::Relaxed);
let path = std::env::temp_dir()
.join("subc-tests")
.join(format!("{label}-{}-{nonce}", std::process::id()));
fs::create_dir_all(&path).expect("create test temp dir");
Self { path, kept: false }
}
pub fn path(&self) -> &Path {
&self.path
}
pub fn keep(mut self) -> PathBuf {
self.kept = true;
self.path.clone()
}
}
impl Deref for TestTempDir {
type Target = Path;
fn deref(&self) -> &Path {
&self.path
}
}
impl Drop for TestTempDir {
fn drop(&mut self) {
if self.kept {
return;
}
if std::thread::panicking() {
eprintln!("TestTempDir preserved on panic: {}", self.path.display());
return;
}
let _ = fs::remove_dir_all(&self.path);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn success_drop_removes_the_tree() {
let dir = TestTempDir::new("lifecycle-success");
let path = dir.path().to_path_buf();
assert!(path.exists());
drop(dir);
assert!(!path.exists(), "guard drop must remove the tree");
}
#[test]
fn panic_preserves_the_tree_and_prints_the_path() {
let path = std::thread::spawn(|| {
let dir = TestTempDir::new("lifecycle-panic");
let path = dir.path().to_path_buf();
panic!("intentional test panic with path {}", path.display());
})
.join()
.expect_err("the spawned thread must panic");
let message = path
.downcast_ref::<String>()
.map(String::as_str)
.unwrap_or("<non-string panic payload>");
assert!(
message.contains("lifecycle-panic"),
"panic payload should name the dir: {message}"
);
let survived = std::env::temp_dir()
.join("subc-tests")
.read_dir()
.unwrap()
.filter_map(|entry| entry.ok())
.map(|entry| entry.file_name().to_string_lossy().into_owned())
.find(|name| name.contains("lifecycle-panic"));
assert!(
survived.is_some(),
"a panicking thread's guard must leave its tree in place"
);
let dir = std::env::temp_dir()
.join("subc-tests")
.join(survived.unwrap());
fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn keep_preserves_without_panic() {
let dir = TestTempDir::new("lifecycle-keep");
let path = dir.keep();
assert!(path.exists(), "keep() must leave the tree in place");
fs::remove_dir_all(&path).unwrap();
}
}