use rustledger_core::cost::Cost;
use rustledger_core::{Amount, Decimal, Inventory, Position};
use std::str::FromStr;
fn big_units() -> Decimal {
Decimal::from_str("10000000000000000").expect("literal")
}
fn big_cost() -> Decimal {
Decimal::from_str("10000000000000").expect("literal")
}
#[test]
fn min_is_exactly_negative_max() {
assert_eq!(
Decimal::MIN,
-Decimal::MAX,
"clamped debits and credits cancel to exactly zero, so a saturating \
residual cannot distinguish a real balance from an overflowed one"
);
assert_eq!(
Decimal::MAX.saturating_add(Decimal::MIN),
Decimal::ZERO,
"this is the sum that made a 1e40 imbalance pass `check` in PR #1890"
);
}
#[test]
fn add_reports_overflow_instead_of_panicking() {
let mut inv = Inventory::new();
inv.add(Position::simple(Amount::new(Decimal::MAX, "USD")))
.expect("one MAX position fits");
let err = inv
.add(Position::simple(Amount::new(Decimal::MAX, "USD")))
.expect_err("the second cannot be represented and must be reported");
assert_eq!(
err.currency.as_str(),
"USD",
"the message must name the currency"
);
}
#[test]
fn a_rejected_add_leaves_the_inventory_unchanged() {
let one = Decimal::from_str("1").expect("literal");
let mut inv = Inventory::new();
inv.add(Position::with_cost(
Amount::new(Decimal::MAX, "HOOL"),
Cost::new(one, "USD"),
))
.expect("costed position fits");
inv.add(Position::simple(Amount::new(Decimal::MIN, "HOOL")))
.expect("cache nets to zero, uncosted total is MIN");
assert_eq!(inv.units("HOOL"), Decimal::ZERO, "precondition: cache is 0");
let positions_before = inv.positions().count();
inv.add(Position::simple(Amount::new(-one, "HOOL")))
.expect_err("the uncosted merge leaves the range");
assert_eq!(
inv.units("HOOL"),
Decimal::ZERO,
"the units cache must not be committed when the merge fails"
);
assert_eq!(
inv.positions().count(),
positions_before,
"no position may be appended by a failed add"
);
}
#[test]
fn at_cost_reports_the_product_overflow() {
let mut inv = Inventory::new();
inv.add(Position::with_cost(
Amount::new(big_units(), "HOOL"),
Cost::new(big_cost(), "USD"),
))
.expect("the position itself fits");
let err = inv.at_cost().expect_err("units * cost leaves the range");
assert_eq!(
err.currency.as_str(),
"USD",
"reported in the COST currency"
);
}
#[test]
fn book_value_distinguishes_uncosted_from_unrepresentable() {
let mut costed = Inventory::new();
costed
.add(Position::with_cost(
Amount::new(big_units(), "HOOL"),
Cost::new(big_cost(), "USD"),
))
.expect("fits");
costed
.book_value("HOOL")
.expect_err("an out-of-range product must be reported");
let mut uncosted = Inventory::new();
uncosted
.add(Position::simple(Amount::new(big_units(), "HOOL")))
.expect("fits");
let totals = uncosted
.book_value("HOOL")
.expect("no cost is not an overflow");
assert!(totals.is_empty(), "an uncosted position has no book value");
}
#[test]
fn total_cost_reports_rather_than_panicking() {
let cost = Cost::new(big_cost(), "USD");
assert!(
cost.total_cost(big_units()).is_none(),
"the product leaves the range"
);
let ok = cost
.total_cost(Decimal::from_str("10").expect("literal"))
.expect("an ordinary quantity fits");
assert_eq!(
ok.number,
Decimal::from_str("100000000000000").expect("literal")
);
}
#[test]
fn subtree_sum_reports_overflow() {
use rustledger_core::{Account, Currency};
let mut a = Inventory::new();
a.add(Position::simple(Amount::new(Decimal::MAX, "USD")))
.expect("fits");
let mut b = Inventory::new();
b.add(Position::simple(Amount::new(Decimal::MAX, "USD")))
.expect("fits");
let accounts = [
(Account::from("Assets:Bank"), a),
(Account::from("Assets:Bank:Sub"), b),
];
let usd = Currency::from("USD");
assert!(
rustledger_core::sum_account_and_subaccounts(
accounts.iter().map(|(k, v)| (k, v)),
"Assets:Bank",
&usd,
)
.is_none(),
"two MAX sub-balances cannot be summed and must report, not panic"
);
}
#[test]
fn ordinary_arithmetic_is_untouched() {
let mut inv = Inventory::new();
inv.add(Position::simple(Amount::new(
Decimal::from_str("100.50").expect("literal"),
"USD",
)))
.expect("fits");
inv.add(Position::simple(Amount::new(
Decimal::from_str("-40.25").expect("literal"),
"USD",
)))
.expect("fits");
assert_eq!(
inv.units("USD"),
Decimal::from_str("60.25").expect("literal")
);
let mut costed = Inventory::new();
costed
.add(Position::with_cost(
Amount::new(Decimal::from_str("10").expect("literal"), "HOOL"),
Cost::new(Decimal::from_str("150.00").expect("literal"), "USD"),
))
.expect("fits");
let at = costed.at_cost().expect("an ordinary product fits");
assert_eq!(
at.units("USD"),
Decimal::from_str("1500.00").expect("literal"),
"10 * 150.00"
);
}