use std::fs;
use std::io::Write;
fn shapes() -> Vec<(&'static str, &'static str)> {
vec![
(
"cost_arithmetic",
"2013-01-01 open Assets:S\n2013-01-01 open Assets:C\n\
2013-05-18 * \"t\"\n Assets:S 2 HOOL {10.00 * 3 USD}\n Assets:C -60.00 USD\n",
),
(
"negative_cost",
"2013-01-01 open Assets:M\n2013-01-01 open Assets:C\n\
2013-05-18 * \"t\"\n Assets:M -10 MSFT {-200.00 USD}\n Assets:C 2000.00 USD\n",
),
(
"compound_cost",
"2014-01-01 open Assets:A\n2014-01-01 open Assets:C\n\
2014-01-01 * \"t\"\n Assets:A 10 AAPL {# 9.95 USD}\n Assets:C -9.95 USD\n",
),
(
"metadata_arithmetic",
"2013-01-01 open Assets:A\n2013-01-01 open Assets:B\n\
2013-05-18 * \"t\"\n num: 2 * 3\n Assets:A 10.00 USD\n Assets:B -10.00 USD\n",
),
(
"balance_tolerance_arithmetic",
"2013-01-01 open Assets:A\n2013-01-01 open Assets:B\n\
2013-05-18 * \"t\"\n Assets:A 10.008 USD\n Assets:B -10.008 USD\n\
2014-01-01 balance Assets:A 10.00 ~ 0.005 * 2 USD\n",
),
(
"unicode_account",
"2018-01-01 open Assets:CORP\u{2728}\n2018-01-01 open Assets:C\n\
2018-06-01 * \"t\"\n Assets:CORP\u{2728} 1.00 USD\n Assets:C -1.00 USD\n",
),
(
"tags_and_links",
"2018-01-01 open Assets:A\n2018-01-01 open Assets:B\n\
2018-06-01 * \"t\" #tag ^link\n Assets:A 1.00 USD\n Assets:B -1.00 USD\n",
),
(
"lots_and_prices",
"2018-01-01 open Assets:B\n2018-01-01 open Assets:C\n2018-01-01 open Income:G\n\
2018-02-01 * \"buy\"\n Assets:B 10 CORP {12.00 USD}\n Assets:C -120.00 USD\n\
2018-06-01 * \"sell\"\n Assets:B -10 CORP {12.00 USD} @ 15.00 USD\n\
\x20 Assets:C 150.00 USD\n Income:G -30.00 USD\n",
),
]
}
fn assert_clean_cache_env() {
for var in [
rustledger_loader::CACHE_FILENAME_ENV,
rustledger_loader::DISABLE_CACHE_ENV,
] {
assert!(
std::env::var_os(var).is_none(),
"unset {var} before running this test - it redirects or disables \
the cache these properties are about",
);
}
}
fn fixture_dir() -> tempfile::TempDir {
tempfile::Builder::new()
.prefix("rledger_cache_props_")
.tempdir()
.expect("temp dir")
}
#[test]
fn cache_roundtrip_preserves_parsed_directives() {
assert_clean_cache_env();
for (name, source) in shapes() {
let dir = fixture_dir();
let file = dir.path().join("main.beancount");
let mut f = fs::File::create(&file).expect("create");
f.write_all(source.as_bytes()).expect("write");
drop(f);
let mut loader = rustledger_loader::Loader::new();
let loaded = loader.load(&file).expect("load");
assert!(
loaded.directives.len() >= 2,
"{name}: expected a multi-directive parse, got {} — the fixture is \
no longer exercising this shape",
loaded.directives.len(),
);
let entry = rustledger_loader::CacheEntry {
directives: loaded.directives.clone(),
options: rustledger_loader::CachedOptions::from(&loaded.options),
plugins: Vec::new(),
files: vec![file.to_string_lossy().into_owned()],
};
rustledger_loader::save_cache_entry(&file, &entry).expect("save");
let back = rustledger_loader::load_cache_entry(&file).expect("load cache");
assert_eq!(
format!("{:?}", back.directives),
format!("{:?}", entry.directives),
"{name}: directives did not survive the cache round-trip",
);
rustledger_loader::invalidate_cache(&file);
}
}
#[test]
fn a_stale_version_is_refused_rather_than_reinterpreted() {
assert_clean_cache_env();
let dir = fixture_dir();
let file = dir.path().join("main.beancount");
let mut f = fs::File::create(&file).expect("create");
f.write_all(b"2018-01-01 open Assets:A\n").expect("write");
drop(f);
let mut loader = rustledger_loader::Loader::new();
let loaded = loader.load(&file).expect("load");
let entry = rustledger_loader::CacheEntry {
directives: loaded.directives.clone(),
options: rustledger_loader::CachedOptions::from(&loaded.options),
plugins: Vec::new(),
files: vec![file.to_string_lossy().into_owned()],
};
rustledger_loader::save_cache_entry(&file, &entry).expect("save");
let path = rustledger_loader::cache_path(&file);
let mut bytes = fs::read(&path).expect("read cache");
let stale = rustledger_loader::cache::CACHE_VERSION
.checked_sub(1)
.expect("CACHE_VERSION >= 1");
bytes[8..12].copy_from_slice(&stale.to_le_bytes());
fs::write(&path, &bytes).expect("write cache");
assert!(
rustledger_loader::load_cache_entry(&file).is_none(),
"a cache blob one version behind must be refused, not reinterpreted",
);
rustledger_loader::invalidate_cache(&file);
}