asset slice + testing initiative: fixed assets, property/mutation/fuzz testing, docs
This commit is contained in:
1 parent
65dd11d26b
commit
9ff4b5261a
1 file changed
+101
@@ -0,0 +1,101 @@
|
||||
// Fuzz target: arbitrary operation sequences against a Ledger. Errors
|
||||
// are fine (invalid ops are rejected); the invariant is that the trial
|
||||
// balance ALWAYS balances afterward, no matter the sequence. This is the
|
||||
// stateful counterpart of the proptest properties — libfuzzer explores
|
||||
// deeper interleavings. Staged here; `cargo fuzz init` wires it up.
|
||||
#![no_main]
|
||||
|
||||
use arbitrary::Arbitrary;
|
||||
use chrono::NaiveDate;
|
||||
use ledger_core::{AccountType, Currency, Ledger, Money, Side};
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
use rust_decimal::Decimal;
|
||||
|
||||
#[derive(Arbitrary, Debug, Clone, Copy)]
|
||||
enum Op {
|
||||
AddAccount { code: u8, typ: u8 },
|
||||
CreateEntry { day: u8 },
|
||||
AddLine {
|
||||
entry: u8,
|
||||
account: u8,
|
||||
debit: bool,
|
||||
cents: u32,
|
||||
},
|
||||
PostEntry { entry: u8 },
|
||||
ReverseEntry { entry: u8 },
|
||||
}
|
||||
|
||||
fn account_type(t: u8) -> AccountType {
|
||||
match t % 5 {
|
||||
0 => AccountType::Asset,
|
||||
1 => AccountType::Liability,
|
||||
2 => AccountType::Equity,
|
||||
3 => AccountType::Revenue,
|
||||
_ => AccountType::Expense,
|
||||
}
|
||||
}
|
||||
|
||||
fuzz_target!(|ops: Vec<Op>| {
|
||||
let mut ledger = Ledger::new(Currency::USD);
|
||||
let mut entry_ids: Vec<u64> = Vec::new();
|
||||
let mut next_code: u32 = 1000;
|
||||
|
||||
for op in ops {
|
||||
match op {
|
||||
Op::AddAccount { code, typ } => {
|
||||
// Unique codes by construction; duplicates are rejected.
|
||||
let c = format!("{:04}", next_code + (code as u32 % 50));
|
||||
next_code += 1;
|
||||
let _ = ledger.add_account(&c, "fuzz", account_type(typ), None, false);
|
||||
}
|
||||
Op::CreateEntry { day } => {
|
||||
let date =
|
||||
NaiveDate::from_ymd_opt(2026, 1, 1 + (day as u32 % 28)).unwrap();
|
||||
let id = ledger.create_entry(date, "fuzz");
|
||||
entry_ids.push(id);
|
||||
}
|
||||
Op::AddLine {
|
||||
entry,
|
||||
account,
|
||||
debit,
|
||||
cents,
|
||||
} => {
|
||||
if entry_ids.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let eid = entry_ids[entry as usize % entry_ids.len()];
|
||||
// Account ids are 1-based; may not exist — rejection is fine.
|
||||
let aid = (account as u64 % 60) + 1;
|
||||
let side = if debit { Side::Debit } else { Side::Credit };
|
||||
let amount = Decimal::new((cents % 1_000_000) as i64, 2);
|
||||
let _ = ledger.add_line(eid, aid, side, Money::new(amount, Currency::USD));
|
||||
}
|
||||
Op::PostEntry { entry } => {
|
||||
if entry_ids.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let eid = entry_ids[entry as usize % entry_ids.len()];
|
||||
let _ = ledger.post_entry(eid);
|
||||
}
|
||||
Op::ReverseEntry { entry } => {
|
||||
if entry_ids.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let eid = entry_ids[entry as usize % entry_ids.len()];
|
||||
let _ = ledger.reverse_entry(
|
||||
eid,
|
||||
NaiveDate::from_ymd_opt(2026, 12, 31).unwrap(),
|
||||
"fuzz reversal",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// THE invariant: no sequence of operations, valid or rejected, may
|
||||
// leave the trial balance out of balance.
|
||||
let tb = ledger_core::reports::trial_balance(&ledger);
|
||||
assert_eq!(
|
||||
tb.total_debits, tb.total_credits,
|
||||
"trial balance broken after fuzzed op sequence"
|
||||
);
|
||||
});
|
||||
Reference in new issue
Block a user