use crate::reservoir::Reservoir;
#[test]
fn reservoir_bounded_at_cap() {
let r = Reservoir::new();
let default_cap = std::env::var("CAPTRACK_SAMPLE_CAP")
.ok()
.and_then(|s| s.parse::<usize>().ok())
.filter(|&n| n > 0)
.unwrap_or(4096);
for i in 0..100_000usize {
r.record(i);
}
let snap = r.snapshot();
assert!(
snap.len() <= default_cap,
"reservoir must not exceed cap={default_cap}, got {}",
snap.len()
);
}
fn percentile(sorted: &[usize], p: u8) -> usize {
let n = sorted.len();
debug_assert!(n > 0);
let rank = ((p as u128 * n as u128).div_ceil(100)) as usize;
let idx = rank.saturating_sub(1).min(n - 1);
sorted[idx]
}
#[test]
fn reservoir_preserves_percentiles_uniform() {
const N: usize = 100_000;
const RANGE: usize = 10_001; let r = Reservoir::new();
for i in 0..N {
r.record(i % RANGE);
}
let full_p95 = RANGE * 95 / 100;
let mut snap = r.snapshot();
assert!(!snap.is_empty(), "reservoir must not be empty");
snap.sort_unstable();
let res_p95 = percentile(&snap, 95);
let tolerance = (full_p95 as f64 * 0.05) as usize;
assert!(
res_p95.abs_diff(full_p95) <= tolerance,
"reservoir p95={res_p95} must be within ±5% of full p95={full_p95} (tolerance={tolerance})"
);
}
#[test]
fn reservoir_preserves_percentiles_bimodal() {
const N: usize = 100_000;
let r = Reservoir::new();
for i in 0..N {
if i % 2 == 0 {
r.record(100);
} else {
r.record(10_000);
}
}
let snap = r.snapshot();
assert!(!snap.is_empty(), "reservoir must not be empty");
let has_low = snap.contains(&100);
let has_high = snap.contains(&10_000);
assert!(has_low, "reservoir must contain the low mode (100)");
assert!(has_high, "reservoir must contain the high mode (10_000)");
let low_count = snap.iter().filter(|&&v| v == 100).count();
let high_count = snap.iter().filter(|&&v| v == 10_000).count();
let total = snap.len();
assert!(
low_count >= total * 30 / 100,
"low mode count={low_count} must be >=30% of total={total}"
);
assert!(
high_count >= total * 30 / 100,
"high mode count={high_count} must be >=30% of total={total}"
);
}
#[test]
fn total_observed_matches_push_count() {
const PUSHES: u64 = 50_000;
let r = Reservoir::new();
for i in 0..PUSHES {
r.record(i as usize);
}
assert_eq!(
r.total_observed(),
PUSHES,
"total_observed must equal exact push count"
);
}
#[test]
fn env_var_override_invariants() {
const PUSHES: usize = 10_000;
let r = Reservoir::new();
for i in 0..PUSHES {
r.record(i);
}
let snap = r.snapshot();
let total = r.total_observed();
assert!(
snap.len() as u64 <= total,
"snapshot len={} must be <= total_observed={}",
snap.len(),
total
);
assert_eq!(total, PUSHES as u64, "total_observed must equal push count");
}
#[test]
fn dump_includes_total_observed() {
use std::io::Read;
{
let _v: crate::TrackedVec<u8> = crate::tvec!("reservoir_tests/dump_total_observed", 512);
}
let dir = std::env::temp_dir().join("captrack_reservoir_test");
let path = dir.join("reservoir_dump.json");
crate::dump_capacity_stats(&path).expect("dump must succeed");
let mut f = std::fs::File::open(&path).expect("dump file must exist");
let mut s = String::new();
f.read_to_string(&mut s).unwrap();
let v: serde_json::Value = serde_json::from_str(&s).expect("must be valid JSON");
let stats = v["stats"].as_array().expect("stats must be an array");
let our_entry = stats
.iter()
.find(|e| e["name"].as_str() == Some("reservoir_tests/dump_total_observed"))
.expect("our named entry must appear in the dump");
let total_observed = our_entry["total_observed"]
.as_u64()
.expect("total_observed must be a u64 field in the JSON");
let sample_count = our_entry["samples"]
.as_array()
.map(|a| a.len() as u64)
.unwrap_or(0);
assert!(
total_observed >= sample_count,
"total_observed={total_observed} must be >= samples count={sample_count}"
);
assert!(total_observed >= 1, "total_observed must be at least 1");
}