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:
1 parent
c587593d78
commit
6c04342427
44 files changed
+952
-163
No files matched your search
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -76,6 +76,13 @@ struct Places {
|
||||
sandbox: PathBuf,
|
||||
}
|
||||
|
||||
/// The places go when the test ends, pass or panic.
|
||||
impl Drop for Places {
|
||||
fn drop(&mut self) {
|
||||
let _ = std::fs::remove_dir_all(&self.base);
|
||||
}
|
||||
}
|
||||
|
||||
fn places(tag: &str) -> Places {
|
||||
let base =
|
||||
std::env::temp_dir().join(format!("ledger-mcp-hostile-{}-{tag}", std::process::id()));
|
||||
|
||||
@@ -28,6 +28,15 @@ impl Drop for Server {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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);
|
||||
}
|
||||
}
|
||||
|
||||
fn free_port() -> u16 {
|
||||
std::net::TcpListener::bind("127.0.0.1:0")
|
||||
.unwrap()
|
||||
@@ -123,6 +132,8 @@ async fn agents_posting_at_once_leave_whole_books() {
|
||||
let dir = std::env::temp_dir().join(format!("ledger-mcp-http-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
// Declared before the server, so dropped after it: pass or panic.
|
||||
let _cleanup = TestDir(dir.clone());
|
||||
let toml = dir.join("wanderbooks.toml");
|
||||
std::fs::write(&toml, "[books]\nreceipt_threshold = \"0.00\"\n").unwrap();
|
||||
let data = dir.join("books-data");
|
||||
|
||||
@@ -126,11 +126,39 @@ impl Mcp {
|
||||
}
|
||||
}
|
||||
|
||||
fn test_dir(name: &str) -> std::path::PathBuf {
|
||||
/// A test's dir and what sits beside it (`.inbox`, `.elsewhere.txt`),
|
||||
/// 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 AsRef<std::path::Path> for TestDir {
|
||||
fn as_ref(&self) -> &std::path::Path {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TestDir {
|
||||
fn drop(&mut self) {
|
||||
let _ = std::fs::remove_dir_all(&self.0);
|
||||
for beside in [".inbox", ".elsewhere.txt"] {
|
||||
let p = std::path::PathBuf::from(format!("{}{beside}", self.0.display()));
|
||||
let _ = std::fs::remove_dir_all(&p);
|
||||
let _ = std::fs::remove_file(&p);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn test_dir(name: &str) -> TestDir {
|
||||
let dir = std::env::temp_dir().join(format!("ledger-mcp-test-{}-{name}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
dir
|
||||
TestDir(dir)
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -56,7 +56,31 @@ impl Drop for Mcp {
|
||||
|
||||
static DIR_SEQ: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
fn test_dir(name: &str) -> PathBuf {
|
||||
/// A test's dir and the folder beside it (`.inbox`),
|
||||
/// 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 AsRef<std::path::Path> for TestDir {
|
||||
fn as_ref(&self) -> &std::path::Path {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TestDir {
|
||||
fn drop(&mut self) {
|
||||
let _ = std::fs::remove_dir_all(&self.0);
|
||||
let _ = std::fs::remove_dir_all(format!("{}.inbox", self.0.display()));
|
||||
}
|
||||
}
|
||||
|
||||
fn test_dir(name: &str) -> TestDir {
|
||||
let n = DIR_SEQ.fetch_add(1, Ordering::Relaxed);
|
||||
let dir = std::env::temp_dir().join(format!(
|
||||
"ledger-mcp-oracle-{}-{name}-{n}",
|
||||
@@ -64,7 +88,7 @@ fn test_dir(name: &str) -> PathBuf {
|
||||
));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
dir
|
||||
TestDir(dir)
|
||||
}
|
||||
|
||||
/// `wanderbooks.toml` for two 50/50 members on 3000/3001, cash 1000,
|
||||
@@ -2249,19 +2273,10 @@ fn receipts_and_documents_round_trip() {
|
||||
#[test]
|
||||
fn uploads_up_to_25_mib_are_accepted_and_no_more() {
|
||||
const LIMIT: usize = 25 * 1024 * 1024;
|
||||
// Over 50 MB of books: removed however the test ends, with its inbox
|
||||
// (the system temp dir is often RAM, and a mutation run starts this
|
||||
// test hundreds of times).
|
||||
let dir = test_dir("limits");
|
||||
// Over 50 MB of books: removed however the test ends (the system temp
|
||||
// dir is often RAM, and a mutation run starts this test hundreds of
|
||||
// times).
|
||||
struct Cleanup(Vec<PathBuf>);
|
||||
impl Drop for Cleanup {
|
||||
fn drop(&mut self) {
|
||||
for d in &self.0 {
|
||||
let _ = std::fs::remove_dir_all(d);
|
||||
}
|
||||
}
|
||||
}
|
||||
let _cleanup = Cleanup(vec![dir.clone(), inbox(&dir)]);
|
||||
let s = new_store(&dir);
|
||||
drop(s);
|
||||
let mut m = Mcp::start(&dir, "1000000.00");
|
||||
|
||||
@@ -49,6 +49,9 @@ struct Mcp {
|
||||
next_id: u64,
|
||||
data: PathBuf,
|
||||
validators: HashMap<String, jsonschema::Validator>,
|
||||
/// The books dir when the server owns it: removed after the server
|
||||
/// stops (fields drop after `Drop::drop`).
|
||||
books_dir: Option<TestDir>,
|
||||
}
|
||||
|
||||
impl Drop for Mcp {
|
||||
@@ -60,7 +63,29 @@ impl Drop for Mcp {
|
||||
|
||||
static DIR_SEQ: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
fn test_dir(name: &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 AsRef<std::path::Path> for TestDir {
|
||||
fn as_ref(&self) -> &std::path::Path {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TestDir {
|
||||
fn drop(&mut self) {
|
||||
let _ = std::fs::remove_dir_all(&self.0);
|
||||
}
|
||||
}
|
||||
|
||||
fn test_dir(name: &str) -> TestDir {
|
||||
let n = DIR_SEQ.fetch_add(1, Ordering::Relaxed);
|
||||
let dir = std::env::temp_dir().join(format!(
|
||||
"ledger-mcp-spec-assets-{}-{name}-{n}",
|
||||
@@ -68,7 +93,7 @@ fn test_dir(name: &str) -> PathBuf {
|
||||
));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
dir
|
||||
TestDir(dir)
|
||||
}
|
||||
|
||||
/// `wanderbooks.toml` with a company name and extra `[books]` TOML. The
|
||||
@@ -113,6 +138,7 @@ impl Mcp {
|
||||
}
|
||||
});
|
||||
let mut mcp = Self {
|
||||
books_dir: None,
|
||||
stdin,
|
||||
rx,
|
||||
child,
|
||||
@@ -356,6 +382,7 @@ fn books(name: &str) -> Mcp {
|
||||
let toml = books_toml(&dir, "Spec Co", "asset_tag_prefix = \"FL\"\n");
|
||||
let mut m = Mcp::start(&dir, &toml);
|
||||
m.accounts(CHART);
|
||||
m.books_dir = Some(dir);
|
||||
m
|
||||
}
|
||||
|
||||
|
||||
@@ -52,6 +52,9 @@ struct Mcp {
|
||||
validators: HashMap<String, jsonschema::Validator>,
|
||||
/// Every response line the server wrote, with the tool it answered.
|
||||
transcript: Vec<(String, String)>,
|
||||
/// The books dir when the server owns it: removed after the server
|
||||
/// stops (fields drop after `Drop::drop`).
|
||||
books_dir: Option<TestDir>,
|
||||
}
|
||||
|
||||
impl Drop for Mcp {
|
||||
@@ -63,7 +66,31 @@ impl Drop for Mcp {
|
||||
|
||||
static DIR_SEQ: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
fn test_dir(name: &str) -> PathBuf {
|
||||
/// A test's dir and the folder beside it (`-outside`),
|
||||
/// 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 AsRef<std::path::Path> for TestDir {
|
||||
fn as_ref(&self) -> &std::path::Path {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TestDir {
|
||||
fn drop(&mut self) {
|
||||
let _ = std::fs::remove_dir_all(&self.0);
|
||||
let _ = std::fs::remove_dir_all(format!("{}-outside", self.0.display()));
|
||||
}
|
||||
}
|
||||
|
||||
fn test_dir(name: &str) -> TestDir {
|
||||
let n = DIR_SEQ.fetch_add(1, Ordering::Relaxed);
|
||||
let dir = std::env::temp_dir().join(format!(
|
||||
"ledger-mcp-spec-attachments-{}-{name}-{n}",
|
||||
@@ -71,7 +98,7 @@ fn test_dir(name: &str) -> PathBuf {
|
||||
));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
dir
|
||||
TestDir(dir)
|
||||
}
|
||||
|
||||
/// A directory beside the books dir `dir`, outside it: the vault key,
|
||||
@@ -131,6 +158,7 @@ impl Mcp {
|
||||
}
|
||||
});
|
||||
let mut mcp = Self {
|
||||
books_dir: None,
|
||||
stdin,
|
||||
rx,
|
||||
child,
|
||||
@@ -363,6 +391,7 @@ fn books_at(name: &str, threshold: &str, extra: &str) -> Mcp {
|
||||
let toml = books_toml(&dir, threshold, extra);
|
||||
let mut m = Mcp::start(&dir, &toml);
|
||||
m.accounts(CHART);
|
||||
m.books_dir = Some(dir);
|
||||
m
|
||||
}
|
||||
|
||||
|
||||
@@ -112,11 +112,33 @@ impl Mcp {
|
||||
}
|
||||
}
|
||||
|
||||
fn test_dir(name: &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 AsRef<std::path::Path> for TestDir {
|
||||
fn as_ref(&self) -> &std::path::Path {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TestDir {
|
||||
fn drop(&mut self) {
|
||||
let _ = std::fs::remove_dir_all(&self.0);
|
||||
}
|
||||
}
|
||||
|
||||
fn test_dir(name: &str) -> TestDir {
|
||||
let dir = std::env::temp_dir().join(format!("ledger-mcp-test-{}-{name}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
dir
|
||||
TestDir(dir)
|
||||
}
|
||||
|
||||
fn init(m: &mut Mcp) -> String {
|
||||
|
||||
@@ -159,6 +159,9 @@ struct Mcp {
|
||||
read_only_checked: BTreeSet<String>,
|
||||
/// Invariant 3 (amounts out) violations: `tool: path = value`.
|
||||
money_violations: Vec<String>,
|
||||
/// The books dir when the server owns it: removed after the server
|
||||
/// stops (fields drop after `Drop::drop`).
|
||||
books_dir: Option<TestDir>,
|
||||
}
|
||||
|
||||
impl Drop for Mcp {
|
||||
@@ -170,14 +173,42 @@ impl Drop for Mcp {
|
||||
|
||||
static DIR_SEQ: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
/// A test's dir and the folders beside it (`.inbox`, `-outside`),
|
||||
/// 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 AsRef<std::path::Path> for TestDir {
|
||||
fn as_ref(&self) -> &std::path::Path {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TestDir {
|
||||
fn drop(&mut self) {
|
||||
let _ = std::fs::remove_dir_all(&self.0);
|
||||
for beside in [".inbox", "-outside"] {
|
||||
let p = PathBuf::from(format!("{}{beside}", self.0.display()));
|
||||
let _ = std::fs::remove_dir_all(&p);
|
||||
let _ = std::fs::remove_file(&p);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A fresh, empty per-test directory under the system temp dir.
|
||||
fn test_dir(name: &str) -> PathBuf {
|
||||
fn test_dir(name: &str) -> TestDir {
|
||||
let n = DIR_SEQ.fetch_add(1, Ordering::Relaxed);
|
||||
let dir =
|
||||
std::env::temp_dir().join(format!("ledger-mcp-spec-{}-{name}-{n}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
dir
|
||||
TestDir(dir)
|
||||
}
|
||||
|
||||
/// A directory beside the books dir `dir`, outside it (vault keys, files
|
||||
@@ -262,6 +293,7 @@ impl Mcp {
|
||||
}
|
||||
});
|
||||
let mut mcp = Self {
|
||||
books_dir: None,
|
||||
stdin,
|
||||
rx,
|
||||
child,
|
||||
@@ -1247,6 +1279,7 @@ fn happy_path(name: &str) -> Mcp {
|
||||
"save_to": outside(&dir).join("cpa.zip").to_str().unwrap()}),
|
||||
);
|
||||
|
||||
m.books_dir = Some(dir);
|
||||
m
|
||||
}
|
||||
|
||||
@@ -1692,7 +1725,7 @@ const REFUSED_AMOUNTS: &[&str] = &[
|
||||
/// the core is expected to accept on a line.
|
||||
const ACCEPTED_AMOUNTS: &[&str] = &["5", "5.00", "123.45", "5.5", "007.50", "1000000.00"];
|
||||
|
||||
fn amounts_server(name: &str) -> (PathBuf, Mcp, u64) {
|
||||
fn amounts_server(name: &str) -> (TestDir, Mcp, u64) {
|
||||
let dir = test_dir(name);
|
||||
let toml = books_toml(&dir, "25.00", "");
|
||||
let mut m = Mcp::start(&dir, &toml);
|
||||
|
||||
@@ -50,6 +50,9 @@ struct Mcp {
|
||||
next_id: u64,
|
||||
data: PathBuf,
|
||||
validators: HashMap<String, jsonschema::Validator>,
|
||||
/// The books dir when the server owns it: removed after the server
|
||||
/// stops (fields drop after `Drop::drop`).
|
||||
books_dir: Option<TestDir>,
|
||||
}
|
||||
|
||||
impl Drop for Mcp {
|
||||
@@ -61,7 +64,29 @@ impl Drop for Mcp {
|
||||
|
||||
static DIR_SEQ: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
fn test_dir(name: &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 AsRef<std::path::Path> for TestDir {
|
||||
fn as_ref(&self) -> &std::path::Path {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TestDir {
|
||||
fn drop(&mut self) {
|
||||
let _ = std::fs::remove_dir_all(&self.0);
|
||||
}
|
||||
}
|
||||
|
||||
fn test_dir(name: &str) -> TestDir {
|
||||
let n = DIR_SEQ.fetch_add(1, Ordering::Relaxed);
|
||||
let dir = std::env::temp_dir().join(format!(
|
||||
"ledger-mcp-spec-parties-{}-{name}-{n}",
|
||||
@@ -69,7 +94,7 @@ fn test_dir(name: &str) -> PathBuf {
|
||||
));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
dir
|
||||
TestDir(dir)
|
||||
}
|
||||
|
||||
/// `wanderbooks.toml` with a company name and extra `[books]` TOML. The
|
||||
@@ -114,6 +139,7 @@ impl Mcp {
|
||||
}
|
||||
});
|
||||
let mut mcp = Self {
|
||||
books_dir: None,
|
||||
stdin,
|
||||
rx,
|
||||
child,
|
||||
@@ -339,6 +365,7 @@ fn books(name: &str) -> Mcp {
|
||||
let toml = books_toml(&dir, "Spec Co", "");
|
||||
let mut m = Mcp::start(&dir, &toml);
|
||||
m.accounts(CHART);
|
||||
m.books_dir = Some(dir);
|
||||
m
|
||||
}
|
||||
|
||||
@@ -1161,6 +1188,7 @@ fn books_with(name: &str, extra: &str) -> Mcp {
|
||||
let toml = books_toml(&dir, "Spec Co", extra);
|
||||
let mut m = Mcp::start(&dir, &toml);
|
||||
m.accounts(CHART);
|
||||
m.books_dir = Some(dir);
|
||||
m
|
||||
}
|
||||
|
||||
@@ -1343,6 +1371,7 @@ fn member_books(name: &str, extra: &str) -> Mcp {
|
||||
m.accounts(&[("3001", "Capital K", "equity")]);
|
||||
m.add_party("Tobin Harlow", &["member"]);
|
||||
m.add_party("Morgan Ellis", &["member"]);
|
||||
m.books_dir = Some(dir);
|
||||
m
|
||||
}
|
||||
|
||||
|
||||
@@ -58,6 +58,9 @@ struct Mcp {
|
||||
validators: HashMap<String, jsonschema::Validator>,
|
||||
/// Every response line the server wrote, with the tool it answered.
|
||||
transcript: Vec<(String, String)>,
|
||||
/// The books dir when the server owns it: removed after the server
|
||||
/// stops (fields drop after `Drop::drop`).
|
||||
books_dir: Option<TestDir>,
|
||||
}
|
||||
|
||||
impl Drop for Mcp {
|
||||
@@ -69,7 +72,35 @@ impl Drop for Mcp {
|
||||
|
||||
static DIR_SEQ: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
fn test_dir(name: &str) -> PathBuf {
|
||||
/// A test's dir and the folders beside it (`-inbox`, `-outside`),
|
||||
/// 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 AsRef<std::path::Path> for TestDir {
|
||||
fn as_ref(&self) -> &std::path::Path {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TestDir {
|
||||
fn drop(&mut self) {
|
||||
let _ = std::fs::remove_dir_all(&self.0);
|
||||
for beside in ["-inbox", "-outside"] {
|
||||
let p = PathBuf::from(format!("{}{beside}", self.0.display()));
|
||||
let _ = std::fs::remove_dir_all(&p);
|
||||
let _ = std::fs::remove_file(&p);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn test_dir(name: &str) -> TestDir {
|
||||
let n = DIR_SEQ.fetch_add(1, Ordering::Relaxed);
|
||||
let dir = std::env::temp_dir().join(format!(
|
||||
"ledger-mcp-spec-vault-{}-{name}-{n}",
|
||||
@@ -77,7 +108,7 @@ fn test_dir(name: &str) -> PathBuf {
|
||||
));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
dir
|
||||
TestDir(dir)
|
||||
}
|
||||
|
||||
/// A directory beside the books dir `dir`, outside it: the vault key,
|
||||
@@ -164,6 +195,7 @@ impl Mcp {
|
||||
}
|
||||
});
|
||||
let mut mcp = Self {
|
||||
books_dir: None,
|
||||
stdin,
|
||||
rx,
|
||||
child,
|
||||
@@ -402,6 +434,7 @@ fn books_with(name: &str, extra: &str) -> Mcp {
|
||||
json!({"vendor": "Acme Design", "form_1099": "nec"}),
|
||||
);
|
||||
m.add_party("Rent Co", &["landlord"]);
|
||||
m.books_dir = Some(dir);
|
||||
m
|
||||
}
|
||||
|
||||
|
||||
@@ -7,11 +7,39 @@ use std::time::{Duration, Instant};
|
||||
|
||||
const BIN: &str = env!("CARGO_BIN_EXE_ledger-mcp");
|
||||
|
||||
fn temp(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 AsRef<std::path::Path> for TestDir {
|
||||
fn as_ref(&self) -> &std::path::Path {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<std::ffi::OsStr> for TestDir {
|
||||
fn as_ref(&self) -> &std::ffi::OsStr {
|
||||
self.0.as_os_str()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TestDir {
|
||||
fn drop(&mut self) {
|
||||
let _ = std::fs::remove_dir_all(&self.0);
|
||||
}
|
||||
}
|
||||
|
||||
fn temp(tag: &str) -> TestDir {
|
||||
let d = std::env::temp_dir().join(format!("ledger-mcp-start-{}-{tag}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&d);
|
||||
std::fs::create_dir_all(&d).unwrap();
|
||||
d
|
||||
TestDir(d)
|
||||
}
|
||||
|
||||
/// Start the server, initialize, call one tool, and return its text
|
||||
@@ -236,7 +264,8 @@ fn refuses_to_start(cmd: &mut Command) -> Option<String> {
|
||||
/// used.
|
||||
#[test]
|
||||
fn an_import_root_holding_the_books_or_the_home_folder_is_refused() {
|
||||
let dir = temp("bad-roots").canonicalize().unwrap();
|
||||
let tmp = temp("bad-roots");
|
||||
let dir = tmp.canonicalize().unwrap();
|
||||
let home = dir.join("home");
|
||||
let books = home.join("work/books");
|
||||
let data = books.join("books-data");
|
||||
@@ -316,7 +345,8 @@ fn an_import_root_holding_the_books_or_the_home_folder_is_refused() {
|
||||
/// is the root, and the rest of the home folder stays out of reach.
|
||||
#[test]
|
||||
fn a_download_dir_of_home_never_opens_the_home_folder() {
|
||||
let dir = temp("xdg-home").canonicalize().unwrap();
|
||||
let tmp = temp("xdg-home");
|
||||
let dir = tmp.canonicalize().unwrap();
|
||||
let home = dir.join("home");
|
||||
std::fs::create_dir_all(home.join(".ssh")).unwrap();
|
||||
std::fs::write(home.join(".ssh/id_ed25519"), "PRIVATE KEY").unwrap();
|
||||
@@ -358,7 +388,8 @@ fn a_download_dir_of_home_never_opens_the_home_folder() {
|
||||
/// folder, and an import says so.
|
||||
#[test]
|
||||
fn a_default_download_dir_holding_the_books_is_dropped_not_fatal() {
|
||||
let dir = temp("xdg-books").canonicalize().unwrap();
|
||||
let tmp = temp("xdg-books");
|
||||
let dir = tmp.canonicalize().unwrap();
|
||||
let home = dir.join("home");
|
||||
let downloads = dir.join("downloads");
|
||||
std::fs::create_dir_all(&home).unwrap();
|
||||
|
||||
@@ -68,6 +68,16 @@ struct Books {
|
||||
toml: PathBuf,
|
||||
}
|
||||
|
||||
/// The books dir and the folders beside it go when the test ends, pass or
|
||||
/// panic.
|
||||
impl Drop for Books {
|
||||
fn drop(&mut self) {
|
||||
if let Some(base) = self.dir.parent() {
|
||||
let _ = std::fs::remove_dir_all(base);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn books(tag: &str) -> Books {
|
||||
let base =
|
||||
std::env::temp_dir().join(format!("ledger-mcp-tin-files-{}-{tag}", std::process::id()));
|
||||
|
||||
Reference in new issue
Block a user