#![cfg(unix)]
use rustledger_loader::{LoadOptions, load};
use std::os::unix::fs::symlink;
use std::path::Path;
fn fixture(opens: &[&str]) -> tempfile::TempDir {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("docs")).unwrap();
let mut open_lines = String::new();
for account in opens {
open_lines.push_str("2020-01-01 open ");
open_lines.push_str(account);
open_lines.push('\n');
}
std::fs::write(
dir.path().join("ledger.bean"),
format!("option \"documents\" \"docs\"\n\n{open_lines}"),
)
.unwrap();
dir
}
fn touch(path: &Path) {
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(path, "dummy").unwrap();
}
fn discovered(dir: &tempfile::TempDir) -> Vec<(String, String)> {
let ledger =
load(&dir.path().join("ledger.bean"), &LoadOptions::default()).expect("ledger loads");
let mut found: Vec<(String, String)> = ledger
.directives
.iter()
.filter_map(|d| match &d.value {
rustledger_core::Directive::Document(doc) => {
Some((doc.account.as_ref().to_string(), doc.path.clone()))
}
_ => None,
})
.collect();
found.sort();
found
}
fn warnings(dir: &tempfile::TempDir) -> Vec<String> {
let ledger =
load(&dir.path().join("ledger.bean"), &LoadOptions::default()).expect("ledger loads");
ledger.errors.iter().map(|e| e.message.clone()).collect()
}
#[test]
fn symlinked_subtree_is_discovered() {
let dir = fixture(&["Expenses:Direct", "Expenses:Bar"]);
touch(
&dir.path()
.join("docs/Expenses/Direct/2026-07-07 control.pdf"),
);
touch(&dir.path().join("library/2026-07-07 behind.pdf"));
symlink(
dir.path().join("library"),
dir.path().join("docs/Expenses/Bar"),
)
.unwrap();
let found = discovered(&dir);
assert_eq!(
found.len(),
2,
"the linked subtree must be walked, not skipped: {found:?}"
);
assert!(
found.iter().any(|(account, _)| account == "Expenses:Bar"),
"expected a document under Expenses:Bar: {found:?}"
);
}
#[test]
fn a_walked_subtree_reports_its_unopened_accounts() {
let dir = fixture(&["Expenses:Direct"]);
touch(
&dir.path()
.join("docs/Expenses/Direct/2026-07-07 control.pdf"),
);
touch(&dir.path().join("library/2026-07-07 other.pdf"));
symlink(
dir.path().join("library"),
dir.path().join("docs/Expenses/Nope"),
)
.unwrap();
assert!(
warnings(&dir)
.iter()
.any(|w| w.contains("Expenses:Nope") && w.contains("unknown account")),
"walking the link should surface its unopened account, got: {:?}",
warnings(&dir)
);
}
#[test]
fn two_links_to_the_same_target_are_both_discovered() {
let dir = fixture(&["Expenses:Alpha", "Expenses:Beta"]);
std::fs::create_dir_all(dir.path().join("docs/Expenses")).unwrap();
touch(&dir.path().join("shared/2026-07-07 shared.pdf"));
symlink(
dir.path().join("shared"),
dir.path().join("docs/Expenses/Alpha"),
)
.unwrap();
symlink(
dir.path().join("shared"),
dir.path().join("docs/Expenses/Beta"),
)
.unwrap();
let accounts: Vec<String> = discovered(&dir).into_iter().map(|(a, _)| a).collect();
assert_eq!(
accounts,
vec!["Expenses:Alpha".to_string(), "Expenses:Beta".to_string()],
"the same folder reached by two links belongs to both accounts"
);
}
#[test]
fn symlink_cycle_terminates_and_warns() {
let dir = fixture(&["Expenses:Deep"]);
touch(&dir.path().join("docs/Expenses/Deep/2026-07-07 real.pdf"));
symlink(
dir.path().join("docs"),
dir.path().join("docs/Expenses/Loop"),
)
.unwrap();
symlink(
dir.path().join("docs/Expenses"),
dir.path().join("docs/Expenses/Self"),
)
.unwrap();
let found = discovered(&dir);
assert_eq!(
found.len(),
1,
"the real document, discovered exactly once: {found:?}"
);
let warns = warnings(&dir);
assert!(
warns
.iter()
.any(|w| w.contains("links back into a directory")),
"a skipped cycle must not be silent, got: {warns:?}"
);
}
#[test]
fn dangling_symlink_is_skipped() {
let dir = fixture(&["Expenses:Direct"]);
touch(
&dir.path()
.join("docs/Expenses/Direct/2026-07-07 control.pdf"),
);
symlink(
dir.path().join("nowhere"),
dir.path().join("docs/Expenses/Broken"),
)
.unwrap();
assert_eq!(discovered(&dir).len(), 1);
}