tests: rework the actually useful tests to live in the integration crate

This commit is contained in:
Nils Andreas Svee committed 2026-10-02 14:44:49 +07:00
1 parent 74e7532965
commit 3290a7508a
94 files changed
+1373 -15245

No files matched your search

+4 -77
View File
@@ -185,65 +185,13 @@ mod tests {
}
}
#[test]
fn ipdv_tracker_late_wrapped_previous_completes_pair() {
let mut tracker = IpdvTracker::new(None);
assert!(tracker.insert(ipdv_sample(0, 14)).is_empty());
let pairs = tracker.insert(ipdv_sample(u32::MAX, 10));
assert_eq!(
pairs,
vec![CompletedIpdvPair {
previous_seq: u32::MAX,
current_seq: 0,
rtt_ipdv_ns: 4,
send_ipdv_ns: None,
receive_ipdv_ns: None,
}]
);
}
#[test]
fn ipdv_tracker_bounded_mode_limits_sequence_state() {
let limit = 4;
let mut tracker = IpdvTracker::new(Some(limit));
// Far more inserts than the limit, so eviction runs repeatedly. Every
// per-sequence collection must stay bounded by the limit rather than
// growing with the number of samples seen, and eviction must not stop
// adjacent sequences from still completing a pair.
for seq in 0..512_u32 {
let pairs = tracker.insert(ipdv_sample(seq, i128::from(seq)));
assert_eq!(pairs.len(), usize::from(seq > 0), "seq {seq}");
assert!(tracker.samples.len() <= limit, "seq {seq}");
assert_eq!(
tracker.sample_order.len(),
tracker.samples.len(),
"seq {seq}"
);
assert!(tracker.completed_pairs.len() <= limit, "seq {seq}");
}
}
mod properties {
use super::*;
use proptest::prelude::*;
proptest! {
/// The hand-written `ipdv_tracker_bounded_mode_limits_sequence_state`
/// test above only exercises a strictly increasing `0..512`
/// sequence. Real traffic can duplicate, skip, or otherwise not
/// strictly increase sequence numbers (see the client's
/// `LateReply`/`DuplicateReply` handling), so this generates
/// arbitrary, possibly-repeated, possibly-non-monotonic sequence
/// streams and checks the same bounds still hold after every
/// insert: `samples` and `sample_order` never exceed the
/// configured limit and stay in lockstep, and `completed_pairs`
/// never exceeds `samples.len()` (a successful entry can only
/// persist once both its endpoint samples are present, see
/// `try_pair`, so it is always a subset of the live sample keys).
// Hidden retention state must stay bounded even with duplicates,
// gaps and reordered replies; snapshots cannot expose a leak here.
#[test]
fn bounded_tracker_state_never_exceeds_the_limit(
limit in 1usize..8,
@@ -253,32 +201,11 @@ mod tests {
for seq in seqs {
tracker.insert(ipdv_sample(seq, i128::from(seq)));
prop_assert!(tracker.samples.len() <= limit);
prop_assert_eq!(tracker.sample_order.len(), tracker.samples.len());
prop_assert!(tracker.completed_pairs.len() <= tracker.samples.len());
prop_assert!(tracker.sample_order.len() <= limit);
prop_assert!(tracker.completed_pairs.len() <= limit);
}
}
/// A pair for a given adjacent `(seq - 1, seq)` boundary is only
/// ever emitted once, however many times `insert` is called
/// (including for already-seen or unrelated sequences in
/// between). This is the unbounded case, so eviction cannot be
/// the reason a pair fails to repeat.
#[test]
fn each_adjacent_pair_is_emitted_at_most_once(
seqs in prop::collection::vec(0u32..12, 0..200),
) {
let mut tracker = IpdvTracker::new(None);
let mut seen_pairs = std::collections::HashSet::new();
for seq in seqs {
for pair in tracker.insert(ipdv_sample(seq, i128::from(seq))) {
let key = (pair.previous_seq, pair.current_seq);
prop_assert!(
seen_pairs.insert(key),
"pair {key:?} emitted more than once"
);
}
}
}
}
}
}
-40
View File
@@ -218,43 +218,3 @@ fn duration_ns_i128(duration: Duration) -> i128 {
fn signed_duration_ns(duration: SignedDuration) -> i128 {
duration.as_nanos()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn system_time_before_epoch_is_supported() {
let before = UNIX_EPOCH - Duration::from_nanos(7);
assert_eq!(system_time_ns(before), Some(-7));
let after = UNIX_EPOCH + Duration::from_nanos(7);
assert_eq!(system_time_ns(after), Some(7));
let now = SystemTime::now();
assert!(system_time_ns(now).is_some());
}
#[test]
fn loss_windowing_timestamp_is_clamped_to_its_own_sent_at() {
let base = Instant::now();
let sent_at = ClientTimestamp {
mono: base + Duration::from_millis(50),
wall: UNIX_EPOCH + Duration::from_millis(50),
};
// The pre-send-anchored `timeout_at` can precede this same probe's
// own post-send `sent_at` when probe_timeout is small relative to
// the send-call duration (see irtt-client's AGENTS.md). Rolling-
// window eviction assumes non-decreasing event timestamps, so the
// windowing `at()` must not regress behind the probe's own Sent
// event even though the public `timeout_at` field legitimately can.
let timeout_at = base + Duration::from_millis(10);
let event = ClientEvent::EchoLoss {
seq: 0,
sent_at,
timeout_at,
};
let normalized = normalize_event(&event).expect("loss event normalizes");
assert_eq!(normalized.at(), sent_at.mono);
}
}
+4 -46
View File
@@ -1,44 +1,6 @@
//! Property tests for the count-based and time-based rolling windows.
//!
//! [`RollingEvents`] (in `src/rolling.rs`) keeps two independent `VecDeque`s:
//! one bounded by [`StatsConfig::rolling_count`] and one bounded by
//! [`StatsConfig::rolling_time`]. They are pushed to together but evicted
//! independently, and each exposes its own snapshot
//! ([`StatsCollector::rolling_count`] / [`StatsCollector::rolling_time`]).
//! Reading `rolling.rs` and the existing `rolling_count_eviction_recomputes_*`
//! / `rolling_time_eviction_uses_event_timestamps` tests in `tests/stats.rs`
//! confirms there is no single window bounded by *both* limits at once: the
//! two bounds do not compose as AND or OR on shared storage, they are simply
//! two separate parallel windows over the same input stream. This file proves
//! that independence generatively: with both bounds configured
//! simultaneously, each window must behave exactly as if the *other* bound
//! did not exist.
//!
//! The reference model is a plain `Vec<(ClientEvent, at_ms)>` history that is
//! re-filtered from scratch after every operation (once by count, once by
//! time), then replayed through a fresh [`StatsCollector`] to obtain a
//! [`Snapshot`]. Because [`StatsCollector::rolling_count`] /
//! [`StatsCollector::rolling_time`] are themselves computed by replaying the
//! retained events through a fresh `CoreStats` on every call (see
//! `rolling.rs`'s `snapshot_window`), and because both the production window
//! and this reference model process the *same* `ClientEvent` values in the
//! *same* order through the *same* pure, deterministic normalization and
//! aggregation code, the two snapshots are expected to be bit-for-bit equal,
//! not merely close. `Snapshot` derives `PartialEq`, so the comparison below
//! is exact `assert_eq!` with no floating-point tolerance: there is no
//! independent numerical computation on the reference side to accumulate
//! error against, both sides run the identical arithmetic in the identical
//! order.
//!
//! The reference model's time-window recomputation (filter the whole history
//! by `at_ms >= latest_at_ms - window_ms`) is only equivalent to the
//! production incremental sliding-eviction (which pops from the front using
//! the cutoff computed at each push) when event timestamps are pushed in
//! non-decreasing order, exactly as `normalization.rs` documents ("Rolling-
//! window eviction assumes events are pushed in non-decreasing `at()`
//! order"). The operation generator below enforces that by construction: a
//! shared clock only ever moves forward, and every generated event borrows
//! its `at()` timestamp from the clock's current value.
//! Independent count and time windows must match a filtered replay through
//! StatsCollector, while cumulative accounting retains the complete history.
//! Generated events use non-decreasing timestamps, as required by the collector.
use std::time::{Duration, Instant, UNIX_EPOCH};
@@ -218,11 +180,7 @@ fn build_event(op: &Op, seq: u32, clock_ms: u64, base: Instant) -> ClientEvent {
}
}
/// Replays `events` in order through a fresh collector using the rolling
/// windows' own semantics (running statistics only, same late-reply policy),
/// mirroring exactly what `rolling.rs`'s private `snapshot_window` does for
/// the production windows. This is the "recompute from scratch" reference
/// primitive both the count and time reference windows below are built on.
/// Public reference: collect exactly the events retained by one window.
fn replay(events: &[ClientEvent], late_replies: LateReplyMode) -> Snapshot {
let mut collector = StatsCollector::new(StatsConfig {
samples: SampleMode::RunningOnly,