use std::cell::RefCell;
use std::ops::{Deref, DerefMut};
use std::path::Path;
use std::sync::Arc;
use tempfile::TempDir;
pub(crate) struct TrackedOwner<T> {
value: T,
liveness: Arc<()>,
label: &'static str,
}
impl<T> TrackedOwner<T> {
pub(crate) fn new(label: &'static str, value: T) -> Self {
Self {
value,
liveness: Arc::new(()),
label,
}
}
}
impl<T> Deref for TrackedOwner<T> {
type Target = T;
fn deref(&self) -> &T {
&self.value
}
}
impl<T> DerefMut for TrackedOwner<T> {
fn deref_mut(&mut self) -> &mut T {
&mut self.value
}
}
type OwnerProbe = (&'static str, Box<dyn Fn() -> bool>);
pub(crate) struct TempGraphDir {
dir: Option<TempDir>,
watched: RefCell<Vec<OwnerProbe>>,
}
impl TempGraphDir {
pub(crate) fn new() -> Self {
Self {
dir: Some(TempDir::new().expect("failed to create temp dir")),
watched: RefCell::new(Vec::new()),
}
}
pub(crate) fn path(&self) -> &Path {
self.dir
.as_ref()
.expect("temp dir already removed by remove_now()")
.path()
}
pub(crate) fn watch<T>(&self, owner: &TrackedOwner<T>) {
let token = Arc::downgrade(&owner.liveness);
self.watched
.borrow_mut()
.push((owner.label, Box::new(move || token.strong_count() > 0)));
}
pub(crate) fn own<T>(&self, label: &'static str, value: T) -> TrackedOwner<T> {
let owner = TrackedOwner::new(label, value);
self.watch(&owner);
owner
}
pub(crate) fn watch_arc<T: 'static>(&self, label: &'static str, shared: &Arc<T>) {
let token = Arc::downgrade(shared);
self.watched
.borrow_mut()
.push((label, Box::new(move || token.strong_count() > 0)));
}
pub(crate) fn remove_now(mut self) {
self.assert_owners_dropped("removing the temp directory");
if let Some(dir) = self.dir.take() {
dir.close().expect("temp dir removal failed");
}
}
fn live_owners(&self) -> Vec<&'static str> {
self.watched
.borrow()
.iter()
.filter(|(_, still_alive)| still_alive())
.map(|(label, _)| *label)
.collect()
}
fn assert_owners_dropped(&self, action: &str) {
let live = self.live_owners();
assert!(
live.is_empty(),
"drop-order violation: {action} while {} owner(s) still hold mappings into it: \
{live:?}. Drop the graph/mapped owner first — declare the temp dir before it, or \
call drop() explicitly before cleanup.",
live.len(),
);
}
}
impl Drop for TempGraphDir {
fn drop(&mut self) {
if std::thread::panicking() {
return;
}
self.assert_owners_dropped("dropping the temp directory");
}
}
#[cfg(test)]
mod seam_tests {
use super::*;
#[test]
fn removal_while_owner_alive_is_detected() {
let result = std::panic::catch_unwind(|| {
let owner = TrackedOwner::new("probe owner", ());
let dir = TempGraphDir::new();
dir.watch(&owner);
dir.remove_now();
drop(owner);
});
let payload = result.expect_err("removal with a live owner must panic");
let message = payload
.downcast_ref::<String>()
.map(String::as_str)
.unwrap_or("<non-string panic>");
assert!(
message.contains("drop-order violation"),
"unexpected panic: {message}"
);
}
#[test]
fn surviving_arc_clone_is_detected() {
let result = std::panic::catch_unwind(|| {
let dir = TempGraphDir::new();
let shared = Arc::new(7u8);
dir.watch_arc("shared probe", &shared);
let leaked = Arc::clone(&shared);
drop(shared);
dir.remove_now();
drop(leaked);
});
let payload = result.expect_err("a surviving Arc clone must panic");
let message = payload
.downcast_ref::<String>()
.map(String::as_str)
.unwrap_or("<non-string panic>");
assert!(
message.contains("drop-order violation"),
"unexpected panic: {message}"
);
}
#[test]
fn owner_dropped_first_is_accepted() {
let dir = TempGraphDir::new();
let owner = TrackedOwner::new("probe owner", ());
dir.watch(&owner);
drop(owner);
dir.remove_now();
}
}