Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions tests/ledgers/stress-many-lots.beancount
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
; Multiple held-at-cost lots of one commodity + a partial FIFO-style reduction
2020-01-01 open Assets:Stock
2020-01-01 open Assets:Cash
2020-01-01 open Income:Gains

2020-02-01 * "buy lot at 50"
Assets:Stock 10 HOOL {50 USD}
Assets:Cash -500 USD

2020-03-01 * "buy lot at 60"
Assets:Stock 10 HOOL {60 USD}
Assets:Cash -600 USD

2020-04-01 * "sell 5 from the 50 lot at 60, realizing a gain"
Assets:Stock -5 HOOL {50 USD}
Assets:Cash 300 USD
Income:Gains
12 changes: 12 additions & 0 deletions tests/ledgers/stress-pad-balance.beancount
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
; pad -> balance resolution, then a later passing balance
2020-01-01 open Assets:Cash USD
2020-01-01 open Equity:Opening-Balances

2020-01-05 pad Assets:Cash Equity:Opening-Balances
2020-01-10 balance Assets:Cash 500 USD

2020-02-01 * "spend"
Assets:Cash -100 USD
Equity:Opening-Balances

2020-02-10 balance Assets:Cash 400 USD
11 changes: 11 additions & 0 deletions tests/ledgers/stress-total-price.beancount
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
; @@ total-price postings (per-unit = total / units); absent from other fixtures
option "operating_currency" "USD"

2020-01-01 open Assets:USD USD
2020-01-01 open Assets:EUR EUR
2020-01-01 open Income:Trade

2020-02-01 * "convert 7 USD at a total price of 10 EUR"
Assets:USD -7 USD @@ 10 EUR
Assets:EUR 10 EUR
Income:Trade
77 changes: 75 additions & 2 deletions tests/test_differential_beancount.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,30 @@
from collections.abc import Iterable

DATA = Path(__file__).parent / "data"
# Hand-authored differential fixtures live outside tests/data/ so they are not
# picked up by the ingest directory walk (test_api_imports snapshots that dir).
LEDGERS_DIR = Path(__file__).parent / "ledgers"


def _ledger_path(name: str) -> str:
"""Resolve a fixture to tests/ledgers/ if present, else tests/data/."""
stress = LEDGERS_DIR / f"{name}.beancount"
return str(stress if stress.exists() else DATA / f"{name}.beancount")

# Fixtures whose booking rustfava must reproduce exactly. These span
# held-at-cost lots, prices, multiple currencies, a pad/balance pair (example)
# and a date-boundary case (off-by-one); long-example alone has ~193 cost lots.
LEDGERS = ["example", "long-example", "query-example", "off-by-one"]
# The stress-* fixtures add constructs the others lack: `@@` total prices,
# multiple lots of one commodity with a partial reduction, and pad->balance.
LEDGERS = [
"example",
"long-example",
"query-example",
"off-by-one",
"stress-total-price",
"stress-many-lots",
"stress-pad-balance",
]

# Type of `cost` normalized to beancount's 4-tuple identity — deliberately
# dropping rustfava's extra ``number_total`` field so two engines' economically
Expand Down Expand Up @@ -92,7 +111,7 @@ def _account_inventories(
@pytest.mark.parametrize("ledger", LEDGERS)
def test_booked_inventories_match_beancount(ledger: str) -> None:
"""Per-account booked inventories must equal beancount's, to the cent."""
path = str(DATA / f"{ledger}.beancount")
path = _ledger_path(ledger)
bc_entries, _bc_errors, _ = beancount_loader.load_file(path)
rf_entries, _rf_errors, _ = rf_loader.load_uncached(path)

Expand Down Expand Up @@ -208,3 +227,57 @@ def test_clamped_totals_match_beancount_balance_at_cutoff() -> None:
f"clamped total mismatch for {account}: "
f"rustfava={rf_inv[account]} beancount={bc_inv[account]}"
)


def test_stress_fixtures_hand_verified() -> None:
"""Explicit expected numbers for the stress fixtures.

The differential tests above already cross-check these against beancount;
this pins the values by hand as a second, oracle-independent check of the
constructs the fixtures were added for.
"""
d = datetime.date

# @@ total price: A drains 7 USD, B gains 10 EUR; the sold posting's price
# is the per-unit 10/7, and no cost is created.
entries, errors, _ = rf_loader.load_uncached(
str(LEDGERS_DIR / "stress-total-price.beancount")
)
assert not errors
inv = _account_inventories(entries)
assert inv["Assets:USD"] == {("USD", None): Decimal(-7)}
assert inv["Assets:EUR"] == {("EUR", None): Decimal(10)}
(usd_posting,) = [
p
for e in entries
for p in getattr(e, "postings", [])
if getattr(p, "account", "") == "Assets:USD"
]
assert usd_posting.cost is None
assert usd_posting.price is not None
# per-unit = 10 EUR / 7 USD = 1.4286 (to 4dp); the engine keeps full
# precision, so compare at a sane scale rather than bit-for-bit.
assert round(usd_posting.price.number, 4) == Decimal("1.4286")

# Many lots: after selling 5 of the 50-lot, 5 HOOL @ {50} + 10 HOOL @ {60}.
entries, errors, _ = rf_loader.load_uncached(
str(LEDGERS_DIR / "stress-many-lots.beancount")
)
assert not errors
stock = _account_inventories(entries)["Assets:Stock"]
assert stock == {
("HOOL", (Decimal(50), "USD", d(2020, 2, 1), None)): Decimal(5),
("HOOL", (Decimal(60), "USD", d(2020, 3, 1), None)): Decimal(10),
}

# pad -> balance: the pad fills Cash to the asserted 500, spend leaves 400,
# and both balance assertions pass (no errors, diff_amount None).
entries, errors, _ = rf_loader.load_uncached(
str(LEDGERS_DIR / "stress-pad-balance.beancount")
)
assert not errors
assert _account_inventories(entries)["Assets:Cash"] == {
("USD", None): Decimal(400)
}
for bal in _balance_dirs(entries):
assert bal.diff_amount is None
Loading