tests: every test dir is removed when its test ends, pass or panic

Many test helpers removed their temp dir only before the test, or only
on pass, so runs left them behind: 117,000 filled /tmp (RAM here) and
stopped a mutation run. Each now holds a guard that removes the dir on
drop, unwind included; servers started on a test dir hold its guard
until they stop. A full cargo test --workspace leaves 0 dirs (859
before). just sweep-test-tmp stays for runs killed mid-test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T3gXMVt7XJPi6ens1or9Dj
This commit is contained in:
swaitsandClaude Opus 5.5 committed 2026-10-17 20:47:44 -06:00
1 parent b292a505b1
commit a833229d87
44 files changed
+952 -163

No files matched your search

+54 -21
View File
@@ -660,6 +660,21 @@ mod tests {
allowed: HashMap<PathBuf, bool>,
}
impl Drop for Tree {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.base);
}
}
/// A test's dir, removed when the test ends, pass or panic.
struct TestDir(PathBuf);
impl Drop for TestDir {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
fn tree(tag: &str) -> Tree {
let base = std::env::temp_dir().join(format!(
"ledger-mcp-local-files-{}-{tag}",
@@ -813,25 +828,43 @@ mod tests {
let _ = std::fs::remove_dir_all(b);
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(512))]
/// Any absolute path built from the tree's names, `.` and `..`, and
/// its symlinks is allowed exactly when it has no `.` or `..` name
/// and resolves to a file the oracle allows: a regular file inside
/// a root and outside every protected dir.
#[test]
fn import_allowed_iff_regular_file_inside_a_root_outside_the_books(
/// Any absolute path built from the tree's names, `.` and `..`, and
/// its symlinks is allowed exactly when it has no `.` or `..` name and
/// resolves to a file the oracle allows: a regular file inside a root
/// and outside every protected dir. (One tree for every case, removed
/// when the test ends.)
#[test]
fn import_allowed_iff_regular_file_inside_a_root_outside_the_books() {
let t = tree("prop");
let starts = [
"Downloads",
"tmp",
"tmp/books",
"tmp/books/books-data",
"tmp/keys",
"elsewhere",
"secret",
];
let names = [
"..",
".",
"sub",
"dir",
"receipt.pdf",
"scan.png",
"shot.png",
"journal.jsonl",
"vault.key",
"key-link",
"good-link",
"data-link",
"hard.key",
"fifo",
];
proptest!(ProptestConfig::with_cases(512), |(
start in 0usize..7,
parts in proptest::collection::vec(0usize..14, 0..5),
) {
use std::sync::OnceLock;
static TREE: OnceLock<Tree> = OnceLock::new();
let t = TREE.get_or_init(|| tree("prop"));
let starts = ["Downloads", "tmp", "tmp/books", "tmp/books/books-data", "tmp/keys", "elsewhere", "secret"];
let names = [
"..", ".", "sub", "dir", "receipt.pdf", "scan.png", "shot.png", "journal.jsonl",
"vault.key", "key-link", "good-link", "data-link", "hard.key", "fifo",
];
)| {
let mut p = t.base.join(starts[start]);
for i in parts {
p.push(names[i]);
@@ -843,7 +876,7 @@ mod tests {
_ => false,
};
prop_assert_eq!(t.check(&p).is_ok(), want, "{}", p.display());
}
});
}
/// INVARIANT 2: a path that is missing, a dangling link, or outside
@@ -1030,9 +1063,8 @@ mod tests {
.is_ok_and(|s| s.success());
if made {
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
tx.send(read_checked(&canonical, &meta, &t.protected.inodes(), 100))
});
let inodes = t.protected.inodes();
std::thread::spawn(move || tx.send(read_checked(&canonical, &meta, &inodes, 100)));
let got = rx
.recv_timeout(std::time::Duration::from_secs(10))
.expect("opening a pipe blocked");
@@ -1219,6 +1251,7 @@ mod tests {
std::process::id()
));
let _ = std::fs::remove_dir_all(&base);
let _cleanup = TestDir(base.clone());
let home = base.join("home");
std::fs::create_dir_all(home.join(".ssh")).unwrap();
std::fs::write(home.join(".ssh/id_ed25519"), b"PRIVATE KEY").unwrap();
+39 -15
View File
@@ -9472,11 +9472,25 @@ mod tests {
Money::new(Decimal::from_str(s).unwrap(), Currency::USD)
}
fn store(tag: &str) -> Store {
/// A test's books dir, removed when the test ends, pass or panic.
struct TestDir(std::path::PathBuf);
impl Drop for TestDir {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
fn unit_dir(tag: &str) -> TestDir {
let dir =
std::env::temp_dir().join(format!("ledger-mcp-unit-{}-{tag}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
Store::create(&dir, Currency::USD).unwrap()
TestDir(dir)
}
/// New books in `dir`, which outlives them.
fn store(dir: &TestDir) -> Store {
Store::create(&dir.0, Currency::USD).unwrap()
}
/// lines_settled: posted, or a draft with lines whose debits equal its
@@ -9635,7 +9649,8 @@ mod tests {
/// close_into, and each member's capital and distributions accounts.
#[test]
fn config_mentions_finds_every_setting_naming_a_code() {
let server = BooksServer::new(store("mentions"), ReceiptPolicy::default())
let books = unit_dir("mentions");
let server = BooksServer::new(store(&books), ReceiptPolicy::default())
.with_company(members())
.with_cash_roles(CashRoles {
cash: vec!["1000".into()],
@@ -9664,20 +9679,21 @@ mod tests {
["[[company.members]] Ada distribution_account"]
);
assert!(server.config_mentions("6000").is_empty());
let solo = BooksServer::new(store("mentions-solo"), ReceiptPolicy::default()).with_company(
Company {
let books = unit_dir("mentions-solo");
let solo =
BooksServer::new(store(&books), ReceiptPolicy::default()).with_company(Company {
close_into: Some("3000".into()),
..Company::default()
},
);
});
assert_eq!(solo.config_mentions("3000"), ["[books] close_into"]);
}
/// Reports name each member with their accounts, in order.
#[test]
fn report_members_carry_names_and_accounts() {
let books = unit_dir("members");
let server =
BooksServer::new(store("members"), ReceiptPolicy::default()).with_company(members());
BooksServer::new(store(&books), ReceiptPolicy::default()).with_company(members());
let m = server.report_members();
assert_eq!(m.len(), 2);
assert_eq!(
@@ -9697,7 +9713,8 @@ mod tests {
/// included, so a closed month drops out).
#[test]
fn year_earnings_split_closed_from_unclosed() {
let mut s = store("earnings");
let books = unit_dir("earnings");
let mut s = store(&books);
let cash = s
.add_account("1000", "Cash", AccountType::Asset, None, false)
.unwrap();
@@ -9747,7 +9764,8 @@ mod tests {
/// December's income is never this year's).
#[test]
fn year_earnings_count_only_closes_and_never_before_the_year() {
let mut s = store("earnings-open");
let books = unit_dir("earnings-open");
let mut s = store(&books);
let cash = s
.add_account("1000", "Cash", AccountType::Asset, None, false)
.unwrap();
@@ -9783,7 +9801,8 @@ mod tests {
/// entry the policy covers (crate::policy).
#[test]
fn receipts_are_kept_only_where_the_policy_needs_them() {
let mut s = store("unlink");
let books = unit_dir("unlink");
let mut s = store(&books);
let cash = s
.add_account("1000", "Cash", AccountType::Asset, None, false)
.unwrap();
@@ -10437,7 +10456,8 @@ mod tests {
}),
];
for (how, body) in bodies {
let server = BooksServer::new(store("stops"), ReceiptPolicy::default());
let books = unit_dir("stops");
let server = BooksServer::new(store(&books), ReceiptPolicy::default());
rt.block_on(async {
let seq: Body = |s| s.lock().map(|st| st.journal_seq());
assert_eq!(run(&server, seq).await.unwrap(), Ok(1), "{how}");
@@ -10459,7 +10479,8 @@ mod tests {
}
// A guard dropped by a panic on a thread of its own stops it too.
let server = BooksServer::new(store("stops-thread"), ReceiptPolicy::default());
let books = unit_dir("stops-thread");
let server = BooksServer::new(store(&books), ReceiptPolicy::default());
let s = server.clone();
let died = std::thread::spawn(move || {
let _store = s.lock().unwrap();
@@ -10500,7 +10521,8 @@ mod tests {
.enable_all()
.build()
.unwrap();
let shared = SharedStore::new(store("wakes"));
let books = unit_dir("wakes");
let shared = SharedStore::new(store(&books));
let got = rt.block_on(async {
tokio::time::timeout(
std::time::Duration::from_secs(10),
@@ -10525,6 +10547,7 @@ mod tests {
));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let _cleanup = TestDir(dir.clone());
let toml = dir.join("wanderbooks.toml");
let threshold = |t: &str| {
std::fs::write(
@@ -10540,7 +10563,8 @@ mod tests {
config_json: dir.join("config.json"),
})
.unwrap();
let server = BooksServer::new(store("info"), ReceiptPolicy::default())
let books = unit_dir("info");
let server = BooksServer::new(store(&books), ReceiptPolicy::default())
.with_live_settings(Arc::new(Mutex::new(live)));
let told = |s: &BooksServer| rmcp::ServerHandler::get_info(s).instructions.unwrap();
let before = told(&server);
+18 -3
View File
@@ -368,12 +368,28 @@ impl Live {
mod tests {
use super::*;
fn dir(tag: &str) -> PathBuf {
/// A test's dir, removed when the test ends, pass or panic.
struct TestDir(std::path::PathBuf);
impl std::ops::Deref for TestDir {
type Target = std::path::Path;
fn deref(&self) -> &std::path::Path {
&self.0
}
}
impl Drop for TestDir {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
fn dir(tag: &str) -> TestDir {
let d =
std::env::temp_dir().join(format!("ledger-mcp-settings-{}-{tag}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
std::fs::create_dir_all(&d).unwrap();
d
TestDir(d)
}
fn source(d: &std::path::Path, required: bool) -> Source {
@@ -497,7 +513,6 @@ mod tests {
assert_eq!(live.current().company.name, "Kept Co 2");
assert_eq!(live.status().loads, 2);
assert!(live.current().journaled && live.is_journaled());
std::fs::remove_dir_all(&d).unwrap();
}
/// Settings built in code are the books' own once synced.
+18 -4
View File
@@ -357,11 +357,27 @@ fn nul_interleaved(bytes: &[u8]) -> bool {
mod tests {
use super::*;
fn dir(tag: &str) -> std::path::PathBuf {
/// A test's dir, removed when the test ends, pass or panic.
struct TestDir(std::path::PathBuf);
impl std::ops::Deref for TestDir {
type Target = std::path::Path;
fn deref(&self) -> &std::path::Path {
&self.0
}
}
impl Drop for TestDir {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
fn dir(tag: &str) -> TestDir {
let d =
std::env::temp_dir().join(format!("ledger-mcp-tin-files-{}-{tag}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
d
TestDir(d)
}
/// INVARIANTS 1-2: a recorded file is refused from then on, by this
@@ -382,7 +398,6 @@ mod tests {
assert_eq!(text.lines().count(), 1);
// Only the hash is kept.
assert!(!text.contains("PNG"));
let _ = std::fs::remove_dir_all(&vault);
}
/// The registry's folder for a check (it never exists) and no vault.
@@ -614,7 +629,6 @@ Date,Description,Amount,Running Bal.,Reference
.expect_err("a registry that can't be read let a file in");
assert!(why.contains("couldn't check the file"), "{why}");
assert!(record(&vault, b"bytes").is_err());
let _ = std::fs::remove_dir_all(&vault);
}
/// INVARIANT 4: text holding more than two TIN-shaped numbers ending