Files
wanderbooks/crates/ledger-config/tests/codec_props.rs
T
swaitsandClaude Opus 5.5 b0a9fd8ad4 P3: depreciation straight-line by days through the final month, schedules by day (defects 22, 9, D-37)
The final month takes what the months before leave, spread over its days;
a disposed asset stops on its disposal day. A skipped month is caught up
in the month the run is made; a reversed run un-posts its month. New
schedules spread by day; equal-months schedules must start on the 1st.
Amounts round to the currency's own minor unit. Kani and Lean proofs; an
exhaustive search over runs, reversals and disposals.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T3gXMVt7XJPi6ens1or9Dj
2026-10-21 14:04:11 -06:00

125 lines
5.2 KiB
Rust

//! The settings file codec over settings built at random from valid and
//! refused changes (plan/served.md P1: toml_round_trip_zero_events).
//!
//! ```text
//! INVARIANTS (crate::codec):
//! 1. An export imports into the same books as no change at all.
//! 2. An export imports into empty books as exactly those settings,
//! identity values aside (exported masked: they stay unset).
//! 3. The export reads, as a settings file, as the books do.
//! ```
use chrono::NaiveDate;
use ledger_config::codec;
use ledger_core::settings::{Change, Settings, Source};
use proptest::prelude::*;
use serde_json::{Value, json};
fn src() -> Source {
Source {
decided_by: "Pat Rivera".into(),
decided_on: NaiveDate::from_ymd_opt(2026, 10, 17).unwrap(),
citation: Some("Operating agreement §4.1\nsecond line".into()),
note: None,
}
}
fn candidates() -> Vec<(&'static str, Value)> {
vec![
("company.name", json!("Acme \"Widgets\" LLC")),
("company.name", json!("Ünïcödé Holdings")),
("company.entity_type", json!("LLC, multi-member")),
("company.tax_classification", json!("S corporation")),
("company.tax_classification", json!("partnership")),
("company.tax_form", json!("1065")),
("company.fiscal_year_start", json!("07-01")),
("company.timezone", json!("America/Denver")),
("company.formation_state", json!("UT")),
("company.formed", json!("2026-10-04")),
("company.state_entity_number", json!("14745117-0160")),
("company.ein", json!("28-8525893")),
("company.irs_name_control", json!("waln")),
("company.duns", json!("149-976-972")),
("company.address", json!("1 Main St\nSuite 2, Midvale, UT")),
("company.registered_agent", json!("Pat Rivera")),
("company.partnership_representative", json!("Pat Rivera")),
("company.cpa", json!("Casey Rowe")),
(
"company.members",
json!([{"name": "A", "share": "50%", "capital_account": "3000",
"distribution_account": "3100", "files_jointly_with": "B"},
{"name": "B", "share": "0.5", "capital_account": "3001",
"files_jointly_with": "A"}]),
),
(
"company.members",
json!([{"name": "Solo", "share": "100%", "capital_account": "3000"}]),
),
("books.accounting_basis", json!("cash")),
("books.cash_accounts", json!(["1000", "1010"])),
("books.financing_accounts", json!(["2100"])),
("books.investing_accounts", json!(["1400"])),
("books.close_into", json!("3000")),
("books.close_sweep", json!(["3100"])),
("books.asset_tag_prefix", json!("FL")),
("books.schedule_convention", json!("monthly_even")),
("books.schedule_convention", json!("daily")),
("policy.evidence.threshold", json!("0.00")),
("policy.evidence.threshold", json!("25.5")),
("policy.evidence.required_accounts", json!(["6200"])),
("policy.evidence.exempt_accounts", json!(["6999", "6998"])),
("policy.parties", json!("require")),
("policy.approval_rules", json!("enforce")),
]
}
fn build(picks: &[(usize, bool)]) -> Settings {
let pool = candidates();
let mut s = Settings::default();
for (seq, (i, clear)) in picks.iter().enumerate() {
let (k, v) = &pool[i % pool.len()];
let change = Change {
key: (*k).into(),
value: (!clear).then(|| v.clone()),
};
// Refused changes (settings that wouldn't hold together) are
// skipped, as the books skip them.
if let Ok(made) = s.check_changes(&[change], &src()) {
s.apply(&made, &src(), seq as u64 + 1).unwrap();
}
}
s
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(512))]
#[test]
fn exports_round_trip(picks in proptest::collection::vec((0usize..64, prop::bool::weighted(0.15)), 0..30)) {
let books = build(&picks);
let text = codec::export(&books, "USD");
// INVARIANT 3: the export reads as a settings file.
let file = ledger_config::parse(&text)
.unwrap_or_else(|e| panic!("the export reads as a settings file: {e}\n{text}"));
prop_assert_eq!(&file.company.name, &books.company("USD").name);
// INVARIANT 1.
let same = codec::import(&text, None, &books, "USD")
.unwrap_or_else(|e| panic!("{e}\n{text}"));
prop_assert!(same.changes.is_empty(), "{:?}\n{}", same.changes, text);
prop_assert!(same.unknown.is_empty(), "{:?}", same.unknown);
// INVARIANT 2.
let fresh = codec::import(&text, None, &Settings::default(), "USD")
.unwrap_or_else(|e| panic!("{e}\n{text}"));
let mut other = Settings::default();
other.apply(&fresh.changes, &src(), 1).unwrap();
for (k, r) in books.iter() {
if k.name == "company.ein" {
prop_assert!(other.get(k.name).is_none());
} else {
prop_assert_eq!(other.get(k.name).map(|o| &o.value), Some(&r.value), "{}", k.name);
}
}
prop_assert_eq!(other.iter().count() + usize::from(books.get("company.ein").is_some()), books.iter().count());
}
}