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:
@@ -1,5 +1,8 @@
|
|||||||
//! `[runner]` in `brokerd.toml` (M3b spec, section 6). Do not edit.
|
//! `[runner]` in `brokerd.toml` (M3b spec, section 6). Do not edit.
|
||||||
|
|
||||||
|
#[path = "support/tmp.rs"]
|
||||||
|
mod tmp;
|
||||||
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
@@ -88,8 +91,7 @@ fn unknown_runner_keys_are_errors() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn bad_values_are_errors_that_name_them() {
|
fn bad_values_are_errors_that_name_them() {
|
||||||
let dir = std::env::temp_dir().join(format!("bx-runner-cfg-{}", std::process::id()));
|
let dir = tmp::TempDir::new("runner-cfg");
|
||||||
std::fs::create_dir_all(&dir).unwrap();
|
|
||||||
let cases = [
|
let cases = [
|
||||||
(
|
(
|
||||||
format!("image = \"x@sha256:{}\"", "0".repeat(63)),
|
format!("image = \"x@sha256:{}\"", "0".repeat(63)),
|
||||||
@@ -123,14 +125,13 @@ fn bad_values_are_errors_that_name_them() {
|
|||||||
(format!("image = \"{DIGEST}\"\npodman = \"\""), "podman"),
|
(format!("image = \"{DIGEST}\"\npodman = \"\""), "podman"),
|
||||||
];
|
];
|
||||||
for (n, (body, word)) in cases.iter().enumerate() {
|
for (n, (body, word)) in cases.iter().enumerate() {
|
||||||
let path = dir.join(format!("c{n}.toml"));
|
let path = dir.path().join(format!("c{n}.toml"));
|
||||||
std::fs::write(&path, format!("[runner]\n{body}\n")).unwrap();
|
std::fs::write(&path, format!("[runner]\n{body}\n")).unwrap();
|
||||||
match Config::load(&path) {
|
match Config::load(&path) {
|
||||||
Err(ConfigError::Invalid(_, why)) => assert!(why.contains(word), "{body}: {why}"),
|
Err(ConfigError::Invalid(_, why)) => assert!(why.contains(word), "{body}: {why}"),
|
||||||
other => panic!("{body}: expected Invalid, got {other:?}"),
|
other => panic!("{body}: expected Invalid, got {other:?}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let _ = std::fs::remove_dir_all(&dir);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
//! Two small texts from the M3b review: every line `brokerd serve` prints about its runtime starts
|
//! Two small texts from the M3b review: every line `brokerd serve` prints about its runtime starts
|
||||||
//! with `brokerd:`, and a bad `[runner]` value is quoted in its error. Do not edit.
|
//! with `brokerd:`, and a bad `[runner]` value is quoted in its error. Do not edit.
|
||||||
|
|
||||||
|
#[path = "support/tmp.rs"]
|
||||||
|
mod tmp;
|
||||||
|
|
||||||
#[path = "support/fake_podman.rs"]
|
#[path = "support/fake_podman.rs"]
|
||||||
mod fake_podman;
|
mod fake_podman;
|
||||||
|
|
||||||
@@ -59,8 +62,7 @@ fn the_runtime_notice_starts_with_brokerd() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn a_bad_runner_value_is_quoted() {
|
fn a_bad_runner_value_is_quoted() {
|
||||||
let dir = std::env::temp_dir().join(format!("bx-notice-cfg-{}", std::process::id()));
|
let dir = tmp::TempDir::new("notice-cfg");
|
||||||
std::fs::create_dir_all(&dir).unwrap();
|
|
||||||
let cases = [
|
let cases = [
|
||||||
("image = \"not by digest\"", "\"not by digest\""),
|
("image = \"not by digest\"", "\"not by digest\""),
|
||||||
(
|
(
|
||||||
@@ -69,12 +71,11 @@ fn a_bad_runner_value_is_quoted() {
|
|||||||
),
|
),
|
||||||
];
|
];
|
||||||
for (n, (body, quoted)) in cases.iter().enumerate() {
|
for (n, (body, quoted)) in cases.iter().enumerate() {
|
||||||
let path = dir.join(format!("q{n}.toml"));
|
let path = dir.path().join(format!("q{n}.toml"));
|
||||||
std::fs::write(&path, format!("[runner]\n{body}\n")).unwrap();
|
std::fs::write(&path, format!("[runner]\n{body}\n")).unwrap();
|
||||||
match Config::load(&path) {
|
match Config::load(&path) {
|
||||||
Err(ConfigError::Invalid(_, why)) => assert!(why.contains(quoted), "{why}"),
|
Err(ConfigError::Invalid(_, why)) => assert!(why.contains(quoted), "{why}"),
|
||||||
other => panic!("{body}: {other:?}"),
|
other => panic!("{body}: {other:?}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let _ = std::fs::remove_dir_all(&dir);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
//! A `brokerd` that accepts and never answers does not hang `bxctl` (M3a review finding 9).
|
//! A `brokerd` that accepts and never answers does not hang `bxctl` (M3a review finding 9).
|
||||||
|
|
||||||
|
#[path = "support/temp.rs"]
|
||||||
|
mod temp;
|
||||||
use std::os::unix::net::UnixListener;
|
use std::os::unix::net::UnixListener;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
@@ -13,9 +15,7 @@ fn the_default_is_thirty_seconds() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn a_silent_brokerd_is_an_error_after_the_timeout() {
|
fn a_silent_brokerd_is_an_error_after_the_timeout() {
|
||||||
let dir = std::env::temp_dir().join(format!("bx-admin-timeout-{}", std::process::id()));
|
let dir = temp::TempDir::new("bx-admin-timeout");
|
||||||
let _ = std::fs::remove_dir_all(&dir);
|
|
||||||
std::fs::create_dir_all(&dir).unwrap();
|
|
||||||
let socket = dir.join("admin.sock");
|
let socket = dir.join("admin.sock");
|
||||||
let listener = UnixListener::bind(&socket).unwrap();
|
let listener = UnixListener::bind(&socket).unwrap();
|
||||||
let held = std::thread::spawn(move || {
|
let held = std::thread::spawn(move || {
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
//! Tests for `bxctl chat`, against a fake `loopd` that speaks the channel protocol. Do not edit.
|
//! Tests for `bxctl chat`, against a fake `loopd` that speaks the channel protocol. Do not edit.
|
||||||
|
|
||||||
|
#[path = "support/temp.rs"]
|
||||||
|
mod temp;
|
||||||
use bxctl::chat::{ChatError, Printer, new_session_id, run_turn};
|
use bxctl::chat::{ChatError, Printer, new_session_id, run_turn};
|
||||||
use proto::{
|
use proto::{
|
||||||
DataClass, Envelope, ErrorCode, Message, PROTOCOL_VERSION, SessionId, Turn, TurnDone,
|
DataClass, Envelope, ErrorCode, Message, PROTOCOL_VERSION, SessionId, Turn, TurnDone,
|
||||||
@@ -8,12 +10,9 @@ use proto::{
|
|||||||
use std::os::unix::net::UnixListener;
|
use std::os::unix::net::UnixListener;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
use std::sync::atomic::{AtomicU32, Ordering};
|
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use std::thread;
|
use std::thread;
|
||||||
|
|
||||||
static NEXT: AtomicU32 = AtomicU32::new(0);
|
|
||||||
|
|
||||||
/// What the fake `loopd` sends back for one turn, after the events: done or an error.
|
/// What the fake `loopd` sends back for one turn, after the events: done or an error.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
enum End {
|
enum End {
|
||||||
@@ -24,6 +23,7 @@ enum End {
|
|||||||
struct FakeLoopd {
|
struct FakeLoopd {
|
||||||
socket: PathBuf,
|
socket: PathBuf,
|
||||||
turns: Arc<Mutex<Vec<Turn>>>,
|
turns: Arc<Mutex<Vec<Turn>>>,
|
||||||
|
_dir: temp::TempDir,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn usage() -> Usage {
|
fn usage() -> Usage {
|
||||||
@@ -39,9 +39,7 @@ fn usage() -> Usage {
|
|||||||
/// Serves every connection with the same script: the given events, then `end`. Records the
|
/// Serves every connection with the same script: the given events, then `end`. Records the
|
||||||
/// turns it received.
|
/// turns it received.
|
||||||
fn fake_loopd(events: Vec<TurnEvent>, end: End) -> FakeLoopd {
|
fn fake_loopd(events: Vec<TurnEvent>, end: End) -> FakeLoopd {
|
||||||
let n = NEXT.fetch_add(1, Ordering::SeqCst);
|
let dir = temp::TempDir::new("bxctl-test");
|
||||||
let dir = std::env::temp_dir().join(format!("bxctl-test-{}-{n}", std::process::id()));
|
|
||||||
std::fs::create_dir_all(&dir).unwrap();
|
|
||||||
let socket = dir.join("loop.sock");
|
let socket = dir.join("loop.sock");
|
||||||
let listener = UnixListener::bind(&socket).unwrap();
|
let listener = UnixListener::bind(&socket).unwrap();
|
||||||
let turns = Arc::new(Mutex::new(Vec::new()));
|
let turns = Arc::new(Mutex::new(Vec::new()));
|
||||||
@@ -93,7 +91,11 @@ fn fake_loopd(events: Vec<TurnEvent>, end: End) -> FakeLoopd {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
FakeLoopd { socket, turns }
|
FakeLoopd {
|
||||||
|
socket,
|
||||||
|
turns,
|
||||||
|
_dir: dir,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn events() -> Vec<TurnEvent> {
|
fn events() -> Vec<TurnEvent> {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
//! `bxctl reindex` and `bxctl memory`: parsing, what is sent to `memoryd`, and what is printed
|
//! `bxctl reindex` and `bxctl memory`: parsing, what is sent to `memoryd`, and what is printed
|
||||||
//! (M5a spec, section 10). Do not edit.
|
//! (M5a spec, section 10). Do not edit.
|
||||||
|
|
||||||
|
#[path = "support/temp.rs"]
|
||||||
|
mod temp;
|
||||||
use std::os::unix::net::UnixListener;
|
use std::os::unix::net::UnixListener;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::mpsc;
|
use std::sync::mpsc;
|
||||||
@@ -112,11 +114,8 @@ fn fake(socket: &Path, answer: Message) -> mpsc::Receiver<Message> {
|
|||||||
rx
|
rx
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dir(tag: &str) -> PathBuf {
|
fn dir(tag: &str) -> temp::TempDir {
|
||||||
let path = std::env::temp_dir().join(format!("bx-mem-{tag}-{}", std::process::id()));
|
temp::TempDir::new(&format!("bx-mem-{tag}"))
|
||||||
let _ = std::fs::remove_dir_all(&path);
|
|
||||||
std::fs::create_dir_all(&path).unwrap();
|
|
||||||
path
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -15,12 +15,38 @@ use std::thread;
|
|||||||
|
|
||||||
static NEXT: AtomicU32 = AtomicU32::new(0);
|
static NEXT: AtomicU32 = AtomicU32::new(0);
|
||||||
|
|
||||||
/// A socket path in a fresh temporary directory.
|
/// A socket path in a fresh temporary directory, which is removed when the value is dropped
|
||||||
pub fn temp_socket(name: &str) -> PathBuf {
|
/// (tip T44).
|
||||||
|
pub struct TempSocket {
|
||||||
|
dir: PathBuf,
|
||||||
|
path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::ops::Deref for TempSocket {
|
||||||
|
type Target = std::path::Path;
|
||||||
|
fn deref(&self) -> &std::path::Path {
|
||||||
|
&self.path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<std::path::Path> for TempSocket {
|
||||||
|
fn as_ref(&self) -> &std::path::Path {
|
||||||
|
&self.path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for TempSocket {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let _ = std::fs::remove_dir_all(&self.dir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn temp_socket(name: &str) -> TempSocket {
|
||||||
let n = NEXT.fetch_add(1, Ordering::SeqCst);
|
let n = NEXT.fetch_add(1, Ordering::SeqCst);
|
||||||
let dir = std::env::temp_dir().join(format!("bxctl-m3a-{}-{n}", std::process::id()));
|
let dir = std::env::temp_dir().join(format!("bxctl-m3a-{}-{n}", std::process::id()));
|
||||||
std::fs::create_dir_all(&dir).unwrap();
|
std::fs::create_dir_all(&dir).unwrap();
|
||||||
dir.join(name)
|
let path = dir.join(name);
|
||||||
|
TempSocket { dir, path }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ts(text: &str) -> Timestamp {
|
pub fn ts(text: &str) -> Timestamp {
|
||||||
@@ -28,7 +54,7 @@ pub fn ts(text: &str) -> Timestamp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct FakeBrokerd {
|
pub struct FakeBrokerd {
|
||||||
pub socket: PathBuf,
|
pub socket: TempSocket,
|
||||||
/// Every request message received, in order.
|
/// Every request message received, in order.
|
||||||
pub requests: Arc<Mutex<Vec<Message>>>,
|
pub requests: Arc<Mutex<Vec<Message>>>,
|
||||||
}
|
}
|
||||||
@@ -124,7 +150,7 @@ pub fn brokerd_with(items: Vec<PendingApproval>, outcome: proto::DecisionRecord)
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct FakeLoopd {
|
pub struct FakeLoopd {
|
||||||
pub socket: PathBuf,
|
pub socket: TempSocket,
|
||||||
/// The content of every turn received, in order.
|
/// The content of every turn received, in order.
|
||||||
pub turns: Arc<Mutex<Vec<String>>>,
|
pub turns: Arc<Mutex<Vec<String>>>,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
//! A temporary directory for a test, removed with everything in it when the value is dropped
|
||||||
|
//! (tip T44). Included with `#[path = "support/temp.rs"] mod temp;`.
|
||||||
|
|
||||||
|
#![allow(dead_code)] // each test file uses a different part of this module
|
||||||
|
|
||||||
|
use std::ffi::OsStr;
|
||||||
|
use std::ops::Deref;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::sync::atomic::{AtomicU32, Ordering};
|
||||||
|
|
||||||
|
static NEXT: AtomicU32 = AtomicU32::new(0);
|
||||||
|
|
||||||
|
pub struct TempDir(PathBuf);
|
||||||
|
|
||||||
|
impl TempDir {
|
||||||
|
/// A fresh, empty directory `<temp>/<tag>-<pid>-<n>`.
|
||||||
|
pub fn new(tag: &str) -> TempDir {
|
||||||
|
let n = NEXT.fetch_add(1, Ordering::SeqCst);
|
||||||
|
let path = std::env::temp_dir().join(format!("{tag}-{}-{n}", std::process::id()));
|
||||||
|
let _ = std::fs::remove_dir_all(&path);
|
||||||
|
std::fs::create_dir_all(&path).unwrap();
|
||||||
|
TempDir(path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for TempDir {
|
||||||
|
type Target = Path;
|
||||||
|
fn deref(&self) -> &Path {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<Path> for TempDir {
|
||||||
|
fn as_ref(&self) -> &Path {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<OsStr> for TempDir {
|
||||||
|
fn as_ref(&self) -> &OsStr {
|
||||||
|
self.0.as_os_str()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for TempDir {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
// Put back the permissions a test may have taken away, or the removal fails.
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
let _ = std::fs::set_permissions(&self.0, std::fs::Permissions::from_mode(0o700));
|
||||||
|
let _ = std::fs::remove_dir_all(&self.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
//! `bxctl audit verify` says which files in `audit/` it did not read: a file that looks like a
|
//! `bxctl audit verify` says which files in `audit/` it did not read: a file that looks like a
|
||||||
//! log but is not named as one is ignored by `brokerd` too, and "ok" must not seem to cover it.
|
//! log but is not named as one is ignored by `brokerd` too, and "ok" must not seem to cover it.
|
||||||
|
|
||||||
|
#[path = "support/temp.rs"]
|
||||||
|
mod temp;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
fn home(tag: &str) -> PathBuf {
|
fn home(tag: &str) -> temp::TempDir {
|
||||||
let dir = std::env::temp_dir().join(format!("bx-verify-skip-{tag}-{}", std::process::id()));
|
let dir = temp::TempDir::new(&format!("bx-verify-skip-{tag}"));
|
||||||
let _ = std::fs::remove_dir_all(&dir);
|
|
||||||
std::fs::create_dir_all(dir.join("audit")).unwrap();
|
std::fs::create_dir_all(dir.join("audit")).unwrap();
|
||||||
let fixture =
|
let fixture =
|
||||||
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../proto/tests/fixtures/audit/good");
|
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../proto/tests/fixtures/audit/good");
|
||||||
|
|||||||
@@ -59,13 +59,15 @@ fn held(home: &TempDir, before: Vec<TurnEvent>) -> (mpsc::Receiver<Turn>, mpsc::
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct Stage {
|
struct Stage {
|
||||||
_home: TempDir,
|
|
||||||
mm: FakeMm,
|
mm: FakeMm,
|
||||||
_running: Running,
|
_running: Running,
|
||||||
ws: WsPeer,
|
ws: WsPeer,
|
||||||
go: mpsc::Sender<()>,
|
go: mpsc::Sender<()>,
|
||||||
socket: PathBuf,
|
socket: PathBuf,
|
||||||
root: String,
|
root: String,
|
||||||
|
// 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: TempDir,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// gatewayd up, a direct message from KYLE whose turn is held, and the approval's socket.
|
/// gatewayd up, a direct message from KYLE whose turn is held, and the approval's socket.
|
||||||
|
|||||||
@@ -50,13 +50,15 @@ fn held(home: &TempDir) -> (mpsc::Receiver<Turn>, mpsc::Sender<()>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct Stage {
|
struct Stage {
|
||||||
_home: TempDir,
|
|
||||||
mm: FakeMm,
|
mm: FakeMm,
|
||||||
_running: Running,
|
_running: Running,
|
||||||
ws: WsPeer,
|
ws: WsPeer,
|
||||||
go: mpsc::Sender<()>,
|
go: mpsc::Sender<()>,
|
||||||
asking: Asking,
|
asking: Asking,
|
||||||
root: String,
|
root: String,
|
||||||
|
// 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: TempDir,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// gatewayd up, EVE's message `text` in `channel` (of `channel_type`), its turn held, and
|
/// gatewayd up, EVE's message `text` in `channel` (of `channel_type`), its turn held, and
|
||||||
|
|||||||
@@ -67,6 +67,17 @@ impl Running {
|
|||||||
impl Drop for Running {
|
impl Drop for Running {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
self.stop.store(true, Ordering::SeqCst);
|
self.stop.store(true, Ordering::SeqCst);
|
||||||
|
// Wait for `run` to finish before the test's home is removed, or it can recreate its
|
||||||
|
// socket directory there afterwards (tip T44). The wait has a limit (T27).
|
||||||
|
if let Some(handle) = self.handle.take() {
|
||||||
|
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
|
||||||
|
while !handle.is_finished() && std::time::Instant::now() < deadline {
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||||
|
}
|
||||||
|
if handle.is_finished() {
|
||||||
|
let _ = handle.join();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,11 +14,37 @@ use std::time::Duration;
|
|||||||
|
|
||||||
static NEXT: AtomicU32 = AtomicU32::new(0);
|
static NEXT: AtomicU32 = AtomicU32::new(0);
|
||||||
|
|
||||||
fn socket_path() -> PathBuf {
|
/// A socket path in a fresh directory of its own, removed when the value is dropped (tip T44).
|
||||||
|
struct TempSocket {
|
||||||
|
dir: PathBuf,
|
||||||
|
path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::ops::Deref for TempSocket {
|
||||||
|
type Target = PathBuf;
|
||||||
|
fn deref(&self) -> &PathBuf {
|
||||||
|
&self.path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<std::path::Path> for TempSocket {
|
||||||
|
fn as_ref(&self) -> &std::path::Path {
|
||||||
|
&self.path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for TempSocket {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let _ = std::fs::remove_dir_all(&self.dir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn socket_path() -> TempSocket {
|
||||||
let n = NEXT.fetch_add(1, Ordering::SeqCst);
|
let n = NEXT.fetch_add(1, Ordering::SeqCst);
|
||||||
let dir = std::env::temp_dir().join(format!("inferproxy-test-{}-{n}", std::process::id()));
|
let dir = std::env::temp_dir().join(format!("inferproxy-test-{}-{n}", std::process::id()));
|
||||||
std::fs::create_dir_all(&dir).unwrap();
|
std::fs::create_dir_all(&dir).unwrap();
|
||||||
dir.join("infer.sock")
|
let path = dir.join("infer.sock");
|
||||||
|
TempSocket { dir, path }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An upstream that reads until the client stops sending, then answers with what it read,
|
/// An upstream that reads until the client stops sending, then answers with what it read,
|
||||||
@@ -52,7 +78,7 @@ fn silent_upstream() -> String {
|
|||||||
addr
|
addr
|
||||||
}
|
}
|
||||||
|
|
||||||
fn start(upstream: String, limits: Limits) -> PathBuf {
|
fn start(upstream: String, limits: Limits) -> TempSocket {
|
||||||
let path = socket_path();
|
let path = socket_path();
|
||||||
let listener = UnixListener::bind(&path).unwrap();
|
let listener = UnixListener::bind(&path).unwrap();
|
||||||
thread::spawn(move || serve(listener, upstream, limits));
|
thread::spawn(move || serve(listener, upstream, limits));
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ fn an_expiry_that_has_already_passed_still_leaves_the_timeout() {
|
|||||||
fn no_socket_is_unavailable() {
|
fn no_socket_is_unavailable() {
|
||||||
let socket = socket_path();
|
let socket = socket_path();
|
||||||
let _ = std::fs::remove_file(&socket);
|
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_unavailable(&got, "no socket file");
|
||||||
assert!(
|
assert!(
|
||||||
got.lines[0].contains(&socket.display().to_string()),
|
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 _listener = UnixListener::bind(&path).unwrap();
|
||||||
let mut big = request();
|
let mut big = request();
|
||||||
big.arguments = format!(r#"{{"path":"/{}"}}"#, "a".repeat(proto::MAX_FRAME));
|
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!(
|
assert_eq!(
|
||||||
got.response,
|
got.response,
|
||||||
ToolResponse::Failed {
|
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 {
|
for n in 0..3 {
|
||||||
let got = port.call(&request(), &mut |_| {});
|
let got = port.call(&request(), &mut |_| {});
|
||||||
assert_eq!(got, result(&format!("answer {n}")));
|
assert_eq!(got, result(&format!("answer {n}")));
|
||||||
|
|||||||
@@ -23,9 +23,11 @@ use support::{FakeServer, Home, Reply, ScriptedPort};
|
|||||||
const CHAT: &str = "/v1/chat/completions";
|
const CHAT: &str = "/v1/chat/completions";
|
||||||
|
|
||||||
struct Loopd {
|
struct Loopd {
|
||||||
home: Home,
|
|
||||||
server: FakeServer,
|
server: FakeServer,
|
||||||
socket: PathBuf,
|
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 {
|
fn start(replies: Vec<proto::ToolResponse>) -> Loopd {
|
||||||
|
|||||||
@@ -59,6 +59,11 @@ impl Proxy {
|
|||||||
impl Drop for Proxy {
|
impl Drop for Proxy {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
self.kill();
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,10 +44,12 @@ impl MemorySource for Shared {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct Loopd {
|
struct Loopd {
|
||||||
_home: Home,
|
|
||||||
server: FakeServer,
|
server: FakeServer,
|
||||||
socket: PathBuf,
|
socket: PathBuf,
|
||||||
memory: Arc<Recorder>,
|
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 {
|
fn start(block: Result<&str, &str>) -> Loopd {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
#![allow(dead_code)] // each test file uses a different part of this module
|
#![allow(dead_code)] // each test file uses a different part of this module
|
||||||
|
|
||||||
use std::os::unix::net::{UnixListener, UnixStream};
|
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::atomic::{AtomicU32, Ordering};
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use std::thread::{self, JoinHandle};
|
use std::thread::{self, JoinHandle};
|
||||||
@@ -21,9 +21,31 @@ use proto::{
|
|||||||
|
|
||||||
static NEXT: AtomicU32 = AtomicU32::new(0);
|
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);
|
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
|
/// Accepts one connection, reads the request frame and hands both to `script`. The connection
|
||||||
@@ -35,13 +57,16 @@ where
|
|||||||
let path = socket_path();
|
let path = socket_path();
|
||||||
let _ = std::fs::remove_file(&path);
|
let _ = std::fs::remove_file(&path);
|
||||||
let listener = UnixListener::bind(&path).unwrap();
|
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 handle = thread::spawn(move || {
|
||||||
|
let _path = path;
|
||||||
let (mut stream, _) = listener.accept().unwrap();
|
let (mut stream, _) = listener.accept().unwrap();
|
||||||
let request = read_frame(&mut stream).unwrap();
|
let request = read_frame(&mut stream).unwrap();
|
||||||
script(&mut stream, &request);
|
script(&mut stream, &request);
|
||||||
request
|
request
|
||||||
});
|
});
|
||||||
(path, handle)
|
(returned, handle)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn frame(id: u64, r#final: bool, msg: Message) -> Envelope {
|
pub fn frame(id: u64, r#final: bool, msg: Message) -> Envelope {
|
||||||
|
|||||||
@@ -281,12 +281,14 @@ struct State {
|
|||||||
pub struct FakeServer {
|
pub struct FakeServer {
|
||||||
pub socket: PathBuf,
|
pub socket: PathBuf,
|
||||||
state: Arc<State>,
|
state: Arc<State>,
|
||||||
|
dir: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FakeServer {
|
impl FakeServer {
|
||||||
pub fn start() -> FakeServer {
|
pub fn start() -> FakeServer {
|
||||||
let n = NEXT.fetch_add(1, Ordering::SeqCst);
|
let n = NEXT.fetch_add(1, Ordering::SeqCst);
|
||||||
let dir = std::env::temp_dir().join(format!("loopd-fake-{}-{n}", std::process::id()));
|
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();
|
std::fs::create_dir_all(&dir).unwrap();
|
||||||
let socket = dir.join("infer.sock");
|
let socket = dir.join("infer.sock");
|
||||||
let _ = std::fs::remove_file(&socket);
|
let _ = std::fs::remove_file(&socket);
|
||||||
@@ -303,7 +305,7 @@ impl FakeServer {
|
|||||||
thread::spawn(move || serve(stream, &state));
|
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.
|
/// 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));
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
//! `toolkit egress-proxy` takes exactly `--socket <path> --allow <list>`: with anything after
|
//! `toolkit egress-proxy` takes exactly `--socket <path> --allow <list>`: with anything after
|
||||||
//! them it is not the proxy, and exits 2 at once rather than listening (M3b review). Do not edit.
|
//! them it is not the proxy, and exits 2 at once rather than listening (M3b review). Do not edit.
|
||||||
|
|
||||||
|
#[path = "support/mod.rs"]
|
||||||
|
mod support;
|
||||||
|
|
||||||
use std::process::{Command, Stdio};
|
use std::process::{Command, Stdio};
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn trailing_arguments_are_not_the_proxy_form() {
|
fn trailing_arguments_are_not_the_proxy_form() {
|
||||||
let dir = std::env::temp_dir().join(format!("tk-egress-form-{}", std::process::id()));
|
let dir = support::TempDir::new("egress-form");
|
||||||
let _ = std::fs::remove_dir_all(&dir);
|
let socket = dir.path().join("egress.sock");
|
||||||
std::fs::create_dir_all(&dir).unwrap();
|
|
||||||
let socket = dir.join("egress.sock");
|
|
||||||
let mut child = Command::new(env!("CARGO_BIN_EXE_toolkit"))
|
let mut child = Command::new(env!("CARGO_BIN_EXE_toolkit"))
|
||||||
.args([
|
.args([
|
||||||
"egress-proxy",
|
"egress-proxy",
|
||||||
@@ -38,5 +39,4 @@ fn trailing_arguments_are_not_the_proxy_form() {
|
|||||||
};
|
};
|
||||||
assert_eq!(status.code(), Some(2));
|
assert_eq!(status.code(), Some(2));
|
||||||
assert!(!socket.exists(), "no socket was made");
|
assert!(!socket.exists(), "no socket was made");
|
||||||
let _ = std::fs::remove_dir_all(&dir);
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user