use rustledger_loader::{LoadOptions, process};
fn codes(source: &str) -> Vec<String> {
let mut vfs = rustledger_loader::VirtualFileSystem::new();
vfs.add_file("/mem/ledger.bean", source);
let raw = rustledger_loader::Loader::new()
.with_filesystem(Box::new(vfs))
.load(std::path::Path::new("/mem/ledger.bean"))
.expect("in-memory ledger loads");
let mut out: Vec<String> = raw.errors.iter().map(|e| format!("PARSE:{e}")).collect();
match process(raw, &LoadOptions::default()) {
Ok(ledger) => out.extend(
ledger
.errors
.iter()
.map(|e| format!("{}:{}", e.code, e.message)),
),
Err(e) => out.push(format!("PROCESS:{e}")),
}
out
}
const OPENS: &str = "2014-01-01 open Assets:Invest:AAPL\n\
2014-01-01 open Assets:Invest:Cash\n";
#[test]
fn two_numberless_cost_specs_in_one_group_are_rejected() {
let got = codes(&format!(
"{OPENS}\
2014-01-01 *\n\
\x20 Assets:Invest:AAPL 20 AAPL {{USD}}\n\
\x20 Assets:Invest:AAPL 20 AAPL {{ # USD}}\n\
\x20 Assets:Invest:Cash 0 USD\n"
));
assert!(
got.iter().any(|c| c.contains("multiple postings missing")),
"expected the too-many-unknowns rejection, got {got:?}"
);
}
#[test]
fn a_single_numberless_cost_spec_still_interpolates() {
for spec in ["{USD}", "{ # USD}", "{}"] {
let got = codes(&format!(
"{OPENS}\
2014-01-01 *\n\
\x20 Assets:Invest:AAPL 20 AAPL {spec}\n\
\x20 Assets:Invest:Cash -2000.00 USD\n"
));
assert!(
got.is_empty(),
"a single unknown is solvable and must load clean: {spec} -> {got:?}"
);
}
}
#[test]
fn hash_with_no_numbers_is_not_a_zero_cost() {
let got = codes(&format!(
"{OPENS}\
2014-01-01 *\n\
\x20 Assets:Invest:AAPL 20 AAPL {{ # USD}}\n\
\x20 Assets:Invest:Cash -2000.00 USD\n"
));
assert!(
!got.iter().any(|c| c.starts_with("E3001")),
"a numberless `#` spec must be solved, not treated as zero cost: {got:?}"
);
}
#[test]
fn a_malformed_cost_spec_does_not_produce_a_balance_error() {
for spec in ["{, 100.0 USD, , }", "{45.23 USD / 2015-07-16 / \"blabla\"}"] {
let got = codes(&format!(
"{OPENS}\
2014-01-01 *\n\
\x20 Assets:Invest:AAPL 10 AAPL {spec}\n\
\x20 Assets:Invest:Cash -19.90 USD\n"
));
assert!(
!got.iter().any(|c| c.starts_with("E3001")),
"malformed spec must not draw an invented-number balance error: \
{spec} -> {got:?}"
);
assert!(
got.iter().any(|c| c.starts_with("PARSE:")),
"the real cause must still be reported: {spec} -> {got:?}"
);
}
}
#[test]
fn a_well_formed_spec_that_does_not_balance_is_still_reported() {
let got = codes(&format!(
"{OPENS}\
2014-01-01 *\n\
\x20 Assets:Invest:AAPL 10 AAPL {{100.00 USD}}\n\
\x20 Assets:Invest:Cash -19.90 USD\n"
));
assert!(
got.iter().any(|c| c.starts_with("E3001")),
"a real imbalance must still be reported: {got:?}"
);
}