//! Shared helpers for unit tests.
//!
//! Compiled only under `cfg(test)`, so nothing here ships in the binary.
use std::path::{Path, PathBuf};
/// A scratch directory that removes itself when it goes out of scope —
/// including when the scope is left by a panicking assertion.
///
/// Cleaning up with a `remove_dir_all` at the end of a test only works when the
/// test passes, which is exactly backwards: the runs that leave debris are the
/// failing ones. Anvil's own runners are stateful and long-lived, so `/tmp` is
/// not reset between jobs and a repeatedly failing test accumulates directories
/// (some holding the 100MB fixture the artifact size-cap test writes) until a
/// later job on that worker dies of ENOSPC.
///
/// The name is derived from `tag` and the pid rather than a timestamp, so it is
/// *reclaimable*: a directory orphaned by a hard kill — which no `Drop` can
/// defend against — is cleared by the next run of the same test instead of
/// living forever. `tag` must therefore be unique per test within a file, since
/// tests run in parallel.
pub struct TempDir {
path: PathBuf,
}
impl TempDir {
pub fn new(tag: &str) -> Self {
let path = std::env::temp_dir().join(format!("anvil-t-{tag}-{}", std::process::id()));
// Clear anything a previously killed run left behind, so each test
// starts from a known-empty tree.
let _ = std::fs::remove_dir_all(&path);
std::fs::create_dir_all(&path).expect("create scratch directory");
Self { path }
}
pub fn path(&self) -> &Path {
&self.path
}
/// The scratch directory with symlinks resolved — what confinement checks
/// and `strip_prefix` comparisons need, since `/tmp` is itself a symlink on
/// some platforms.
pub fn canonical(&self) -> PathBuf {
self.path.canonicalize().expect("canonicalize scratch dir")
}
pub fn join(&self, rel: &str) -> PathBuf {
self.path.join(rel)
}
/// Write `contents` to `rel`, creating parent directories. Returns the path.
pub fn write(&self, rel: &str, contents: &str) -> PathBuf {
let p = self.path.join(rel);
if let Some(parent) = p.parent() {
std::fs::create_dir_all(parent).expect("create parent directory");
}
std::fs::write(&p, contents).expect("write fixture file");
p
}
}
impl Drop for TempDir {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.path);
}
}
/// True when the process is running as root, where discretionary permission
/// checks are bypassed. Tests whose whole point is "this is refused" have to
/// skip rather than silently pass for the wrong reason — CI runs as root in a
/// container.
#[cfg(unix)]
pub fn running_as_root() -> bool {
// Safe: getuid has no preconditions and cannot fail.
(unsafe { libc::getuid() }) == 0
}