mod common;
use std::process::Command;
const SRC: &str = r#"option "operating_currency" "USD"
2024-01-01 open Assets:Bank USD
2024-01-01 open Expenses:Food USD
2024-01-15 * "Coffee"
Expenses:Food 5.00 USD
Assets:Bank
2024-01-20 * "Lunch"
Expenses:Food 12.50 USD
Assets:Bank
"#;
const QUERY: &str = "SELECT account, sum(position) GROUP BY account ORDER BY account";
#[test]
fn query_uses_parse_cache_with_identical_output() {
let bin = require_rledger!();
let dir = tempfile::tempdir().expect("tempdir");
let file = dir.path().join("ledger.beancount");
std::fs::write(&file, SRC).expect("write ledger");
let run = |verbose: bool, disable_cache: bool| {
let mut cmd = Command::new(&bin);
cmd.arg("query");
if verbose {
cmd.arg("--verbose");
}
cmd.arg(&file).arg(QUERY);
cmd.env_remove("BEANCOUNT_DISABLE_LOAD_CACHE")
.env_remove("BEANCOUNT_LOAD_CACHE_FILENAME");
if disable_cache {
cmd.env("BEANCOUNT_DISABLE_LOAD_CACHE", "1");
}
cmd.output().expect("run rledger query")
};
let cold = run(false, false);
assert!(
cold.status.success(),
"cold query failed: {}",
String::from_utf8_lossy(&cold.stderr)
);
let cold_out = String::from_utf8_lossy(&cold.stdout).into_owned();
let cache_file = dir.path().join(".ledger.beancount.cache");
assert!(
cache_file.exists(),
"a cold query should have written the parse cache at {}",
cache_file.display()
);
let warm = run(true, false);
assert!(
warm.status.success(),
"warm query failed: {}",
String::from_utf8_lossy(&warm.stderr)
);
assert_eq!(
String::from_utf8_lossy(&warm.stdout),
cold_out,
"cached query output must match the cold run (incl. amount precision)"
);
let warm_err = String::from_utf8_lossy(&warm.stderr);
assert!(
warm_err.contains("from cache"),
"a verbose warm run should report a cache hit; stderr was:\n{warm_err}"
);
let nocache = run(false, true);
assert!(nocache.status.success());
assert_eq!(
String::from_utf8_lossy(&nocache.stdout),
cold_out,
"cache-disabled query output must match the cached run"
);
}
const FIFO_SRC: &str = r#"option "operating_currency" "USD"
option "booking_method" "FIFO"
2024-01-01 open Assets:Stock
2024-01-01 open Assets:Cash
2024-01-01 open Income:PnL
2024-02-01 * "Buy lot A"
Assets:Stock 10 STK {1 USD}
Assets:Cash -10 USD
2024-03-01 * "Buy lot B"
Assets:Stock 10 STK {2 USD}
Assets:Cash -20 USD
2024-04-01 * "Sell across both lots"
Assets:Stock -15 STK {}
Assets:Cash 30 USD
Income:PnL
"#;
#[test]
fn query_cache_preserves_file_booking_method() {
let bin = require_rledger!();
let dir = tempfile::tempdir().expect("tempdir");
let file = dir.path().join("ledger.beancount");
std::fs::write(&file, FIFO_SRC).expect("write ledger");
let query = "SELECT count(*) WHERE account = 'Assets:Stock' AND number < 0";
let run = |disable_cache: bool| {
let mut cmd = Command::new(&bin);
cmd.arg("query").arg(&file).arg(query);
cmd.env_remove("BEANCOUNT_DISABLE_LOAD_CACHE")
.env_remove("BEANCOUNT_LOAD_CACHE_FILENAME");
if disable_cache {
cmd.env("BEANCOUNT_DISABLE_LOAD_CACHE", "1");
}
cmd.output().expect("run rledger query")
};
let cold = run(false);
assert!(
cold.status.success(),
"cold query failed: {}",
String::from_utf8_lossy(&cold.stderr)
);
let cold_out = String::from_utf8_lossy(&cold.stdout).into_owned();
let nocache_out = {
let o = run(true);
assert!(o.status.success());
String::from_utf8_lossy(&o.stdout).into_owned()
};
let warm = run(false);
assert!(warm.status.success());
let warm_out = String::from_utf8_lossy(&warm.stdout).into_owned();
assert_eq!(
warm_out, cold_out,
"cached FIFO query diverged from the cold run — booking method \
lost across the cache round-trip (#1340)"
);
assert_eq!(warm_out, nocache_out, "cached output must match --no-cache");
let count: i64 = warm_out
.lines()
.find_map(|l| l.trim().parse::<i64>().ok())
.expect("query output should contain a numeric COUNT value");
assert_eq!(
count, 2,
"FIFO should expand the multi-lot reduction into 2 rows; got:\n{warm_out}"
);
}
#[test]
fn query_no_cache_flag_skips_cache() {
let bin = require_rledger!();
let dir = tempfile::tempdir().expect("tempdir");
let file = dir.path().join("ledger.beancount");
std::fs::write(&file, SRC).expect("write ledger");
let out = std::process::Command::new(&bin)
.arg("query")
.arg("--no-cache")
.arg(&file)
.arg(QUERY)
.env_remove("BEANCOUNT_DISABLE_LOAD_CACHE")
.env_remove("BEANCOUNT_LOAD_CACHE_FILENAME")
.output()
.expect("run rledger query --no-cache");
assert!(
out.status.success(),
"query --no-cache failed: {}",
String::from_utf8_lossy(&out.stderr)
);
let cache_file = dir.path().join(".ledger.beancount.cache");
assert!(
!cache_file.exists(),
"--no-cache must not write the parse cache, but {} exists",
cache_file.display()
);
}