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

+4 -4
View File
@@ -670,16 +670,16 @@ mod tests {
let dir =
std::env::temp_dir().join(format!("ledger-gui-attention-{}-{tag}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
// Loaded books need no files: removed once loaded, pass or panic.
let _cleanup = crate::test_support::TempDir(dir.clone());
let data_dir = dir.join("books-data");
ledger_store::Store::create(&data_dir, ledger_core::Currency::USD).unwrap();
let b = crate::data::load_once(&crate::data::Source {
crate::data::load_once(&crate::data::Source {
data_dir,
books_toml: None,
today: Some(today),
})
.unwrap();
let _ = std::fs::remove_dir_all(&dir);
b
.unwrap()
}
/// INVARIANTS 2 and 4: a dated rule past its review is one Todo item
+11 -2
View File
@@ -2169,6 +2169,15 @@ mod tests {
}
use super::*;
/// A test's 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);
}
}
/// The fixed asset register report's tax columns are the register's:
/// each held asset's contributor's basis, tax basis, and built-in gain
/// (ledger-core's Asset::tax_basis and built_in_gain), the total its
@@ -2178,6 +2187,7 @@ mod tests {
let today = NaiveDate::from_ymd_opt(2026, 10, 12).unwrap();
let dir = std::env::temp_dir().join(format!("ledger-gui-tax-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
let _cleanup = TestDir(dir.clone());
crate::demo::build(&dir, today).unwrap();
let b = crate::data::load_once(&crate::data::Source {
data_dir: dir.join("books-data"),
@@ -2234,7 +2244,6 @@ mod tests {
.iter()
.any(|row| row.kind == Kind::Note && row.label.contains("fair market value"))
);
let _ = std::fs::remove_dir_all(&dir);
}
/// Every table-backed report typesets as a PDF, in its plain, by-month,
@@ -2244,6 +2253,7 @@ mod tests {
let today = NaiveDate::from_ymd_opt(2026, 10, 12).unwrap();
let dir = std::env::temp_dir().join(format!("ledger-gui-pdf-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
let _cleanup = TestDir(dir.clone());
crate::demo::build(&dir, today).unwrap();
let b = crate::data::load_once(&crate::data::Source {
data_dir: dir.join("books-data"),
@@ -2296,6 +2306,5 @@ mod tests {
std::fs::write(d.join(format!("{name}.pdf")), bytes).unwrap();
}
}
let _ = std::fs::remove_dir_all(&dir);
}
}
+17 -5
View File
@@ -17,22 +17,34 @@ use ledger_gui::nav::{ReportKind, Route, Section};
use ledger_gui::palette::{crumbs, describe};
use ledger_gui::{App, Source, data};
fn demo(tag: &str) -> Source {
/// A test's 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);
}
}
/// The demo books, kept while the guard lives.
fn demo(tag: &str) -> (TestDir, Source) {
let dir = std::env::temp_dir().join(format!("ledger-gui-crumbs-{tag}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
let today = chrono::NaiveDate::from_ymd_opt(2026, 10, 12).unwrap();
ledger_gui::demo::build(&dir, today).unwrap();
Source {
let source = Source {
data_dir: dir.join("books-data"),
books_toml: Some(dir.join("wanderbooks.toml")),
today: Some(today),
}
};
(TestDir(dir), source)
}
/// INVARIANTS 1-3 on every kind of page in the demo books.
#[test]
fn crumbs_link_every_level_but_the_last() {
let b = data::load_once(&demo("unit")).unwrap();
let (_dir, source) = demo("unit");
let b = data::load_once(&source).unwrap();
let party = b.ledger.parties().next().unwrap().id;
let rec = b.reconciliations.first().expect("the demo reconciles");
let entry = b.entries.first().unwrap().id;
@@ -90,7 +102,7 @@ fn crumbs_link_every_level_but_the_last() {
/// INVARIANT 4, through the top bar.
#[test]
fn clicking_a_crumb_goes_there() {
let source = demo("click");
let (_dir, source) = demo("click");
let b = data::load_once(&source).unwrap();
let rec = b.reconciliations.first().unwrap().clone();
let account = describe(&b, &Route::Account(rec.account_id));
+10 -1
View File
@@ -21,6 +21,15 @@ use ledger_gui::{App, Source, data, fmt};
use proptest::prelude::*;
use rust_decimal::Decimal;
/// A test's 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);
}
}
proptest! {
/// INVARIANT 1: every display style copies back to the same value.
#[test]
@@ -54,6 +63,7 @@ fn copied(h: &Harness<'_, App>) -> Vec<String> {
fn clicking_a_value_copies_it() {
let dir = std::env::temp_dir().join(format!("ledger-gui-copy-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
let _cleanup = TestDir(dir.clone());
let today = chrono::NaiveDate::from_ymd_opt(2026, 10, 12).unwrap();
ledger_gui::demo::build(&dir, today).unwrap();
let source = Source {
@@ -106,5 +116,4 @@ fn clicking_a_value_copies_it() {
h.get_by_label(&format!("Entry #{id}")).click();
h.step();
assert_eq!(copied(&h), [format!("#{id}")]);
let _ = std::fs::remove_dir_all(&dir);
}
+10 -1
View File
@@ -9,10 +9,20 @@ use ledger_store::Store;
use rust_decimal::Decimal;
use std::time::{Duration, Instant};
/// A test's 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);
}
}
#[test]
fn a_change_in_another_process_shows_up_live() {
let dir = std::env::temp_dir().join(format!("ledger-gui-live-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
let _cleanup = TestDir(dir.clone());
let data = dir.join("books-data");
let mut store = Store::create(&data, Currency::USD).unwrap();
let cash = store
@@ -86,5 +96,4 @@ fn a_change_in_another_process_shows_up_live() {
assert!(shown, "the show request never reached the viewer");
eprintln!("show request arrived in {:?}", asked.elapsed());
let _ = std::fs::remove_file(ledger_store::viewer::command_path(&data));
let _ = std::fs::remove_dir_all(&dir);
}
+22 -2
View File
@@ -16,7 +16,27 @@ use serde_json::json;
use std::path::{Path, PathBuf};
use std::time::{Duration, Instant};
fn books(tag: &str) -> PathBuf {
/// A test's data dir; its books dir is removed when the test ends, pass or
/// panic.
struct DataDir {
dir: PathBuf,
data: PathBuf,
}
impl std::ops::Deref for DataDir {
type Target = Path;
fn deref(&self) -> &Path {
&self.data
}
}
impl Drop for DataDir {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.dir);
}
}
fn books(tag: &str) -> DataDir {
let dir = std::env::temp_dir().join(format!("ledger-gui-gate-{tag}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
let data = dir.join("books-data");
@@ -24,7 +44,7 @@ fn books(tag: &str) -> PathBuf {
store
.add_account("1000", "Checking", AccountType::Asset, None, false)
.unwrap();
data
DataDir { dir, data }
}
fn source(data: &Path) -> Source {
+5 -4
View File
@@ -67,13 +67,15 @@ fn set_read_only(root: &Path, read_only: bool) {
walk(root, read_only);
}
/// Makes the books writable again when dropped, so a failing or
/// interrupted run never leaves a folder no one can delete.
/// Makes the books writable again and removes them when dropped, so a
/// run that passes or fails never leaves a folder behind, let alone one no
/// one can delete.
struct Writable(PathBuf);
impl Drop for Writable {
fn drop(&mut self) {
if self.0.exists() {
set_read_only(&self.0, false);
let _ = std::fs::remove_dir_all(&self.0);
}
}
}
@@ -105,6 +107,7 @@ fn the_viewer_never_writes_into_the_books() {
set_read_only(&dir, false);
std::fs::remove_dir_all(&dir).unwrap();
}
let _restore = Writable(dir.clone());
let today = chrono::NaiveDate::from_ymd_opt(2026, 10, 12).unwrap();
ledger_gui::demo::build(&dir, today).unwrap();
let source = Source {
@@ -112,7 +115,6 @@ fn the_viewer_never_writes_into_the_books() {
books_toml: Some(dir.join("wanderbooks.toml")),
today: Some(today),
};
let _restore = Writable(dir.clone());
set_read_only(&dir, true);
let before = tree(&dir);
@@ -219,5 +221,4 @@ fn the_viewer_never_writes_into_the_books() {
"the viewer created or removed files in writable books"
);
assert!(before == after, "the viewer changed writable books");
let _ = std::fs::remove_dir_all(&dir);
}
+12
View File
@@ -7,6 +7,15 @@ use ledger_gui::nav::{ReportKind, Route};
use ledger_gui::{App, Source, data};
use std::path::PathBuf;
/// 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);
}
}
#[test]
fn screenshots() {
let Ok(out) = std::env::var("SHOT_DIR") else {
@@ -15,6 +24,8 @@ fn screenshots() {
let fixed_today: Option<chrono::NaiveDate> = std::env::var("SHOT_TODAY")
.ok()
.and_then(|d| d.parse().ok());
// Demo books (no SHOT_BOOKS) go when the run ends, pass or panic.
let mut _demo = None;
let books = match std::env::var("SHOT_BOOKS") {
Ok(b) => PathBuf::from(b),
Err(_) => {
@@ -23,6 +34,7 @@ fn screenshots() {
let dir =
std::env::temp_dir().join(format!("ledger-gui-shots-demo-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
_demo = Some(TestDir(dir.clone()));
ledger_gui::demo::build(&dir, d).unwrap();
dir
}
+10 -1
View File
@@ -5,11 +5,21 @@ use ledger_gui::nav::{ReportKind, Route};
use ledger_gui::{App, Source, data};
use ledger_store::viewer::ViewerTarget;
/// A test's 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);
}
}
#[test]
fn show_requests_navigate_the_viewer() {
let today = chrono::NaiveDate::from_ymd_opt(2026, 10, 12).unwrap();
let dir = std::env::temp_dir().join(format!("ledger-gui-show-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
let _cleanup = TestDir(dir.clone());
ledger_gui::demo::build(&dir, today).unwrap();
let source = Source {
data_dir: dir.join("books-data"),
@@ -108,5 +118,4 @@ fn show_requests_navigate_the_viewer() {
end: None,
});
assert_eq!(h.state_mut().route(), &Route::Transactions);
let _ = std::fs::remove_dir_all(&dir);
}
+10 -1
View File
@@ -10,10 +10,20 @@ use ledger_vault::{Tin, Vault};
const SSN: &str = "123-45-6789";
/// A test's 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);
}
}
#[test]
fn the_key_named_on_the_command_line_wins() {
let root = std::env::temp_dir().join(format!("ledger-gui-key-flag-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&root);
let _cleanup = TestDir(root.clone());
let books = root.join("books");
let key = root.join("keys/vault.key");
std::fs::create_dir_all(&books).unwrap();
@@ -52,7 +62,6 @@ fn the_key_named_on_the_command_line_wins() {
// The settings name a key that isn't there; the command line wins.
b.settings.vault_key_file = Some(root.join("missing/vault.key"));
assert_eq!(b.tin_from_vault(party).unwrap(), SSN);
let _ = std::fs::remove_dir_all(&root);
}
#[test]
+28 -20
View File
@@ -51,14 +51,31 @@ fn plan() -> impl Strategy<Value = Plan> {
})
}
fn dir(tag: &str) -> PathBuf {
/// A test's dir, removed when the test ends, pass or panic.
struct TestDir(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-gui-vm-{}-{tag}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
std::fs::create_dir_all(&d).unwrap();
d
TestDir(d)
}
fn build(plan: &Plan, tag: &str) -> (Source, ledger_core::Ledger) {
/// The books, where they are (kept while the guard lives), and their ledger.
fn build(plan: &Plan, tag: &str) -> (TestDir, Source, ledger_core::Ledger) {
let d = dir(tag);
let mut s = Store::create(&d.join("books-data"), Currency::USD).unwrap();
let ids: Vec<_> = CHART
@@ -81,14 +98,12 @@ fn build(plan: &Plan, tag: &str) -> (Source, ledger_core::Ledger) {
)
.unwrap();
let ledger = s.ledger().clone();
(
Source {
data_dir: d.join("books-data"),
books_toml: Some(d.join("wanderbooks.toml")),
today: Some(plan.today),
},
ledger,
)
let source = Source {
data_dir: d.join("books-data"),
books_toml: Some(d.join("wanderbooks.toml")),
today: Some(plan.today),
};
(d, source, ledger)
}
/// An account's normal-side balance at the end of `date`, from the trial
@@ -122,7 +137,7 @@ proptest! {
/// Kpis INVARIANTS 1-7 and the period helpers, against ledger-core.
#[test]
fn the_overview_numbers_are_the_books_numbers(p in plan()) {
let (source, l) = build(&p, "kpi");
let (_dir, source, l) = build(&p, "kpi");
let b = data::load_once(&source).unwrap();
let k = &b.kpis;
let t = p.today;
@@ -283,7 +298,6 @@ fn origins_and_parties() {
assert_eq!(std::fs::read(path).unwrap(), b"%PDF receipt");
assert_eq!(b.integrity.files_ok, 1);
assert!(b.integrity.file_failures.is_empty());
let _ = std::fs::remove_dir_all(&d);
}
/// fmt helpers the viewer shows: percent change and tax forms.
@@ -306,7 +320,7 @@ fn percent_change_and_tax_forms() {
assert_eq!(fmt::compact(d("-1234")), "\u{2212}$1.2k");
}
fn store_at(tag: &str) -> (PathBuf, Store) {
fn store_at(tag: &str) -> (TestDir, Store) {
let d = dir(tag);
let s = Store::create(&d.join("books-data"), Currency::USD).unwrap();
(d, s)
@@ -476,9 +490,6 @@ fn comparisons_and_burn_on_fixed_dates() {
drop(s5);
let b5 = data::load_once(&source(&d5, "", today)).unwrap();
assert_eq!(b5.kpis.monthly_burn, None);
for x in [d, d2, d3, d4, d5] {
let _ = std::fs::remove_dir_all(x);
}
}
/// The view model's indexes: cash accounts by name when the settings name
@@ -548,7 +559,6 @@ fn indexes_cash_cards_accounts_and_fresh() {
let integrity = ledger_gui::books::Integrity::check(&snap, &mut verified);
let b2 = ledger_gui::books::Books::build(&snap, settings, today, Some(seq), integrity).unwrap();
assert_eq!(b2.fresh, [e2].into_iter().collect());
let _ = std::fs::remove_dir_all(&d);
}
/// Integrity counts every stored file, read once: a second check with the
@@ -580,7 +590,6 @@ fn integrity_counts_files_once_verified() {
assert_eq!((first.files_ok, first.file_failures.len()), (2, 0));
let again = ledger_gui::books::Integrity::check(&snap, &mut verified);
assert_eq!((again.files_ok, again.file_failures.len()), (2, 0));
let _ = std::fs::remove_dir_all(&d);
}
/// Origins and parties for the rest of the journal's kinds: the invoice
@@ -647,5 +656,4 @@ fn more_origins_and_parties() {
let r = b.entry(remit).unwrap();
assert_eq!(r.origin, "Payroll taxes");
assert!(!r.missing_receipt, "a remittance isn't a hand-made entry");
let _ = std::fs::remove_dir_all(&d);
}