test: cover non-finite results and color-group disjointness
The two gaps #26 named that were never filled. NonFiniteResult had no test at all — the name appeared in `tests/` only inside a doc comment, and it is the sub-claim in that issue's title. It turns out to be very much reachable, and from *finite* inputs: sigma at 1e300, beta at 1e300, sigma at 1e-300, score_sigma at 1e-300, and scores at 1e308 all overflow inside inference, where the boundary checks cannot see them. That matters because the failure is silent by default — NaN fails every comparison, so a naive `step < epsilon` reads a NaN step as converged, which is why the crate has `step_converged`/`step_is_finite`. Pinned from outside, including that `converge_partial` does not launder a breakdown into an `Ok`, and with a control asserting merely extreme parameters still converge so the suite cannot pass by always failing. Color-group disjointness was #26's fourth acceptance criterion and had only five hand-written cases. Now a proptest over three shapes: a dense pool where collisions force colors to multiply, a sparse one where most events are independent, and repeated members within a single event. Two of my first assertions were wrong about the code rather than the reverse. A competitor named twice *within* one event is not a collision — `color_greedy` collects each event's members into a set for that reason. And contiguity is not a property of `color_greedy`: it holds only after `recompute_color_groups` reorders events so each color occupies one range. The test now asserts what is actually promised — that the reorder is always *possible*, since the parallel sweep slices `&mut` sub-ranges from those groups and overlapping ranges would be unsound. Refs #26 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011hcFjNDmHXZF8URGLku5zZ
This commit is contained in:
@@ -191,3 +191,121 @@ mod tests {
|
||||
assert_eq!(cg.total_events(), 4);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod properties {
|
||||
use std::collections::HashSet;
|
||||
|
||||
use proptest::prelude::*;
|
||||
|
||||
use super::*;
|
||||
|
||||
/// The property the whole parallel sweep rests on: two events sharing a
|
||||
/// competitor must never land in the same color, because a color group is
|
||||
/// run concurrently and two events touching one competitor would race.
|
||||
///
|
||||
/// Hand-written cases cover the shapes someone thought of. This covers the
|
||||
/// ones nobody did — the correctness of `sweep_color_groups` depends on it
|
||||
/// holding for every input, not for five.
|
||||
fn check(events: &[Vec<usize>]) {
|
||||
let groups = color_greedy(events.len(), |ev| {
|
||||
events[ev]
|
||||
.iter()
|
||||
.copied()
|
||||
.map(Index::from)
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
|
||||
// Disjointness *between events* within a color. Deduplicated per
|
||||
// event, because one event legitimately naming a competitor twice is
|
||||
// not a collision — `color_greedy` collects each event's members into
|
||||
// a set for exactly that reason.
|
||||
for color in 0..groups.n_colors() {
|
||||
let mut seen: HashSet<usize> = HashSet::new();
|
||||
for &ev in &groups.groups[color] {
|
||||
let members: HashSet<usize> = events[ev].iter().copied().collect();
|
||||
for competitor in members {
|
||||
assert!(
|
||||
seen.insert(competitor),
|
||||
"competitor {competitor} shared by two events in color {color}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Every event is assigned exactly once. Without this, a partition that
|
||||
// dropped events would satisfy disjointness trivially.
|
||||
let mut assigned: Vec<usize> = groups.groups.iter().flatten().copied().collect();
|
||||
assigned.sort_unstable();
|
||||
assert_eq!(assigned, (0..events.len()).collect::<Vec<_>>());
|
||||
assert_eq!(groups.total_events(), events.len());
|
||||
|
||||
// No empty colors: one would waste a sweep and make `n_colors`
|
||||
// misleading.
|
||||
for (color, group) in groups.groups.iter().enumerate() {
|
||||
assert!(!group.is_empty(), "color {color} is empty");
|
||||
}
|
||||
|
||||
// Contiguity is not a property of `color_greedy` — it holds only after
|
||||
// `recompute_color_groups` reorders the events so each color occupies
|
||||
// one range. What must always hold is that the reorder is *possible*:
|
||||
// relabelling events in group order yields contiguous groups. The
|
||||
// parallel sweep slices `&mut` sub-ranges from those, so if this ever
|
||||
// failed the reorder would produce overlapping ranges.
|
||||
let mut next = 0usize;
|
||||
let relabelled: Vec<Vec<usize>> = groups
|
||||
.groups
|
||||
.iter()
|
||||
.map(|group| {
|
||||
group
|
||||
.iter()
|
||||
.map(|_| {
|
||||
let i = next;
|
||||
next += 1;
|
||||
i
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.collect();
|
||||
assert!(ColorGroups { groups: relabelled }.groups_are_contiguous());
|
||||
}
|
||||
|
||||
proptest! {
|
||||
#![proptest_config(ProptestConfig::with_cases(512))]
|
||||
|
||||
/// Small competitor pool, so collisions are common and colors are
|
||||
/// forced to multiply.
|
||||
#[test]
|
||||
fn colors_are_disjoint_on_a_dense_pool(
|
||||
events in prop::collection::vec(
|
||||
prop::collection::vec(0usize..6, 1..4),
|
||||
0..20,
|
||||
)
|
||||
) {
|
||||
check(&events);
|
||||
}
|
||||
|
||||
/// Wide pool, so most events are independent and land in one color.
|
||||
#[test]
|
||||
fn colors_are_disjoint_on_a_sparse_pool(
|
||||
events in prop::collection::vec(
|
||||
prop::collection::vec(0usize..200, 1..6),
|
||||
0..30,
|
||||
)
|
||||
) {
|
||||
check(&events);
|
||||
}
|
||||
|
||||
/// Repeated competitors within one event must not confuse the
|
||||
/// member-set bookkeeping.
|
||||
#[test]
|
||||
fn colors_are_disjoint_with_repeated_members(
|
||||
events in prop::collection::vec(
|
||||
prop::collection::vec(0usize..3, 1..8),
|
||||
0..15,
|
||||
)
|
||||
) {
|
||||
check(&events);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user