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:
44 files changed
+952
-163
No files matched your search
@@ -3,11 +3,27 @@
|
|||||||
|
|
||||||
use ledger_config::{BooksSettings, effective_receipt_policy};
|
use ledger_config::{BooksSettings, effective_receipt_policy};
|
||||||
|
|
||||||
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-config-policy-{}-{tag}", std::process::id()));
|
let d = std::env::temp_dir().join(format!("ledger-config-policy-{}-{tag}", std::process::id()));
|
||||||
let _ = std::fs::remove_dir_all(&d);
|
let _ = std::fs::remove_dir_all(&d);
|
||||||
std::fs::create_dir_all(&d).unwrap();
|
std::fs::create_dir_all(&d).unwrap();
|
||||||
d
|
TestDir(d)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -37,5 +53,4 @@ fn config_json_wins_else_the_settings_file() {
|
|||||||
err.starts_with(&format!("bad config {}", json.display())),
|
err.starts_with(&format!("bad config {}", json.display())),
|
||||||
"{err}"
|
"{err}"
|
||||||
);
|
);
|
||||||
std::fs::remove_dir_all(&d).unwrap();
|
|
||||||
}
|
}
|
||||||
@@ -670,16 +670,16 @@ mod tests {
|
|||||||
let dir =
|
let dir =
|
||||||
std::env::temp_dir().join(format!("ledger-gui-attention-{}-{tag}", std::process::id()));
|
std::env::temp_dir().join(format!("ledger-gui-attention-{}-{tag}", std::process::id()));
|
||||||
let _ = std::fs::remove_dir_all(&dir);
|
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");
|
let data_dir = dir.join("books-data");
|
||||||
ledger_store::Store::create(&data_dir, ledger_core::Currency::USD).unwrap();
|
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,
|
data_dir,
|
||||||
books_toml: None,
|
books_toml: None,
|
||||||
today: Some(today),
|
today: Some(today),
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap()
|
||||||
let _ = std::fs::remove_dir_all(&dir);
|
|
||||||
b
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// INVARIANTS 2 and 4: a dated rule past its review is one Todo item
|
/// INVARIANTS 2 and 4: a dated rule past its review is one Todo item
|
||||||
|
|||||||
@@ -2169,6 +2169,15 @@ mod tests {
|
|||||||
}
|
}
|
||||||
use super::*;
|
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:
|
/// 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
|
/// 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
|
/// (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 today = NaiveDate::from_ymd_opt(2026, 10, 12).unwrap();
|
||||||
let dir = std::env::temp_dir().join(format!("ledger-gui-tax-{}", std::process::id()));
|
let dir = std::env::temp_dir().join(format!("ledger-gui-tax-{}", std::process::id()));
|
||||||
let _ = std::fs::remove_dir_all(&dir);
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
|
let _cleanup = TestDir(dir.clone());
|
||||||
crate::demo::build(&dir, today).unwrap();
|
crate::demo::build(&dir, today).unwrap();
|
||||||
let b = crate::data::load_once(&crate::data::Source {
|
let b = crate::data::load_once(&crate::data::Source {
|
||||||
data_dir: dir.join("books-data"),
|
data_dir: dir.join("books-data"),
|
||||||
@@ -2234,7 +2244,6 @@ mod tests {
|
|||||||
.iter()
|
.iter()
|
||||||
.any(|row| row.kind == Kind::Note && row.label.contains("fair market value"))
|
.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,
|
/// 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 today = NaiveDate::from_ymd_opt(2026, 10, 12).unwrap();
|
||||||
let dir = std::env::temp_dir().join(format!("ledger-gui-pdf-{}", std::process::id()));
|
let dir = std::env::temp_dir().join(format!("ledger-gui-pdf-{}", std::process::id()));
|
||||||
let _ = std::fs::remove_dir_all(&dir);
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
|
let _cleanup = TestDir(dir.clone());
|
||||||
crate::demo::build(&dir, today).unwrap();
|
crate::demo::build(&dir, today).unwrap();
|
||||||
let b = crate::data::load_once(&crate::data::Source {
|
let b = crate::data::load_once(&crate::data::Source {
|
||||||
data_dir: dir.join("books-data"),
|
data_dir: dir.join("books-data"),
|
||||||
@@ -2296,6 +2306,5 @@ mod tests {
|
|||||||
std::fs::write(d.join(format!("{name}.pdf")), bytes).unwrap();
|
std::fs::write(d.join(format!("{name}.pdf")), bytes).unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let _ = std::fs::remove_dir_all(&dir);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -17,22 +17,34 @@ use ledger_gui::nav::{ReportKind, Route, Section};
|
|||||||
use ledger_gui::palette::{crumbs, describe};
|
use ledger_gui::palette::{crumbs, describe};
|
||||||
use ledger_gui::{App, Source, data};
|
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 dir = std::env::temp_dir().join(format!("ledger-gui-crumbs-{tag}-{}", std::process::id()));
|
||||||
let _ = std::fs::remove_dir_all(&dir);
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
let today = chrono::NaiveDate::from_ymd_opt(2026, 10, 12).unwrap();
|
let today = chrono::NaiveDate::from_ymd_opt(2026, 10, 12).unwrap();
|
||||||
ledger_gui::demo::build(&dir, today).unwrap();
|
ledger_gui::demo::build(&dir, today).unwrap();
|
||||||
Source {
|
let source = Source {
|
||||||
data_dir: dir.join("books-data"),
|
data_dir: dir.join("books-data"),
|
||||||
books_toml: Some(dir.join("wanderbooks.toml")),
|
books_toml: Some(dir.join("wanderbooks.toml")),
|
||||||
today: Some(today),
|
today: Some(today),
|
||||||
}
|
};
|
||||||
|
(TestDir(dir), source)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// INVARIANTS 1-3 on every kind of page in the demo books.
|
/// INVARIANTS 1-3 on every kind of page in the demo books.
|
||||||
#[test]
|
#[test]
|
||||||
fn crumbs_link_every_level_but_the_last() {
|
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 party = b.ledger.parties().next().unwrap().id;
|
||||||
let rec = b.reconciliations.first().expect("the demo reconciles");
|
let rec = b.reconciliations.first().expect("the demo reconciles");
|
||||||
let entry = b.entries.first().unwrap().id;
|
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.
|
/// INVARIANT 4, through the top bar.
|
||||||
#[test]
|
#[test]
|
||||||
fn clicking_a_crumb_goes_there() {
|
fn clicking_a_crumb_goes_there() {
|
||||||
let source = demo("click");
|
let (_dir, source) = demo("click");
|
||||||
let b = data::load_once(&source).unwrap();
|
let b = data::load_once(&source).unwrap();
|
||||||
let rec = b.reconciliations.first().unwrap().clone();
|
let rec = b.reconciliations.first().unwrap().clone();
|
||||||
let account = describe(&b, &Route::Account(rec.account_id));
|
let account = describe(&b, &Route::Account(rec.account_id));
|
||||||
|
|||||||
@@ -21,6 +21,15 @@ use ledger_gui::{App, Source, data, fmt};
|
|||||||
use proptest::prelude::*;
|
use proptest::prelude::*;
|
||||||
use rust_decimal::Decimal;
|
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! {
|
proptest! {
|
||||||
/// INVARIANT 1: every display style copies back to the same value.
|
/// INVARIANT 1: every display style copies back to the same value.
|
||||||
#[test]
|
#[test]
|
||||||
@@ -54,6 +63,7 @@ fn copied(h: &Harness<'_, App>) -> Vec<String> {
|
|||||||
fn clicking_a_value_copies_it() {
|
fn clicking_a_value_copies_it() {
|
||||||
let dir = std::env::temp_dir().join(format!("ledger-gui-copy-{}", std::process::id()));
|
let dir = std::env::temp_dir().join(format!("ledger-gui-copy-{}", std::process::id()));
|
||||||
let _ = std::fs::remove_dir_all(&dir);
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
|
let _cleanup = TestDir(dir.clone());
|
||||||
let today = chrono::NaiveDate::from_ymd_opt(2026, 10, 12).unwrap();
|
let today = chrono::NaiveDate::from_ymd_opt(2026, 10, 12).unwrap();
|
||||||
ledger_gui::demo::build(&dir, today).unwrap();
|
ledger_gui::demo::build(&dir, today).unwrap();
|
||||||
let source = Source {
|
let source = Source {
|
||||||
@@ -106,5 +116,4 @@ fn clicking_a_value_copies_it() {
|
|||||||
h.get_by_label(&format!("Entry #{id}")).click();
|
h.get_by_label(&format!("Entry #{id}")).click();
|
||||||
h.step();
|
h.step();
|
||||||
assert_eq!(copied(&h), [format!("#{id}")]);
|
assert_eq!(copied(&h), [format!("#{id}")]);
|
||||||
let _ = std::fs::remove_dir_all(&dir);
|
|
||||||
}
|
}
|
||||||
@@ -9,10 +9,20 @@ use ledger_store::Store;
|
|||||||
use rust_decimal::Decimal;
|
use rust_decimal::Decimal;
|
||||||
use std::time::{Duration, Instant};
|
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]
|
#[test]
|
||||||
fn a_change_in_another_process_shows_up_live() {
|
fn a_change_in_another_process_shows_up_live() {
|
||||||
let dir = std::env::temp_dir().join(format!("ledger-gui-live-{}", std::process::id()));
|
let dir = std::env::temp_dir().join(format!("ledger-gui-live-{}", std::process::id()));
|
||||||
let _ = std::fs::remove_dir_all(&dir);
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
|
let _cleanup = TestDir(dir.clone());
|
||||||
let data = dir.join("books-data");
|
let data = dir.join("books-data");
|
||||||
let mut store = Store::create(&data, Currency::USD).unwrap();
|
let mut store = Store::create(&data, Currency::USD).unwrap();
|
||||||
let cash = store
|
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");
|
assert!(shown, "the show request never reached the viewer");
|
||||||
eprintln!("show request arrived in {:?}", asked.elapsed());
|
eprintln!("show request arrived in {:?}", asked.elapsed());
|
||||||
let _ = std::fs::remove_file(ledger_store::viewer::command_path(&data));
|
let _ = std::fs::remove_file(ledger_store::viewer::command_path(&data));
|
||||||
let _ = std::fs::remove_dir_all(&dir);
|
|
||||||
}
|
}
|
||||||
@@ -16,7 +16,27 @@ use serde_json::json;
|
|||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::time::{Duration, Instant};
|
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 dir = std::env::temp_dir().join(format!("ledger-gui-gate-{tag}-{}", std::process::id()));
|
||||||
let _ = std::fs::remove_dir_all(&dir);
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
let data = dir.join("books-data");
|
let data = dir.join("books-data");
|
||||||
@@ -24,7 +44,7 @@ fn books(tag: &str) -> PathBuf {
|
|||||||
store
|
store
|
||||||
.add_account("1000", "Checking", AccountType::Asset, None, false)
|
.add_account("1000", "Checking", AccountType::Asset, None, false)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
data
|
DataDir { dir, data }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn source(data: &Path) -> Source {
|
fn source(data: &Path) -> Source {
|
||||||
|
|||||||
@@ -67,13 +67,15 @@ fn set_read_only(root: &Path, read_only: bool) {
|
|||||||
walk(root, read_only);
|
walk(root, read_only);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Makes the books writable again when dropped, so a failing or
|
/// Makes the books writable again and removes them when dropped, so a
|
||||||
/// interrupted run never leaves a folder no one can delete.
|
/// run that passes or fails never leaves a folder behind, let alone one no
|
||||||
|
/// one can delete.
|
||||||
struct Writable(PathBuf);
|
struct Writable(PathBuf);
|
||||||
impl Drop for Writable {
|
impl Drop for Writable {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
if self.0.exists() {
|
if self.0.exists() {
|
||||||
set_read_only(&self.0, false);
|
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);
|
set_read_only(&dir, false);
|
||||||
std::fs::remove_dir_all(&dir).unwrap();
|
std::fs::remove_dir_all(&dir).unwrap();
|
||||||
}
|
}
|
||||||
|
let _restore = Writable(dir.clone());
|
||||||
let today = chrono::NaiveDate::from_ymd_opt(2026, 10, 12).unwrap();
|
let today = chrono::NaiveDate::from_ymd_opt(2026, 10, 12).unwrap();
|
||||||
ledger_gui::demo::build(&dir, today).unwrap();
|
ledger_gui::demo::build(&dir, today).unwrap();
|
||||||
let source = Source {
|
let source = Source {
|
||||||
@@ -112,7 +115,6 @@ fn the_viewer_never_writes_into_the_books() {
|
|||||||
books_toml: Some(dir.join("wanderbooks.toml")),
|
books_toml: Some(dir.join("wanderbooks.toml")),
|
||||||
today: Some(today),
|
today: Some(today),
|
||||||
};
|
};
|
||||||
let _restore = Writable(dir.clone());
|
|
||||||
set_read_only(&dir, true);
|
set_read_only(&dir, true);
|
||||||
let before = tree(&dir);
|
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"
|
"the viewer created or removed files in writable books"
|
||||||
);
|
);
|
||||||
assert!(before == after, "the viewer changed writable books");
|
assert!(before == after, "the viewer changed writable books");
|
||||||
let _ = std::fs::remove_dir_all(&dir);
|
|
||||||
}
|
}
|
||||||
@@ -7,6 +7,15 @@ use ledger_gui::nav::{ReportKind, Route};
|
|||||||
use ledger_gui::{App, Source, data};
|
use ledger_gui::{App, Source, data};
|
||||||
use std::path::PathBuf;
|
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]
|
#[test]
|
||||||
fn screenshots() {
|
fn screenshots() {
|
||||||
let Ok(out) = std::env::var("SHOT_DIR") else {
|
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")
|
let fixed_today: Option<chrono::NaiveDate> = std::env::var("SHOT_TODAY")
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|d| d.parse().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") {
|
let books = match std::env::var("SHOT_BOOKS") {
|
||||||
Ok(b) => PathBuf::from(b),
|
Ok(b) => PathBuf::from(b),
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
@@ -23,6 +34,7 @@ fn screenshots() {
|
|||||||
let dir =
|
let dir =
|
||||||
std::env::temp_dir().join(format!("ledger-gui-shots-demo-{}", std::process::id()));
|
std::env::temp_dir().join(format!("ledger-gui-shots-demo-{}", std::process::id()));
|
||||||
let _ = std::fs::remove_dir_all(&dir);
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
|
_demo = Some(TestDir(dir.clone()));
|
||||||
ledger_gui::demo::build(&dir, d).unwrap();
|
ledger_gui::demo::build(&dir, d).unwrap();
|
||||||
dir
|
dir
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,11 +5,21 @@ use ledger_gui::nav::{ReportKind, Route};
|
|||||||
use ledger_gui::{App, Source, data};
|
use ledger_gui::{App, Source, data};
|
||||||
use ledger_store::viewer::ViewerTarget;
|
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]
|
#[test]
|
||||||
fn show_requests_navigate_the_viewer() {
|
fn show_requests_navigate_the_viewer() {
|
||||||
let today = chrono::NaiveDate::from_ymd_opt(2026, 10, 12).unwrap();
|
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 dir = std::env::temp_dir().join(format!("ledger-gui-show-{}", std::process::id()));
|
||||||
let _ = std::fs::remove_dir_all(&dir);
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
|
let _cleanup = TestDir(dir.clone());
|
||||||
ledger_gui::demo::build(&dir, today).unwrap();
|
ledger_gui::demo::build(&dir, today).unwrap();
|
||||||
let source = Source {
|
let source = Source {
|
||||||
data_dir: dir.join("books-data"),
|
data_dir: dir.join("books-data"),
|
||||||
@@ -108,5 +118,4 @@ fn show_requests_navigate_the_viewer() {
|
|||||||
end: None,
|
end: None,
|
||||||
});
|
});
|
||||||
assert_eq!(h.state_mut().route(), &Route::Transactions);
|
assert_eq!(h.state_mut().route(), &Route::Transactions);
|
||||||
let _ = std::fs::remove_dir_all(&dir);
|
|
||||||
}
|
}
|
||||||
@@ -10,10 +10,20 @@ use ledger_vault::{Tin, Vault};
|
|||||||
|
|
||||||
const SSN: &str = "123-45-6789";
|
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]
|
#[test]
|
||||||
fn the_key_named_on_the_command_line_wins() {
|
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 root = std::env::temp_dir().join(format!("ledger-gui-key-flag-{}", std::process::id()));
|
||||||
let _ = std::fs::remove_dir_all(&root);
|
let _ = std::fs::remove_dir_all(&root);
|
||||||
|
let _cleanup = TestDir(root.clone());
|
||||||
let books = root.join("books");
|
let books = root.join("books");
|
||||||
let key = root.join("keys/vault.key");
|
let key = root.join("keys/vault.key");
|
||||||
std::fs::create_dir_all(&books).unwrap();
|
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.
|
// The settings name a key that isn't there; the command line wins.
|
||||||
b.settings.vault_key_file = Some(root.join("missing/vault.key"));
|
b.settings.vault_key_file = Some(root.join("missing/vault.key"));
|
||||||
assert_eq!(b.tin_from_vault(party).unwrap(), SSN);
|
assert_eq!(b.tin_from_vault(party).unwrap(), SSN);
|
||||||
let _ = std::fs::remove_dir_all(&root);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -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 d = std::env::temp_dir().join(format!("ledger-gui-vm-{}-{tag}", std::process::id()));
|
||||||
let _ = std::fs::remove_dir_all(&d);
|
let _ = std::fs::remove_dir_all(&d);
|
||||||
std::fs::create_dir_all(&d).unwrap();
|
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 d = dir(tag);
|
||||||
let mut s = Store::create(&d.join("books-data"), Currency::USD).unwrap();
|
let mut s = Store::create(&d.join("books-data"), Currency::USD).unwrap();
|
||||||
let ids: Vec<_> = CHART
|
let ids: Vec<_> = CHART
|
||||||
@@ -81,14 +98,12 @@ fn build(plan: &Plan, tag: &str) -> (Source, ledger_core::Ledger) {
|
|||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let ledger = s.ledger().clone();
|
let ledger = s.ledger().clone();
|
||||||
(
|
let source = Source {
|
||||||
Source {
|
data_dir: d.join("books-data"),
|
||||||
data_dir: d.join("books-data"),
|
books_toml: Some(d.join("wanderbooks.toml")),
|
||||||
books_toml: Some(d.join("wanderbooks.toml")),
|
today: Some(plan.today),
|
||||||
today: Some(plan.today),
|
};
|
||||||
},
|
(d, source, ledger)
|
||||||
ledger,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An account's normal-side balance at the end of `date`, from the trial
|
/// 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.
|
/// Kpis INVARIANTS 1-7 and the period helpers, against ledger-core.
|
||||||
#[test]
|
#[test]
|
||||||
fn the_overview_numbers_are_the_books_numbers(p in plan()) {
|
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 b = data::load_once(&source).unwrap();
|
||||||
let k = &b.kpis;
|
let k = &b.kpis;
|
||||||
let t = p.today;
|
let t = p.today;
|
||||||
@@ -283,7 +298,6 @@ fn origins_and_parties() {
|
|||||||
assert_eq!(std::fs::read(path).unwrap(), b"%PDF receipt");
|
assert_eq!(std::fs::read(path).unwrap(), b"%PDF receipt");
|
||||||
assert_eq!(b.integrity.files_ok, 1);
|
assert_eq!(b.integrity.files_ok, 1);
|
||||||
assert!(b.integrity.file_failures.is_empty());
|
assert!(b.integrity.file_failures.is_empty());
|
||||||
let _ = std::fs::remove_dir_all(&d);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// fmt helpers the viewer shows: percent change and tax forms.
|
/// 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");
|
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 d = dir(tag);
|
||||||
let s = Store::create(&d.join("books-data"), Currency::USD).unwrap();
|
let s = Store::create(&d.join("books-data"), Currency::USD).unwrap();
|
||||||
(d, s)
|
(d, s)
|
||||||
@@ -476,9 +490,6 @@ fn comparisons_and_burn_on_fixed_dates() {
|
|||||||
drop(s5);
|
drop(s5);
|
||||||
let b5 = data::load_once(&source(&d5, "", today)).unwrap();
|
let b5 = data::load_once(&source(&d5, "", today)).unwrap();
|
||||||
assert_eq!(b5.kpis.monthly_burn, None);
|
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
|
/// 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 integrity = ledger_gui::books::Integrity::check(&snap, &mut verified);
|
||||||
let b2 = ledger_gui::books::Books::build(&snap, settings, today, Some(seq), integrity).unwrap();
|
let b2 = ledger_gui::books::Books::build(&snap, settings, today, Some(seq), integrity).unwrap();
|
||||||
assert_eq!(b2.fresh, [e2].into_iter().collect());
|
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
|
/// 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));
|
assert_eq!((first.files_ok, first.file_failures.len()), (2, 0));
|
||||||
let again = ledger_gui::books::Integrity::check(&snap, &mut verified);
|
let again = ledger_gui::books::Integrity::check(&snap, &mut verified);
|
||||||
assert_eq!((again.files_ok, again.file_failures.len()), (2, 0));
|
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
|
/// 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();
|
let r = b.entry(remit).unwrap();
|
||||||
assert_eq!(r.origin, "Payroll taxes");
|
assert_eq!(r.origin, "Payroll taxes");
|
||||||
assert!(!r.missing_receipt, "a remittance isn't a hand-made entry");
|
assert!(!r.missing_receipt, "a remittance isn't a hand-made entry");
|
||||||
let _ = std::fs::remove_dir_all(&d);
|
|
||||||
}
|
}
|
||||||
@@ -660,6 +660,21 @@ mod tests {
|
|||||||
allowed: HashMap<PathBuf, bool>,
|
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 {
|
fn tree(tag: &str) -> Tree {
|
||||||
let base = std::env::temp_dir().join(format!(
|
let base = std::env::temp_dir().join(format!(
|
||||||
"ledger-mcp-local-files-{}-{tag}",
|
"ledger-mcp-local-files-{}-{tag}",
|
||||||
@@ -813,25 +828,43 @@ mod tests {
|
|||||||
let _ = std::fs::remove_dir_all(b);
|
let _ = std::fs::remove_dir_all(b);
|
||||||
}
|
}
|
||||||
|
|
||||||
proptest! {
|
/// Any absolute path built from the tree's names, `.` and `..`, and
|
||||||
#![proptest_config(ProptestConfig::with_cases(512))]
|
/// its symlinks is allowed exactly when it has no `.` or `..` name and
|
||||||
/// Any absolute path built from the tree's names, `.` and `..`, and
|
/// resolves to a file the oracle allows: a regular file inside a root
|
||||||
/// its symlinks is allowed exactly when it has no `.` or `..` name
|
/// and outside every protected dir. (One tree for every case, removed
|
||||||
/// and resolves to a file the oracle allows: a regular file inside
|
/// when the test ends.)
|
||||||
/// a root and outside every protected dir.
|
#[test]
|
||||||
#[test]
|
fn import_allowed_iff_regular_file_inside_a_root_outside_the_books() {
|
||||||
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,
|
start in 0usize..7,
|
||||||
parts in proptest::collection::vec(0usize..14, 0..5),
|
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]);
|
let mut p = t.base.join(starts[start]);
|
||||||
for i in parts {
|
for i in parts {
|
||||||
p.push(names[i]);
|
p.push(names[i]);
|
||||||
@@ -843,7 +876,7 @@ mod tests {
|
|||||||
_ => false,
|
_ => false,
|
||||||
};
|
};
|
||||||
prop_assert_eq!(t.check(&p).is_ok(), want, "{}", p.display());
|
prop_assert_eq!(t.check(&p).is_ok(), want, "{}", p.display());
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// INVARIANT 2: a path that is missing, a dangling link, or outside
|
/// INVARIANT 2: a path that is missing, a dangling link, or outside
|
||||||
@@ -1030,9 +1063,8 @@ mod tests {
|
|||||||
.is_ok_and(|s| s.success());
|
.is_ok_and(|s| s.success());
|
||||||
if made {
|
if made {
|
||||||
let (tx, rx) = std::sync::mpsc::channel();
|
let (tx, rx) = std::sync::mpsc::channel();
|
||||||
std::thread::spawn(move || {
|
let inodes = t.protected.inodes();
|
||||||
tx.send(read_checked(&canonical, &meta, &t.protected.inodes(), 100))
|
std::thread::spawn(move || tx.send(read_checked(&canonical, &meta, &inodes, 100)));
|
||||||
});
|
|
||||||
let got = rx
|
let got = rx
|
||||||
.recv_timeout(std::time::Duration::from_secs(10))
|
.recv_timeout(std::time::Duration::from_secs(10))
|
||||||
.expect("opening a pipe blocked");
|
.expect("opening a pipe blocked");
|
||||||
@@ -1219,6 +1251,7 @@ mod tests {
|
|||||||
std::process::id()
|
std::process::id()
|
||||||
));
|
));
|
||||||
let _ = std::fs::remove_dir_all(&base);
|
let _ = std::fs::remove_dir_all(&base);
|
||||||
|
let _cleanup = TestDir(base.clone());
|
||||||
let home = base.join("home");
|
let home = base.join("home");
|
||||||
std::fs::create_dir_all(home.join(".ssh")).unwrap();
|
std::fs::create_dir_all(home.join(".ssh")).unwrap();
|
||||||
std::fs::write(home.join(".ssh/id_ed25519"), b"PRIVATE KEY").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)
|
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 =
|
let dir =
|
||||||
std::env::temp_dir().join(format!("ledger-mcp-unit-{}-{tag}", std::process::id()));
|
std::env::temp_dir().join(format!("ledger-mcp-unit-{}-{tag}", std::process::id()));
|
||||||
let _ = std::fs::remove_dir_all(&dir);
|
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
|
/// 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.
|
/// close_into, and each member's capital and distributions accounts.
|
||||||
#[test]
|
#[test]
|
||||||
fn config_mentions_finds_every_setting_naming_a_code() {
|
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_company(members())
|
||||||
.with_cash_roles(CashRoles {
|
.with_cash_roles(CashRoles {
|
||||||
cash: vec!["1000".into()],
|
cash: vec!["1000".into()],
|
||||||
@@ -9664,20 +9679,21 @@ mod tests {
|
|||||||
["[[company.members]] Ada distribution_account"]
|
["[[company.members]] Ada distribution_account"]
|
||||||
);
|
);
|
||||||
assert!(server.config_mentions("6000").is_empty());
|
assert!(server.config_mentions("6000").is_empty());
|
||||||
let solo = BooksServer::new(store("mentions-solo"), ReceiptPolicy::default()).with_company(
|
let books = unit_dir("mentions-solo");
|
||||||
Company {
|
let solo =
|
||||||
|
BooksServer::new(store(&books), ReceiptPolicy::default()).with_company(Company {
|
||||||
close_into: Some("3000".into()),
|
close_into: Some("3000".into()),
|
||||||
..Company::default()
|
..Company::default()
|
||||||
},
|
});
|
||||||
);
|
|
||||||
assert_eq!(solo.config_mentions("3000"), ["[books] close_into"]);
|
assert_eq!(solo.config_mentions("3000"), ["[books] close_into"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reports name each member with their accounts, in order.
|
/// Reports name each member with their accounts, in order.
|
||||||
#[test]
|
#[test]
|
||||||
fn report_members_carry_names_and_accounts() {
|
fn report_members_carry_names_and_accounts() {
|
||||||
|
let books = unit_dir("members");
|
||||||
let server =
|
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();
|
let m = server.report_members();
|
||||||
assert_eq!(m.len(), 2);
|
assert_eq!(m.len(), 2);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -9697,7 +9713,8 @@ mod tests {
|
|||||||
/// included, so a closed month drops out).
|
/// included, so a closed month drops out).
|
||||||
#[test]
|
#[test]
|
||||||
fn year_earnings_split_closed_from_unclosed() {
|
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
|
let cash = s
|
||||||
.add_account("1000", "Cash", AccountType::Asset, None, false)
|
.add_account("1000", "Cash", AccountType::Asset, None, false)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -9747,7 +9764,8 @@ mod tests {
|
|||||||
/// December's income is never this year's).
|
/// December's income is never this year's).
|
||||||
#[test]
|
#[test]
|
||||||
fn year_earnings_count_only_closes_and_never_before_the_year() {
|
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
|
let cash = s
|
||||||
.add_account("1000", "Cash", AccountType::Asset, None, false)
|
.add_account("1000", "Cash", AccountType::Asset, None, false)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -9783,7 +9801,8 @@ mod tests {
|
|||||||
/// entry the policy covers (crate::policy).
|
/// entry the policy covers (crate::policy).
|
||||||
#[test]
|
#[test]
|
||||||
fn receipts_are_kept_only_where_the_policy_needs_them() {
|
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
|
let cash = s
|
||||||
.add_account("1000", "Cash", AccountType::Asset, None, false)
|
.add_account("1000", "Cash", AccountType::Asset, None, false)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -10437,7 +10456,8 @@ mod tests {
|
|||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
for (how, body) in bodies {
|
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 {
|
rt.block_on(async {
|
||||||
let seq: Body = |s| s.lock().map(|st| st.journal_seq());
|
let seq: Body = |s| s.lock().map(|st| st.journal_seq());
|
||||||
assert_eq!(run(&server, seq).await.unwrap(), Ok(1), "{how}");
|
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.
|
// 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 s = server.clone();
|
||||||
let died = std::thread::spawn(move || {
|
let died = std::thread::spawn(move || {
|
||||||
let _store = s.lock().unwrap();
|
let _store = s.lock().unwrap();
|
||||||
@@ -10500,7 +10521,8 @@ mod tests {
|
|||||||
.enable_all()
|
.enable_all()
|
||||||
.build()
|
.build()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let shared = SharedStore::new(store("wakes"));
|
let books = unit_dir("wakes");
|
||||||
|
let shared = SharedStore::new(store(&books));
|
||||||
let got = rt.block_on(async {
|
let got = rt.block_on(async {
|
||||||
tokio::time::timeout(
|
tokio::time::timeout(
|
||||||
std::time::Duration::from_secs(10),
|
std::time::Duration::from_secs(10),
|
||||||
@@ -10525,6 +10547,7 @@ mod tests {
|
|||||||
));
|
));
|
||||||
let _ = std::fs::remove_dir_all(&dir);
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
std::fs::create_dir_all(&dir).unwrap();
|
std::fs::create_dir_all(&dir).unwrap();
|
||||||
|
let _cleanup = TestDir(dir.clone());
|
||||||
let toml = dir.join("wanderbooks.toml");
|
let toml = dir.join("wanderbooks.toml");
|
||||||
let threshold = |t: &str| {
|
let threshold = |t: &str| {
|
||||||
std::fs::write(
|
std::fs::write(
|
||||||
@@ -10540,7 +10563,8 @@ mod tests {
|
|||||||
config_json: dir.join("config.json"),
|
config_json: dir.join("config.json"),
|
||||||
})
|
})
|
||||||
.unwrap();
|
.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)));
|
.with_live_settings(Arc::new(Mutex::new(live)));
|
||||||
let told = |s: &BooksServer| rmcp::ServerHandler::get_info(s).instructions.unwrap();
|
let told = |s: &BooksServer| rmcp::ServerHandler::get_info(s).instructions.unwrap();
|
||||||
let before = told(&server);
|
let before = told(&server);
|
||||||
|
|||||||
@@ -368,12 +368,28 @@ impl Live {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
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 =
|
let d =
|
||||||
std::env::temp_dir().join(format!("ledger-mcp-settings-{}-{tag}", std::process::id()));
|
std::env::temp_dir().join(format!("ledger-mcp-settings-{}-{tag}", std::process::id()));
|
||||||
let _ = std::fs::remove_dir_all(&d);
|
let _ = std::fs::remove_dir_all(&d);
|
||||||
std::fs::create_dir_all(&d).unwrap();
|
std::fs::create_dir_all(&d).unwrap();
|
||||||
d
|
TestDir(d)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn source(d: &std::path::Path, required: bool) -> Source {
|
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.current().company.name, "Kept Co 2");
|
||||||
assert_eq!(live.status().loads, 2);
|
assert_eq!(live.status().loads, 2);
|
||||||
assert!(live.current().journaled && live.is_journaled());
|
assert!(live.current().journaled && live.is_journaled());
|
||||||
std::fs::remove_dir_all(&d).unwrap();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Settings built in code are the books' own once synced.
|
/// Settings built in code are the books' own once synced.
|
||||||
|
|||||||
@@ -357,11 +357,27 @@ fn nul_interleaved(bytes: &[u8]) -> bool {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
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 =
|
let d =
|
||||||
std::env::temp_dir().join(format!("ledger-mcp-tin-files-{}-{tag}", std::process::id()));
|
std::env::temp_dir().join(format!("ledger-mcp-tin-files-{}-{tag}", std::process::id()));
|
||||||
let _ = std::fs::remove_dir_all(&d);
|
let _ = std::fs::remove_dir_all(&d);
|
||||||
d
|
TestDir(d)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// INVARIANTS 1-2: a recorded file is refused from then on, by this
|
/// 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);
|
assert_eq!(text.lines().count(), 1);
|
||||||
// Only the hash is kept.
|
// Only the hash is kept.
|
||||||
assert!(!text.contains("PNG"));
|
assert!(!text.contains("PNG"));
|
||||||
let _ = std::fs::remove_dir_all(&vault);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The registry's folder for a check (it never exists) and no 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");
|
.expect_err("a registry that can't be read let a file in");
|
||||||
assert!(why.contains("couldn't check the file"), "{why}");
|
assert!(why.contains("couldn't check the file"), "{why}");
|
||||||
assert!(record(&vault, b"bytes").is_err());
|
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
|
/// INVARIANT 4: text holding more than two TIN-shaped numbers ending
|
||||||
|
|||||||
@@ -76,6 +76,13 @@ struct Places {
|
|||||||
sandbox: PathBuf,
|
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 {
|
fn places(tag: &str) -> Places {
|
||||||
let base =
|
let base =
|
||||||
std::env::temp_dir().join(format!("ledger-mcp-hostile-{}-{tag}", std::process::id()));
|
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 {
|
fn free_port() -> u16 {
|
||||||
std::net::TcpListener::bind("127.0.0.1:0")
|
std::net::TcpListener::bind("127.0.0.1:0")
|
||||||
.unwrap()
|
.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 dir = std::env::temp_dir().join(format!("ledger-mcp-http-{}", std::process::id()));
|
||||||
let _ = std::fs::remove_dir_all(&dir);
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
std::fs::create_dir_all(&dir).unwrap();
|
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");
|
let toml = dir.join("wanderbooks.toml");
|
||||||
std::fs::write(&toml, "[books]\nreceipt_threshold = \"0.00\"\n").unwrap();
|
std::fs::write(&toml, "[books]\nreceipt_threshold = \"0.00\"\n").unwrap();
|
||||||
let data = dir.join("books-data");
|
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 dir = std::env::temp_dir().join(format!("ledger-mcp-test-{}-{name}", std::process::id()));
|
||||||
let _ = std::fs::remove_dir_all(&dir);
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
std::fs::create_dir_all(&dir).unwrap();
|
std::fs::create_dir_all(&dir).unwrap();
|
||||||
dir
|
TestDir(dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -56,7 +56,31 @@ impl Drop for Mcp {
|
|||||||
|
|
||||||
static DIR_SEQ: AtomicU64 = AtomicU64::new(0);
|
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 n = DIR_SEQ.fetch_add(1, Ordering::Relaxed);
|
||||||
let dir = std::env::temp_dir().join(format!(
|
let dir = std::env::temp_dir().join(format!(
|
||||||
"ledger-mcp-oracle-{}-{name}-{n}",
|
"ledger-mcp-oracle-{}-{name}-{n}",
|
||||||
@@ -64,7 +88,7 @@ fn test_dir(name: &str) -> PathBuf {
|
|||||||
));
|
));
|
||||||
let _ = std::fs::remove_dir_all(&dir);
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
std::fs::create_dir_all(&dir).unwrap();
|
std::fs::create_dir_all(&dir).unwrap();
|
||||||
dir
|
TestDir(dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `wanderbooks.toml` for two 50/50 members on 3000/3001, cash 1000,
|
/// `wanderbooks.toml` for two 50/50 members on 3000/3001, cash 1000,
|
||||||
@@ -2249,19 +2273,10 @@ fn receipts_and_documents_round_trip() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn uploads_up_to_25_mib_are_accepted_and_no_more() {
|
fn uploads_up_to_25_mib_are_accepted_and_no_more() {
|
||||||
const LIMIT: usize = 25 * 1024 * 1024;
|
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");
|
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);
|
let s = new_store(&dir);
|
||||||
drop(s);
|
drop(s);
|
||||||
let mut m = Mcp::start(&dir, "1000000.00");
|
let mut m = Mcp::start(&dir, "1000000.00");
|
||||||
|
|||||||
@@ -49,6 +49,9 @@ struct Mcp {
|
|||||||
next_id: u64,
|
next_id: u64,
|
||||||
data: PathBuf,
|
data: PathBuf,
|
||||||
validators: HashMap<String, jsonschema::Validator>,
|
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 {
|
impl Drop for Mcp {
|
||||||
@@ -60,7 +63,29 @@ impl Drop for Mcp {
|
|||||||
|
|
||||||
static DIR_SEQ: AtomicU64 = AtomicU64::new(0);
|
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 n = DIR_SEQ.fetch_add(1, Ordering::Relaxed);
|
||||||
let dir = std::env::temp_dir().join(format!(
|
let dir = std::env::temp_dir().join(format!(
|
||||||
"ledger-mcp-spec-assets-{}-{name}-{n}",
|
"ledger-mcp-spec-assets-{}-{name}-{n}",
|
||||||
@@ -68,7 +93,7 @@ fn test_dir(name: &str) -> PathBuf {
|
|||||||
));
|
));
|
||||||
let _ = std::fs::remove_dir_all(&dir);
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
std::fs::create_dir_all(&dir).unwrap();
|
std::fs::create_dir_all(&dir).unwrap();
|
||||||
dir
|
TestDir(dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `wanderbooks.toml` with a company name and extra `[books]` TOML. The
|
/// `wanderbooks.toml` with a company name and extra `[books]` TOML. The
|
||||||
@@ -113,6 +138,7 @@ impl Mcp {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
let mut mcp = Self {
|
let mut mcp = Self {
|
||||||
|
books_dir: None,
|
||||||
stdin,
|
stdin,
|
||||||
rx,
|
rx,
|
||||||
child,
|
child,
|
||||||
@@ -356,6 +382,7 @@ fn books(name: &str) -> Mcp {
|
|||||||
let toml = books_toml(&dir, "Spec Co", "asset_tag_prefix = \"WL\"\n");
|
let toml = books_toml(&dir, "Spec Co", "asset_tag_prefix = \"WL\"\n");
|
||||||
let mut m = Mcp::start(&dir, &toml);
|
let mut m = Mcp::start(&dir, &toml);
|
||||||
m.accounts(CHART);
|
m.accounts(CHART);
|
||||||
|
m.books_dir = Some(dir);
|
||||||
m
|
m
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,9 @@ struct Mcp {
|
|||||||
validators: HashMap<String, jsonschema::Validator>,
|
validators: HashMap<String, jsonschema::Validator>,
|
||||||
/// Every response line the server wrote, with the tool it answered.
|
/// Every response line the server wrote, with the tool it answered.
|
||||||
transcript: Vec<(String, String)>,
|
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 {
|
impl Drop for Mcp {
|
||||||
@@ -63,7 +66,31 @@ impl Drop for Mcp {
|
|||||||
|
|
||||||
static DIR_SEQ: AtomicU64 = AtomicU64::new(0);
|
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 n = DIR_SEQ.fetch_add(1, Ordering::Relaxed);
|
||||||
let dir = std::env::temp_dir().join(format!(
|
let dir = std::env::temp_dir().join(format!(
|
||||||
"ledger-mcp-spec-attachments-{}-{name}-{n}",
|
"ledger-mcp-spec-attachments-{}-{name}-{n}",
|
||||||
@@ -71,7 +98,7 @@ fn test_dir(name: &str) -> PathBuf {
|
|||||||
));
|
));
|
||||||
let _ = std::fs::remove_dir_all(&dir);
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
std::fs::create_dir_all(&dir).unwrap();
|
std::fs::create_dir_all(&dir).unwrap();
|
||||||
dir
|
TestDir(dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A directory beside the books dir `dir`, outside it: the vault key,
|
/// A directory beside the books dir `dir`, outside it: the vault key,
|
||||||
@@ -131,6 +158,7 @@ impl Mcp {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
let mut mcp = Self {
|
let mut mcp = Self {
|
||||||
|
books_dir: None,
|
||||||
stdin,
|
stdin,
|
||||||
rx,
|
rx,
|
||||||
child,
|
child,
|
||||||
@@ -363,6 +391,7 @@ fn books_at(name: &str, threshold: &str, extra: &str) -> Mcp {
|
|||||||
let toml = books_toml(&dir, threshold, extra);
|
let toml = books_toml(&dir, threshold, extra);
|
||||||
let mut m = Mcp::start(&dir, &toml);
|
let mut m = Mcp::start(&dir, &toml);
|
||||||
m.accounts(CHART);
|
m.accounts(CHART);
|
||||||
|
m.books_dir = Some(dir);
|
||||||
m
|
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 dir = std::env::temp_dir().join(format!("ledger-mcp-test-{}-{name}", std::process::id()));
|
||||||
let _ = std::fs::remove_dir_all(&dir);
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
std::fs::create_dir_all(&dir).unwrap();
|
std::fs::create_dir_all(&dir).unwrap();
|
||||||
dir
|
TestDir(dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn init(m: &mut Mcp) -> String {
|
fn init(m: &mut Mcp) -> String {
|
||||||
|
|||||||
@@ -159,6 +159,9 @@ struct Mcp {
|
|||||||
read_only_checked: BTreeSet<String>,
|
read_only_checked: BTreeSet<String>,
|
||||||
/// Invariant 3 (amounts out) violations: `tool: path = value`.
|
/// Invariant 3 (amounts out) violations: `tool: path = value`.
|
||||||
money_violations: Vec<String>,
|
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 {
|
impl Drop for Mcp {
|
||||||
@@ -170,14 +173,42 @@ impl Drop for Mcp {
|
|||||||
|
|
||||||
static DIR_SEQ: AtomicU64 = AtomicU64::new(0);
|
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.
|
/// 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 n = DIR_SEQ.fetch_add(1, Ordering::Relaxed);
|
||||||
let dir =
|
let dir =
|
||||||
std::env::temp_dir().join(format!("ledger-mcp-spec-{}-{name}-{n}", std::process::id()));
|
std::env::temp_dir().join(format!("ledger-mcp-spec-{}-{name}-{n}", std::process::id()));
|
||||||
let _ = std::fs::remove_dir_all(&dir);
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
std::fs::create_dir_all(&dir).unwrap();
|
std::fs::create_dir_all(&dir).unwrap();
|
||||||
dir
|
TestDir(dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A directory beside the books dir `dir`, outside it (vault keys, files
|
/// A directory beside the books dir `dir`, outside it (vault keys, files
|
||||||
@@ -262,6 +293,7 @@ impl Mcp {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
let mut mcp = Self {
|
let mut mcp = Self {
|
||||||
|
books_dir: None,
|
||||||
stdin,
|
stdin,
|
||||||
rx,
|
rx,
|
||||||
child,
|
child,
|
||||||
@@ -1247,6 +1279,7 @@ fn happy_path(name: &str) -> Mcp {
|
|||||||
"save_to": outside(&dir).join("cpa.zip").to_str().unwrap()}),
|
"save_to": outside(&dir).join("cpa.zip").to_str().unwrap()}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
m.books_dir = Some(dir);
|
||||||
m
|
m
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1692,7 +1725,7 @@ const REFUSED_AMOUNTS: &[&str] = &[
|
|||||||
/// the core is expected to accept on a line.
|
/// the core is expected to accept on a line.
|
||||||
const ACCEPTED_AMOUNTS: &[&str] = &["5", "5.00", "123.45", "5.5", "007.50", "1000000.00"];
|
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 dir = test_dir(name);
|
||||||
let toml = books_toml(&dir, "25.00", "");
|
let toml = books_toml(&dir, "25.00", "");
|
||||||
let mut m = Mcp::start(&dir, &toml);
|
let mut m = Mcp::start(&dir, &toml);
|
||||||
|
|||||||
@@ -50,6 +50,9 @@ struct Mcp {
|
|||||||
next_id: u64,
|
next_id: u64,
|
||||||
data: PathBuf,
|
data: PathBuf,
|
||||||
validators: HashMap<String, jsonschema::Validator>,
|
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 {
|
impl Drop for Mcp {
|
||||||
@@ -61,7 +64,29 @@ impl Drop for Mcp {
|
|||||||
|
|
||||||
static DIR_SEQ: AtomicU64 = AtomicU64::new(0);
|
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 n = DIR_SEQ.fetch_add(1, Ordering::Relaxed);
|
||||||
let dir = std::env::temp_dir().join(format!(
|
let dir = std::env::temp_dir().join(format!(
|
||||||
"ledger-mcp-spec-parties-{}-{name}-{n}",
|
"ledger-mcp-spec-parties-{}-{name}-{n}",
|
||||||
@@ -69,7 +94,7 @@ fn test_dir(name: &str) -> PathBuf {
|
|||||||
));
|
));
|
||||||
let _ = std::fs::remove_dir_all(&dir);
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
std::fs::create_dir_all(&dir).unwrap();
|
std::fs::create_dir_all(&dir).unwrap();
|
||||||
dir
|
TestDir(dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `wanderbooks.toml` with a company name and extra `[books]` TOML. The
|
/// `wanderbooks.toml` with a company name and extra `[books]` TOML. The
|
||||||
@@ -114,6 +139,7 @@ impl Mcp {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
let mut mcp = Self {
|
let mut mcp = Self {
|
||||||
|
books_dir: None,
|
||||||
stdin,
|
stdin,
|
||||||
rx,
|
rx,
|
||||||
child,
|
child,
|
||||||
@@ -339,6 +365,7 @@ fn books(name: &str) -> Mcp {
|
|||||||
let toml = books_toml(&dir, "Spec Co", "");
|
let toml = books_toml(&dir, "Spec Co", "");
|
||||||
let mut m = Mcp::start(&dir, &toml);
|
let mut m = Mcp::start(&dir, &toml);
|
||||||
m.accounts(CHART);
|
m.accounts(CHART);
|
||||||
|
m.books_dir = Some(dir);
|
||||||
m
|
m
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1161,6 +1188,7 @@ fn books_with(name: &str, extra: &str) -> Mcp {
|
|||||||
let toml = books_toml(&dir, "Spec Co", extra);
|
let toml = books_toml(&dir, "Spec Co", extra);
|
||||||
let mut m = Mcp::start(&dir, &toml);
|
let mut m = Mcp::start(&dir, &toml);
|
||||||
m.accounts(CHART);
|
m.accounts(CHART);
|
||||||
|
m.books_dir = Some(dir);
|
||||||
m
|
m
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1343,6 +1371,7 @@ fn member_books(name: &str, extra: &str) -> Mcp {
|
|||||||
m.accounts(&[("3001", "Capital K", "equity")]);
|
m.accounts(&[("3001", "Capital K", "equity")]);
|
||||||
m.add_party("Stephen Waits", &["member"]);
|
m.add_party("Stephen Waits", &["member"]);
|
||||||
m.add_party("Katherine Pendergast", &["member"]);
|
m.add_party("Katherine Pendergast", &["member"]);
|
||||||
|
m.books_dir = Some(dir);
|
||||||
m
|
m
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -58,6 +58,9 @@ struct Mcp {
|
|||||||
validators: HashMap<String, jsonschema::Validator>,
|
validators: HashMap<String, jsonschema::Validator>,
|
||||||
/// Every response line the server wrote, with the tool it answered.
|
/// Every response line the server wrote, with the tool it answered.
|
||||||
transcript: Vec<(String, String)>,
|
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 {
|
impl Drop for Mcp {
|
||||||
@@ -69,7 +72,35 @@ impl Drop for Mcp {
|
|||||||
|
|
||||||
static DIR_SEQ: AtomicU64 = AtomicU64::new(0);
|
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 n = DIR_SEQ.fetch_add(1, Ordering::Relaxed);
|
||||||
let dir = std::env::temp_dir().join(format!(
|
let dir = std::env::temp_dir().join(format!(
|
||||||
"ledger-mcp-spec-vault-{}-{name}-{n}",
|
"ledger-mcp-spec-vault-{}-{name}-{n}",
|
||||||
@@ -77,7 +108,7 @@ fn test_dir(name: &str) -> PathBuf {
|
|||||||
));
|
));
|
||||||
let _ = std::fs::remove_dir_all(&dir);
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
std::fs::create_dir_all(&dir).unwrap();
|
std::fs::create_dir_all(&dir).unwrap();
|
||||||
dir
|
TestDir(dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A directory beside the books dir `dir`, outside it: the vault key,
|
/// A directory beside the books dir `dir`, outside it: the vault key,
|
||||||
@@ -164,6 +195,7 @@ impl Mcp {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
let mut mcp = Self {
|
let mut mcp = Self {
|
||||||
|
books_dir: None,
|
||||||
stdin,
|
stdin,
|
||||||
rx,
|
rx,
|
||||||
child,
|
child,
|
||||||
@@ -402,6 +434,7 @@ fn books_with(name: &str, extra: &str) -> Mcp {
|
|||||||
json!({"vendor": "Acme Design", "form_1099": "nec"}),
|
json!({"vendor": "Acme Design", "form_1099": "nec"}),
|
||||||
);
|
);
|
||||||
m.add_party("Rent Co", &["landlord"]);
|
m.add_party("Rent Co", &["landlord"]);
|
||||||
|
m.books_dir = Some(dir);
|
||||||
m
|
m
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,11 +7,39 @@ use std::time::{Duration, Instant};
|
|||||||
|
|
||||||
const BIN: &str = env!("CARGO_BIN_EXE_ledger-mcp");
|
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 d = std::env::temp_dir().join(format!("ledger-mcp-start-{}-{tag}", std::process::id()));
|
||||||
let _ = std::fs::remove_dir_all(&d);
|
let _ = std::fs::remove_dir_all(&d);
|
||||||
std::fs::create_dir_all(&d).unwrap();
|
std::fs::create_dir_all(&d).unwrap();
|
||||||
d
|
TestDir(d)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Start the server, initialize, call one tool, and return its text
|
/// 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.
|
/// used.
|
||||||
#[test]
|
#[test]
|
||||||
fn an_import_root_holding_the_books_or_the_home_folder_is_refused() {
|
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 home = dir.join("home");
|
||||||
let books = home.join("work/books");
|
let books = home.join("work/books");
|
||||||
let data = books.join("books-data");
|
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.
|
/// is the root, and the rest of the home folder stays out of reach.
|
||||||
#[test]
|
#[test]
|
||||||
fn a_download_dir_of_home_never_opens_the_home_folder() {
|
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");
|
let home = dir.join("home");
|
||||||
std::fs::create_dir_all(home.join(".ssh")).unwrap();
|
std::fs::create_dir_all(home.join(".ssh")).unwrap();
|
||||||
std::fs::write(home.join(".ssh/id_ed25519"), "PRIVATE KEY").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.
|
/// folder, and an import says so.
|
||||||
#[test]
|
#[test]
|
||||||
fn a_default_download_dir_holding_the_books_is_dropped_not_fatal() {
|
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 home = dir.join("home");
|
||||||
let downloads = dir.join("downloads");
|
let downloads = dir.join("downloads");
|
||||||
std::fs::create_dir_all(&home).unwrap();
|
std::fs::create_dir_all(&home).unwrap();
|
||||||
|
|||||||
@@ -68,6 +68,16 @@ struct Books {
|
|||||||
toml: PathBuf,
|
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 {
|
fn books(tag: &str) -> Books {
|
||||||
let base =
|
let base =
|
||||||
std::env::temp_dir().join(format!("ledger-mcp-tin-files-{}-{tag}", std::process::id()));
|
std::env::temp_dir().join(format!("ledger-mcp-tin-files-{}-{tag}", std::process::id()));
|
||||||
|
|||||||
@@ -139,10 +139,20 @@ pub fn read(data_dir: &Path) -> Option<ViewerCommand> {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn requests_round_trip_and_count_up() {
|
fn requests_round_trip_and_count_up() {
|
||||||
let dir = std::env::temp_dir().join(format!("wb-viewer-{}", std::process::id()));
|
let dir = std::env::temp_dir().join(format!("wb-viewer-{}", std::process::id()));
|
||||||
std::fs::create_dir_all(&dir).unwrap();
|
std::fs::create_dir_all(&dir).unwrap();
|
||||||
|
let _cleanup = TestDir(dir.clone());
|
||||||
let a = request(&dir, ViewerTarget::Entry { entry_id: 7 }).unwrap();
|
let a = request(&dir, ViewerTarget::Entry { entry_id: 7 }).unwrap();
|
||||||
let b = request(
|
let b = request(
|
||||||
&dir,
|
&dir,
|
||||||
|
|||||||
@@ -18,11 +18,33 @@ use ledger_store::{DB_FILE, JOURNAL_FILE, JournalWriter, Store, StoreError};
|
|||||||
use serde_json::{Value, json};
|
use serde_json::{Value, json};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
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 dir = std::env::temp_dir().join(format!("wb-asset-tags-{name}-{}", std::process::id()));
|
let dir = std::env::temp_dir().join(format!("wb-asset-tags-{name}-{}", std::process::id()));
|
||||||
let _ = std::fs::remove_dir_all(&dir);
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
std::fs::create_dir_all(&dir).unwrap();
|
std::fs::create_dir_all(&dir).unwrap();
|
||||||
dir
|
TestDir(dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn usd(amount: &str) -> Money {
|
fn usd(amount: &str) -> Money {
|
||||||
@@ -78,7 +100,7 @@ fn laptop(store: &mut Store, a: &Accts, name: &str) -> AssetId {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// A store with two untagged laptops.
|
/// A store with two untagged laptops.
|
||||||
fn fixture(name: &str) -> (PathBuf, Store, Accts, AssetId, AssetId) {
|
fn fixture(name: &str) -> (TestDir, Store, Accts, AssetId, AssetId) {
|
||||||
let dir = test_dir(name);
|
let dir = test_dir(name);
|
||||||
let mut s = Store::create(&dir, Currency::USD).unwrap();
|
let mut s = Store::create(&dir, Currency::USD).unwrap();
|
||||||
let a = accounts(&mut s);
|
let a = accounts(&mut s);
|
||||||
|
|||||||
@@ -25,11 +25,33 @@ use std::str::FromStr;
|
|||||||
|
|
||||||
// ---------------------------------------------------------------- helpers
|
// ---------------------------------------------------------------- helpers
|
||||||
|
|
||||||
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 dir = std::env::temp_dir().join(format!("wb-attach-{name}-{}", std::process::id()));
|
let dir = std::env::temp_dir().join(format!("wb-attach-{name}-{}", std::process::id()));
|
||||||
let _ = std::fs::remove_dir_all(&dir);
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
std::fs::create_dir_all(&dir).unwrap();
|
std::fs::create_dir_all(&dir).unwrap();
|
||||||
dir
|
TestDir(dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dec(s: &str) -> Decimal {
|
fn dec(s: &str) -> Decimal {
|
||||||
|
|||||||
@@ -21,14 +21,36 @@ use rust_decimal::Decimal;
|
|||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
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 dir = std::env::temp_dir().join(format!(
|
let dir = std::env::temp_dir().join(format!(
|
||||||
"wanderbooks-store-bounds-{name}-{}",
|
"wanderbooks-store-bounds-{name}-{}",
|
||||||
std::process::id()
|
std::process::id()
|
||||||
));
|
));
|
||||||
let _ = std::fs::remove_dir_all(&dir);
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
std::fs::create_dir_all(&dir).unwrap();
|
std::fs::create_dir_all(&dir).unwrap();
|
||||||
dir
|
TestDir(dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn d(y: i32, m: u32, day: u32) -> NaiveDate {
|
fn d(y: i32, m: u32, day: u32) -> NaiveDate {
|
||||||
@@ -36,8 +58,8 @@ fn d(y: i32, m: u32, day: u32) -> NaiveDate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The old-books fixture `name` (tests/fixtures/old-books: written by main
|
/// The old-books fixture `name` (tests/fixtures/old-books: written by main
|
||||||
/// 188c90ea), opened from a copy.
|
/// 188c90ea), opened from a copy (kept while the guard lives).
|
||||||
fn open_old_books(name: &str) -> Store {
|
fn open_old_books(name: &str) -> (TestDir, Store) {
|
||||||
let from = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
let from = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||||
.join("tests/fixtures/old-books")
|
.join("tests/fixtures/old-books")
|
||||||
.join(name);
|
.join(name);
|
||||||
@@ -55,7 +77,8 @@ fn open_old_books(name: &str) -> Store {
|
|||||||
std::fs::copy(&e, to).unwrap();
|
std::fs::copy(&e, to).unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Store::open(&dir).unwrap_or_else(|e| panic!("{name}: {e}"))
|
let store = Store::open(&dir).unwrap_or_else(|e| panic!("{name}: {e}"));
|
||||||
|
(dir, store)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Every file under `dir`.
|
/// Every file under `dir`.
|
||||||
@@ -114,7 +137,7 @@ fn a_statement_the_books_cant_hold_is_refused() {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
// A statement an earlier version recorded past both bounds opens, its
|
// A statement an earlier version recorded past both bounds opens, its
|
||||||
// balances as recorded.
|
// balances as recorded.
|
||||||
let old = open_old_books("x_statement_max");
|
let (_dir, old) = open_old_books("x_statement_max");
|
||||||
let recorded = old.statements(None).unwrap();
|
let recorded = old.statements(None).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
recorded[0].closing_balance,
|
recorded[0].closing_balance,
|
||||||
@@ -158,7 +181,7 @@ fn a_recurring_run_has_an_end() {
|
|||||||
assert!(s.run_recurring(last).unwrap().is_empty());
|
assert!(s.run_recurring(last).unwrap().is_empty());
|
||||||
// One run an earlier version made of more than that opens, every
|
// One run an earlier version made of more than that opens, every
|
||||||
// draft as generated.
|
// draft as generated.
|
||||||
let old = open_old_books("x_recurring_run_1044");
|
let (_dir, old) = open_old_books("x_recurring_run_1044");
|
||||||
assert_eq!(old.ledger().next_entry_id(), 1045);
|
assert_eq!(old.ledger().next_entry_id(), 1045);
|
||||||
old.verify().unwrap();
|
old.verify().unwrap();
|
||||||
}
|
}
|
||||||
@@ -206,7 +229,7 @@ fn disposal_proceeds_larger_than_a_line_are_refused() {
|
|||||||
assert!(s.run_depreciation(id, period).is_err(), "{period}");
|
assert!(s.run_depreciation(id, period).is_err(), "{period}");
|
||||||
}
|
}
|
||||||
// Depreciation an earlier version posted for 10000-01 opens.
|
// Depreciation an earlier version posted for 10000-01 opens.
|
||||||
let old = open_old_books("x_depreciation_year10000");
|
let (_dir, old) = open_old_books("x_depreciation_year10000");
|
||||||
assert_eq!(old.ledger().get_entry(2).unwrap().date(), d(10000, 1, 31));
|
assert_eq!(old.ledger().get_entry(2).unwrap().date(), d(10000, 1, 31));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,11 +19,33 @@ use rust_decimal::Decimal;
|
|||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use std::sync::{Arc, Barrier};
|
use std::sync::{Arc, Barrier};
|
||||||
|
|
||||||
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!("wanderbooks-conc-{name}-{}", std::process::id()));
|
let dir = std::env::temp_dir().join(format!("wanderbooks-conc-{name}-{}", std::process::id()));
|
||||||
let _ = std::fs::remove_dir_all(&dir);
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
std::fs::create_dir_all(&dir).unwrap();
|
std::fs::create_dir_all(&dir).unwrap();
|
||||||
dir
|
TestDir(dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
const ENTRIES: u32 = 400;
|
const ENTRIES: u32 = 400;
|
||||||
|
|||||||
@@ -15,11 +15,33 @@ use rust_decimal::Decimal;
|
|||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
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 dir = std::env::temp_dir().join(format!("wanderbooks-fault-{name}-{}", std::process::id()));
|
let dir = std::env::temp_dir().join(format!("wanderbooks-fault-{name}-{}", std::process::id()));
|
||||||
let _ = std::fs::remove_dir_all(&dir);
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
std::fs::create_dir_all(&dir).unwrap();
|
std::fs::create_dir_all(&dir).unwrap();
|
||||||
dir
|
TestDir(dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn post_one(store: &mut Store, n: u32) -> Result<(), ledger_store::StoreError> {
|
fn post_one(store: &mut Store, n: u32) -> Result<(), ledger_store::StoreError> {
|
||||||
@@ -104,7 +126,7 @@ fn full_disk_case(name: &str, drafts: u32) -> String {
|
|||||||
"trap '' XFSZ; ulimit -f {blocks}; exec \"$0\" --exact child_append_until_the_disk_is_full --nocapture --test-threads=1"
|
"trap '' XFSZ; ulimit -f {blocks}; exec \"$0\" --exact child_append_until_the_disk_is_full --nocapture --test-threads=1"
|
||||||
))
|
))
|
||||||
.arg(&exe)
|
.arg(&exe)
|
||||||
.env("WB_FAULT_DIR", &dir)
|
.env("WB_FAULT_DIR", dir.as_os_str())
|
||||||
.output()
|
.output()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let stdout = String::from_utf8_lossy(&out.stdout);
|
let stdout = String::from_utf8_lossy(&out.stdout);
|
||||||
|
|||||||
@@ -27,12 +27,34 @@ use rust_decimal::Decimal;
|
|||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
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 dir =
|
let dir =
|
||||||
std::env::temp_dir().join(format!("wanderbooks-format-{name}-{}", std::process::id()));
|
std::env::temp_dir().join(format!("wanderbooks-format-{name}-{}", std::process::id()));
|
||||||
let _ = std::fs::remove_dir_all(&dir);
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
std::fs::create_dir_all(&dir).unwrap();
|
std::fs::create_dir_all(&dir).unwrap();
|
||||||
dir
|
TestDir(dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Books with one posted entry, closed again.
|
/// Books with one posted entry, closed again.
|
||||||
|
|||||||
@@ -24,11 +24,33 @@ use std::str::FromStr;
|
|||||||
|
|
||||||
// ---------------------------------------------------------------- helpers
|
// ---------------------------------------------------------------- helpers
|
||||||
|
|
||||||
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 dir = std::env::temp_dir().join(format!("wb-parties-{name}-{}", std::process::id()));
|
let dir = std::env::temp_dir().join(format!("wb-parties-{name}-{}", std::process::id()));
|
||||||
let _ = std::fs::remove_dir_all(&dir);
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
std::fs::create_dir_all(&dir).unwrap();
|
std::fs::create_dir_all(&dir).unwrap();
|
||||||
dir
|
TestDir(dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dec(s: &str) -> Decimal {
|
fn dec(s: &str) -> Decimal {
|
||||||
|
|||||||
@@ -12,11 +12,33 @@ use ledger_store::{DB_FILE, JOURNAL_FILE, JournalWriter, Store};
|
|||||||
use serde_json::{Value, json};
|
use serde_json::{Value, json};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
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 dir = std::env::temp_dir().join(format!("wb-party-tin-{name}-{}", std::process::id()));
|
let dir = std::env::temp_dir().join(format!("wb-party-tin-{name}-{}", std::process::id()));
|
||||||
let _ = std::fs::remove_dir_all(&dir);
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
std::fs::create_dir_all(&dir).unwrap();
|
std::fs::create_dir_all(&dir).unwrap();
|
||||||
dir
|
TestDir(dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn journal(dir: &Path) -> Vec<(String, Value)> {
|
fn journal(dir: &Path) -> Vec<(String, Value)> {
|
||||||
|
|||||||
@@ -18,11 +18,33 @@ use serde_json::{Value, json};
|
|||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
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 dir = std::env::temp_dir().join(format!("wb-roles-{name}-{}", std::process::id()));
|
let dir = std::env::temp_dir().join(format!("wb-roles-{name}-{}", std::process::id()));
|
||||||
let _ = std::fs::remove_dir_all(&dir);
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
std::fs::create_dir_all(&dir).unwrap();
|
std::fs::create_dir_all(&dir).unwrap();
|
||||||
dir
|
TestDir(dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dec(s: &str) -> Decimal {
|
fn dec(s: &str) -> Decimal {
|
||||||
|
|||||||
@@ -17,14 +17,36 @@ use rust_decimal::Decimal;
|
|||||||
use serde_json::{Value, json};
|
use serde_json::{Value, json};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
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 dir = std::env::temp_dir().join(format!(
|
let dir = std::env::temp_dir().join(format!(
|
||||||
"wanderbooks-store-settings-{name}-{}",
|
"wanderbooks-store-settings-{name}-{}",
|
||||||
std::process::id()
|
std::process::id()
|
||||||
));
|
));
|
||||||
let _ = std::fs::remove_dir_all(&dir);
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
std::fs::create_dir_all(&dir).unwrap();
|
std::fs::create_dir_all(&dir).unwrap();
|
||||||
dir
|
TestDir(dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn d(y: i32, m: u32, day: u32) -> NaiveDate {
|
fn d(y: i32, m: u32, day: u32) -> NaiveDate {
|
||||||
@@ -48,7 +70,7 @@ fn set(k: &str, v: Value) -> Change {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Books with a small chart.
|
/// Books with a small chart.
|
||||||
fn books(name: &str) -> (PathBuf, Store) {
|
fn books(name: &str) -> (TestDir, Store) {
|
||||||
let dir = test_dir(name);
|
let dir = test_dir(name);
|
||||||
let mut s = Store::create(&dir, Currency::USD).unwrap();
|
let mut s = Store::create(&dir, Currency::USD).unwrap();
|
||||||
for (code, name, t) in [
|
for (code, name, t) in [
|
||||||
|
|||||||
@@ -8,11 +8,33 @@ use rust_decimal::Decimal;
|
|||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
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 dir = std::env::temp_dir().join(format!("wanderbooks-snap-{name}-{}", std::process::id()));
|
let dir = std::env::temp_dir().join(format!("wanderbooks-snap-{name}-{}", std::process::id()));
|
||||||
let _ = std::fs::remove_dir_all(&dir);
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
std::fs::create_dir_all(&dir).unwrap();
|
std::fs::create_dir_all(&dir).unwrap();
|
||||||
dir
|
TestDir(dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn usd(n: i64) -> Money {
|
fn usd(n: i64) -> Money {
|
||||||
|
|||||||
@@ -21,11 +21,33 @@ use std::path::{Path, PathBuf};
|
|||||||
|
|
||||||
const GENESIS: &str = "0000000000000000000000000000000000000000000000000000000000000000";
|
const GENESIS: &str = "0000000000000000000000000000000000000000000000000000000000000000";
|
||||||
|
|
||||||
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 dir = std::env::temp_dir().join(format!("wb-spec-store-{name}-{}", std::process::id()));
|
let dir = std::env::temp_dir().join(format!("wb-spec-store-{name}-{}", std::process::id()));
|
||||||
let _ = std::fs::remove_dir_all(&dir);
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
std::fs::create_dir_all(&dir).unwrap();
|
std::fs::create_dir_all(&dir).unwrap();
|
||||||
dir
|
TestDir(dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn usd(amount: u64) -> Money {
|
fn usd(amount: u64) -> Money {
|
||||||
|
|||||||
@@ -13,11 +13,33 @@ fn usd(amount: u64) -> Money {
|
|||||||
Money::new(Decimal::from(amount), Currency::USD)
|
Money::new(Decimal::from(amount), Currency::USD)
|
||||||
}
|
}
|
||||||
|
|
||||||
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 dir = std::env::temp_dir().join(format!("wanderbooks-{name}-{}", std::process::id()));
|
let dir = std::env::temp_dir().join(format!("wanderbooks-{name}-{}", std::process::id()));
|
||||||
let _ = std::fs::remove_dir_all(&dir);
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
std::fs::create_dir_all(&dir).unwrap();
|
std::fs::create_dir_all(&dir).unwrap();
|
||||||
dir
|
TestDir(dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_books(dir: &std::path::Path) -> Store {
|
fn build_books(dir: &std::path::Path) -> Store {
|
||||||
@@ -1619,7 +1641,7 @@ fn old_books() -> Vec<PathBuf> {
|
|||||||
/// A fixture's books (journal and blobs, never expected.json) in a fresh
|
/// A fixture's books (journal and blobs, never expected.json) in a fresh
|
||||||
/// directory of its own for `test`: opening writes a database and a lock
|
/// directory of its own for `test`: opening writes a database and a lock
|
||||||
/// beside them.
|
/// beside them.
|
||||||
fn old_books_copy(fixture: &std::path::Path, test: &str) -> PathBuf {
|
fn old_books_copy(fixture: &std::path::Path, test: &str) -> TestDir {
|
||||||
fn copy(from: &std::path::Path, to: &std::path::Path) {
|
fn copy(from: &std::path::Path, to: &std::path::Path) {
|
||||||
std::fs::create_dir_all(to).unwrap();
|
std::fs::create_dir_all(to).unwrap();
|
||||||
for e in std::fs::read_dir(from).unwrap() {
|
for e in std::fs::read_dir(from).unwrap() {
|
||||||
|
|||||||
@@ -3,11 +3,27 @@
|
|||||||
use ledger_vault::{Error, Tin, TinType, Vault, W9File};
|
use ledger_vault::{Error, Tin, TinType, Vault, W9File};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
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!("wb-vault-{tag}-{}", std::process::id()));
|
let d = std::env::temp_dir().join(format!("wb-vault-{tag}-{}", std::process::id()));
|
||||||
let _ = std::fs::remove_dir_all(&d);
|
let _ = std::fs::remove_dir_all(&d);
|
||||||
std::fs::create_dir_all(&d).unwrap();
|
std::fs::create_dir_all(&d).unwrap();
|
||||||
d
|
TestDir(d)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn vault(d: &std::path::Path) -> Vault {
|
fn vault(d: &std::path::Path) -> Vault {
|
||||||
|
|||||||
@@ -17,10 +17,12 @@ check: sweep-test-tmp
|
|||||||
cargo test --workspace
|
cargo test --workspace
|
||||||
@just sweep-test-tmp
|
@just sweep-test-tmp
|
||||||
|
|
||||||
# Test books go in the system temp dir (often RAM). A test cleans up after
|
# Test books go in the system temp dir (often RAM). Every test's dir is
|
||||||
# itself, but one killed mid-run (a mutant's timeout, ^C) can't: tens of
|
# removed when the test ends, pass or panic (a guard dropped on unwind;
|
||||||
# thousands piled up to 23 GB of RAM (2026-10-15). Remove this user's test
|
# until 2026-10-17 many were removed only on pass, or never: 117,000 piled
|
||||||
# book dirs older than an hour, so a run in progress elsewhere keeps its own.
|
# up). A run killed mid-test (a mutant's timeout, ^C) runs no destructor,
|
||||||
|
# so this sweep removes this user's test book dirs older than an hour; a
|
||||||
|
# run in progress elsewhere keeps its own.
|
||||||
sweep-test-tmp:
|
sweep-test-tmp:
|
||||||
@find "${TMPDIR:-/tmp}" /tmp -maxdepth 1 -user "$(id -u)" -type d -mmin +60 \( -name 'wb-*' -o -name 'wanderbooks-*' -o -name 'ledger-*' \) -exec rm -rf {} + 2>/dev/null || true
|
@find "${TMPDIR:-/tmp}" /tmp -maxdepth 1 -user "$(id -u)" -type d -mmin +60 \( -name 'wb-*' -o -name 'wanderbooks-*' -o -name 'ledger-*' \) -exec rm -rf {} + 2>/dev/null || true
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user