tests: every temporary path is removed by a guard; no leftovers in /tmp

About 10,000 test leftovers had built up in titan's /tmp. loopd's Home
and FakeServer, its fake broker's socket, bxctl's temp sockets, fake
loopd and command homes, inferproxy's socket, and a few brokerd and
toolkit tests created uniquely named directories or sockets and never
removed them, or removed them by hand at the end, which a failing test
skips. Each is now a guard removed on drop (bxctl gains
tests/support/temp.rs). Two races also left gatewayd homes behind: the
test Running only signalled its server to stop, and two Stage structs
declared the home first, so it was removed while the server could still
write into it. Running's drop now waits for the thread (with a 5-second
limit), and every home is declared last.

Checked: make gate twice, then the gate and three rounds of gatewayd and
loopd tests; no new entry in /tmp; 944 tests pass.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-26 01:29:32 -07:00
co-authored by Claude Opus 5.5
parent 53532e04ff
commit a15c428ea6
19 changed files with 230 additions and 51 deletions
+3 -3
View File
@@ -135,7 +135,7 @@ fn an_expiry_that_has_already_passed_still_leaves_the_timeout() {
fn no_socket_is_unavailable() {
let socket = socket_path();
let _ = std::fs::remove_file(&socket);
let got = call(socket.clone(), 2_000, &request());
let got = call(socket.to_path_buf(), 2_000, &request());
assert_unavailable(&got, "no socket file");
assert!(
got.lines[0].contains(&socket.display().to_string()),
@@ -260,7 +260,7 @@ fn a_request_too_large_for_a_frame_is_its_own_failure() {
let _listener = UnixListener::bind(&path).unwrap();
let mut big = request();
big.arguments = format!(r#"{{"path":"/{}"}}"#, "a".repeat(proto::MAX_FRAME));
let got = call(path, 1_000, &big);
let got = call(path.to_path_buf(), 1_000, &big);
assert_eq!(
got.response,
ToolResponse::Failed {
@@ -291,7 +291,7 @@ fn every_call_is_its_own_connection() {
);
}
});
let port = BrokerPort::new(path, Duration::from_millis(2_000));
let port = BrokerPort::new(path.to_path_buf(), Duration::from_millis(2_000));
for n in 0..3 {
let got = port.call(&request(), &mut |_| {});
assert_eq!(got, result(&format!("answer {n}")));
+3 -1
View File
@@ -23,9 +23,11 @@ use support::{FakeServer, Home, Reply, ScriptedPort};
const CHAT: &str = "/v1/chat/completions";
struct Loopd {
home: Home,
server: FakeServer,
socket: PathBuf,
// Declared last so it is dropped last: fields drop in order, and the home must outlive
// everything that may still write into it (tip T44).
home: Home,
}
fn start(replies: Vec<proto::ToolResponse>) -> Loopd {
+5
View File
@@ -59,6 +59,11 @@ impl Proxy {
impl Drop for Proxy {
fn drop(&mut self) {
self.kill();
// `socket_path` made a directory of its own for the socket; remove it with the proxy
// (tip T44).
if let Some(dir) = self.socket.parent() {
let _ = std::fs::remove_dir_all(dir);
}
}
}
+3 -1
View File
@@ -44,10 +44,12 @@ impl MemorySource for Shared {
}
struct Loopd {
_home: Home,
server: FakeServer,
socket: PathBuf,
memory: Arc<Recorder>,
// Declared last so it is dropped last: fields drop in order, and the home must outlive
// everything that may still write into it (tip T44).
_home: Home,
}
fn start(block: Result<&str, &str>) -> Loopd {
+29 -4
View File
@@ -6,7 +6,7 @@
#![allow(dead_code)] // each test file uses a different part of this module
use std::os::unix::net::{UnixListener, UnixStream};
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::{Arc, Mutex};
use std::thread::{self, JoinHandle};
@@ -21,9 +21,31 @@ use proto::{
static NEXT: AtomicU32 = AtomicU32::new(0);
pub fn socket_path() -> PathBuf {
/// A fresh socket path, removed when the value is dropped (tip T44).
pub struct SocketPath(PathBuf);
impl std::ops::Deref for SocketPath {
type Target = Path;
fn deref(&self) -> &Path {
&self.0
}
}
impl AsRef<Path> for SocketPath {
fn as_ref(&self) -> &Path {
&self.0
}
}
impl Drop for SocketPath {
fn drop(&mut self) {
let _ = std::fs::remove_file(&self.0);
}
}
pub fn socket_path() -> SocketPath {
let n = NEXT.fetch_add(1, Ordering::SeqCst);
std::env::temp_dir().join(format!("loopd-broker-{}-{n}.sock", std::process::id()))
SocketPath(std::env::temp_dir().join(format!("loopd-broker-{}-{n}.sock", std::process::id())))
}
/// Accepts one connection, reads the request frame and hands both to `script`. The connection
@@ -35,13 +57,16 @@ where
let path = socket_path();
let _ = std::fs::remove_file(&path);
let listener = UnixListener::bind(&path).unwrap();
let returned = path.to_path_buf();
// The thread owns the path, so the socket file goes once the exchange is over.
let handle = thread::spawn(move || {
let _path = path;
let (mut stream, _) = listener.accept().unwrap();
let request = read_frame(&mut stream).unwrap();
script(&mut stream, &request);
request
});
(path, handle)
(returned, handle)
}
pub fn frame(id: u64, r#final: bool, msg: Message) -> Envelope {
+23 -1
View File
@@ -281,12 +281,14 @@ struct State {
pub struct FakeServer {
pub socket: PathBuf,
state: Arc<State>,
dir: PathBuf,
}
impl FakeServer {
pub fn start() -> FakeServer {
let n = NEXT.fetch_add(1, Ordering::SeqCst);
let dir = std::env::temp_dir().join(format!("loopd-fake-{}-{n}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let socket = dir.join("infer.sock");
let _ = std::fs::remove_file(&socket);
@@ -303,7 +305,7 @@ impl FakeServer {
thread::spawn(move || serve(stream, &state));
}
});
FakeServer { socket, state }
FakeServer { socket, state, dir }
}
/// Replies for `path` (the query is ignored), served in order. The last one repeats.
@@ -401,3 +403,23 @@ fn serve(mut stream: UnixStream, state: &State) {
}
thread::sleep(Duration::from_millis(reply.hang_ms));
}
/// Removes a test's directory, first putting back the permissions a test may have taken away,
/// or the removal fails (tip T44).
fn remove_test_dir(dir: &Path) {
use std::os::unix::fs::PermissionsExt;
let _ = std::fs::set_permissions(dir, std::fs::Permissions::from_mode(0o700));
let _ = std::fs::remove_dir_all(dir);
}
impl Drop for Home {
fn drop(&mut self) {
remove_test_dir(&self.dir);
}
}
impl Drop for FakeServer {
fn drop(&mut self) {
remove_test_dir(&self.dir);
}
}