use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
pub fn path(name: &str) -> PathBuf {
static SEQ: AtomicU64 = AtomicU64::new(0);
std::env::temp_dir().join(format!(
"cn-{}-{}-{name}",
std::process::id(),
SEQ.fetch_add(1, Ordering::Relaxed)
))
}
pub struct Scratch {
path: PathBuf,
directory: bool,
}
impl Scratch {
pub fn file(name: &str) -> Self {
Self {
path: path(name),
directory: false,
}
}
pub fn dir(name: &str) -> std::io::Result<Self> {
let path = path(name);
std::fs::create_dir(&path)?;
Ok(Self {
path,
directory: true,
})
}
pub fn path(&self) -> &Path {
&self.path
}
}
impl Drop for Scratch {
fn drop(&mut self) {
let _ = if self.directory {
std::fs::remove_dir_all(&self.path)
} else {
std::fs::remove_file(&self.path)
};
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_path_is_never_handed_out_twice() {
let first = path("thing");
let second = path("thing");
assert_ne!(first, second, "two calls must not share a path");
assert!(
first.parent().is_some_and(|p| second.starts_with(p)),
"both live in the temporary directory"
);
}
#[test]
fn a_name_keeps_its_extension() {
let path = path("My-Game.iconset");
assert_eq!(path.extension().and_then(|e| e.to_str()), Some("iconset"));
}
#[test]
fn a_name_carries_the_process_that_made_it() {
let path = path("thing");
assert!(
path.file_name()
.and_then(|n| n.to_str())
.is_some_and(|n| n.starts_with(&format!("cn-{}-", std::process::id()))),
"got {}",
path.display()
);
}
#[test]
fn a_file_goes_when_its_scratch_does() {
let scratch = Scratch::file("leftover");
std::fs::write(scratch.path(), b"work").expect("write");
let path = scratch.path().to_path_buf();
assert!(path.is_file());
drop(scratch);
assert!(!path.exists(), "the file went with the guard");
}
#[test]
fn a_directory_goes_with_everything_in_it() {
let scratch = Scratch::dir("work").expect("create");
std::fs::write(scratch.path().join("inner"), b"work").expect("write");
let path = scratch.path().to_path_buf();
assert!(path.is_dir());
drop(scratch);
assert!(!path.exists(), "the tree went with the guard");
}
#[test]
fn a_path_that_was_never_written_drops_quietly() {
let scratch = Scratch::file("never-written");
let path = scratch.path().to_path_buf();
drop(scratch);
assert!(!path.exists());
}
}