test: add property-based tests, a shared finiteness helper, and boundary inputs
Most of what remained on #26. **Property tests (`tests/properties.rs`, proptest as a dev-dependency).** Four invariants over generated 1v1 schedules rather than hand-written fixtures, which is where this crate's shipped defects actually hid — a linear evidence product that underflowed only past ~1000 teams, and a batching path no golden exercised because every golden ingests in one call: - converged posteriors are always finite with positive sigma - log-evidence, batch and filtered, is finite and never above zero - filtered evidence is invariant to whether `converge` has run - one-at-a-time ingestion reaches the same fixed point as batched The invariance property was mutation-proved: making `filtered_step` read `skill.forward` instead of the carried message fails it with `-1.1038430064192069 -> -1.1135747072822761`. **Shared finiteness helper (`tests/common/mod.rs`).** `assert_finite` was local to `degenerate_inputs.rs`. It now also rejects a non-positive sigma, which the old version let through — `Gaussian::sigma` reports a non-positive precision as improper rather than trapping, so a collapsed posterior would have passed a finite-only check. **Boundary inputs.** Zero and negative weights, out-of-order timestamps, and extreme beta/sigma combinations. Worth recording that zero weight reaches `(m - performance.exclude(..)) * (1.0 / w)` — a division by zero — and the posterior comes out finite anyway; the test pins that rather than asserting what ought to happen. The weight tests `expect()` the commit rather than returning early on error, because an early return would have made them vacuous the moment validation changed. I checked that specifically by turning the return into a failure and confirming it did not fire. Not done, and left on #26: benchmark regression gating. Nothing fails on a regression today; making it fail needs a threshold chosen against how noisy the shared runner is, which is a policy call rather than a mechanical one. 60 test binaries, up from 56. MSRV 1.85 verified with proptest in the graph. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T5SYDExxL4vZgvunrcNSMc
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
//! Helpers shared across the integration suites.
|
||||
//!
|
||||
//! Each integration file is its own binary, so `mod common;` compiles a copy
|
||||
//! per suite. Anything unused in a given suite would warn, hence the
|
||||
//! `#![allow(dead_code)]`.
|
||||
|
||||
#![allow(dead_code)]
|
||||
|
||||
use trueskill_tt::Gaussian;
|
||||
|
||||
/// A posterior must be finite with a strictly positive sigma.
|
||||
///
|
||||
/// A non-finite posterior is the failure mode this crate is most prone to —
|
||||
/// EP breaking down produces NaN rather than an error — and a zero or negative
|
||||
/// sigma means the precision went non-positive, which `Gaussian::sigma` reports
|
||||
/// as improper rather than trapping.
|
||||
pub fn assert_finite(g: Gaussian, what: &str) {
|
||||
assert!(
|
||||
g.mu().is_finite(),
|
||||
"{what}: mu is not finite (mu={}, sigma={})",
|
||||
g.mu(),
|
||||
g.sigma()
|
||||
);
|
||||
|
||||
assert!(
|
||||
g.sigma().is_finite() && g.sigma() > 0.0,
|
||||
"{what}: sigma must be finite and positive (mu={}, sigma={})",
|
||||
g.mu(),
|
||||
g.sigma()
|
||||
);
|
||||
}
|
||||
|
||||
/// Every point on every learning curve must be finite.
|
||||
pub fn assert_curve_finite(curve: &[(i64, Gaussian)], who: &str) {
|
||||
for (time, g) in curve {
|
||||
assert_finite(*g, &format!("{who} at t={time}"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user