use approx::assert_relative_eq;
use proptest::prelude::*;
use reqsketch::{RankAccuracy, ReqSketch, SearchCriteria};
mod reference_tests {
use super::*;
#[test]
fn test_empty_sketch() {
let mut sketch: ReqSketch<f32> = ReqSketch::new();
assert_eq!(sketch.k(), 12);
assert!(sketch.is_empty());
assert!(!sketch.is_estimation_mode());
assert_eq!(sketch.len(), 0);
assert_eq!(sketch.num_retained(), 0);
assert!(sketch.min_item().is_none());
assert!(sketch.max_item().is_none());
assert!(sketch.rank(&0.0, SearchCriteria::Inclusive).is_err());
assert!(sketch.quantile(0.5, SearchCriteria::Inclusive).is_err());
assert!(sketch.pmf(&[0.0], SearchCriteria::Inclusive).is_err());
assert!(sketch.cdf(&[0.0], SearchCriteria::Inclusive).is_err());
}
#[test]
fn test_single_value_hra() {
let mut sketch = ReqSketch::new();
sketch.update(1.0f32);
assert!(!sketch.is_empty());
assert!(!sketch.is_estimation_mode());
assert_eq!(sketch.len(), 1);
assert_eq!(sketch.num_retained(), 1);
assert_eq!(sketch.min_item(), Some(&1.0));
assert_eq!(sketch.max_item(), Some(&1.0));
assert_relative_eq!(
sketch
.rank(&1.0, SearchCriteria::Exclusive)
.expect("Operation should succeed"),
0.0
);
assert_relative_eq!(
sketch
.rank(&1.0, SearchCriteria::Inclusive)
.expect("Operation should succeed"),
1.0
);
assert_relative_eq!(
sketch
.rank(&1.1, SearchCriteria::Exclusive)
.expect("Operation should succeed"),
1.0
);
assert_relative_eq!(
sketch
.rank(&f32::INFINITY, SearchCriteria::Inclusive)
.expect("Operation should succeed"),
1.0
);
assert_relative_eq!(
sketch
.quantile(0.0, SearchCriteria::Exclusive)
.expect("Operation should succeed"),
1.0
);
assert_relative_eq!(
sketch
.quantile(0.5, SearchCriteria::Exclusive)
.expect("Operation should succeed"),
1.0
);
assert_relative_eq!(
sketch
.quantile(1.0, SearchCriteria::Exclusive)
.expect("Operation should succeed"),
1.0
);
}
#[test]
fn test_single_value_lra() {
let mut sketch: ReqSketch<f32> = ReqSketch::builder()
.rank_accuracy(RankAccuracy::LowRank)
.build()
.expect("Operation should succeed");
sketch.update(1.0f32);
assert_eq!(sketch.rank_accuracy(), RankAccuracy::LowRank);
assert!(!sketch.is_empty());
assert!(!sketch.is_estimation_mode());
assert_eq!(sketch.len(), 1);
assert_eq!(sketch.num_retained(), 1);
}
#[test]
fn test_repeated_values() {
let mut sketch = ReqSketch::new();
sketch.update(1.0f32);
sketch.update(1.0f32);
sketch.update(1.0f32);
sketch.update(2.0f32);
sketch.update(2.0f32);
sketch.update(2.0f32);
assert!(!sketch.is_empty());
assert!(!sketch.is_estimation_mode());
assert_eq!(sketch.len(), 6);
assert_eq!(sketch.num_retained(), 6);
assert_relative_eq!(
sketch
.rank(&1.0, SearchCriteria::Exclusive)
.expect("Operation should succeed"),
0.0
);
assert_relative_eq!(
sketch
.rank(&1.0, SearchCriteria::Inclusive)
.expect("Operation should succeed"),
0.5
);
assert_relative_eq!(
sketch
.rank(&2.0, SearchCriteria::Exclusive)
.expect("Operation should succeed"),
0.5
);
assert_relative_eq!(
sketch
.rank(&2.0, SearchCriteria::Inclusive)
.expect("Operation should succeed"),
1.0
);
}
#[test]
fn test_exact_mode_10_values() {
let mut sketch = ReqSketch::new();
for i in 1..=10 {
sketch.update(i as f32);
}
assert!(!sketch.is_empty());
assert!(!sketch.is_estimation_mode());
assert_eq!(sketch.len(), 10);
assert_eq!(sketch.num_retained(), 10);
assert_relative_eq!(
sketch
.rank(&1.0, SearchCriteria::Exclusive)
.expect("Operation should succeed"),
0.0,
epsilon = 1e-6
);
assert_relative_eq!(
sketch
.rank(&2.0, SearchCriteria::Exclusive)
.expect("Operation should succeed"),
0.1,
epsilon = 1e-6
);
assert_relative_eq!(
sketch
.rank(&6.0, SearchCriteria::Exclusive)
.expect("Operation should succeed"),
0.5,
epsilon = 1e-6
);
assert_relative_eq!(
sketch
.rank(&9.0, SearchCriteria::Exclusive)
.expect("Operation should succeed"),
0.8,
epsilon = 1e-6
);
assert_relative_eq!(
sketch
.rank(&10.0, SearchCriteria::Exclusive)
.expect("Operation should succeed"),
0.9,
epsilon = 1e-6
);
assert_relative_eq!(
sketch
.rank(&1.0, SearchCriteria::Inclusive)
.expect("Operation should succeed"),
0.1,
epsilon = 1e-6
);
assert_relative_eq!(
sketch
.rank(&2.0, SearchCriteria::Inclusive)
.expect("Operation should succeed"),
0.2,
epsilon = 1e-6
);
assert_relative_eq!(
sketch
.rank(&5.0, SearchCriteria::Inclusive)
.expect("Operation should succeed"),
0.5,
epsilon = 1e-6
);
assert_relative_eq!(
sketch
.rank(&9.0, SearchCriteria::Inclusive)
.expect("Operation should succeed"),
0.9,
epsilon = 1e-6
);
assert_relative_eq!(
sketch
.rank(&10.0, SearchCriteria::Inclusive)
.expect("Operation should succeed"),
1.0,
epsilon = 1e-6
);
assert_relative_eq!(
sketch
.quantile(0.0, SearchCriteria::Exclusive)
.expect("Operation should succeed"),
1.0,
epsilon = 1e-6
);
assert_relative_eq!(
sketch
.quantile(0.1, SearchCriteria::Exclusive)
.expect("Operation should succeed"),
2.0,
epsilon = 1e-6
);
assert_relative_eq!(
sketch
.quantile(0.5, SearchCriteria::Exclusive)
.expect("Operation should succeed"),
6.0,
epsilon = 1e-6
);
assert_relative_eq!(
sketch
.quantile(0.9, SearchCriteria::Exclusive)
.expect("Operation should succeed"),
10.0,
epsilon = 1e-6
);
assert_relative_eq!(
sketch
.quantile(1.0, SearchCriteria::Exclusive)
.expect("Operation should succeed"),
10.0,
epsilon = 1e-6
);
assert_relative_eq!(
sketch
.quantile(0.0, SearchCriteria::Inclusive)
.expect("Operation should succeed"),
1.0,
epsilon = 1e-6
);
assert_relative_eq!(
sketch
.quantile(0.1, SearchCriteria::Inclusive)
.expect("Operation should succeed"),
1.0,
epsilon = 1e-6
);
assert_relative_eq!(
sketch
.quantile(0.5, SearchCriteria::Inclusive)
.expect("Operation should succeed"),
5.0,
epsilon = 1e-6
);
assert_relative_eq!(
sketch
.quantile(0.9, SearchCriteria::Inclusive)
.expect("Operation should succeed"),
9.0,
epsilon = 1e-6
);
assert_relative_eq!(
sketch
.quantile(1.0, SearchCriteria::Inclusive)
.expect("Operation should succeed"),
10.0,
epsilon = 1e-6
);
let splits = [2.0, 6.0, 9.0];
let cdf = sketch
.cdf(&splits, SearchCriteria::Exclusive)
.expect("Operation should succeed");
assert_relative_eq!(cdf[0], 0.1, epsilon = 1e-6);
assert_relative_eq!(cdf[1], 0.5, epsilon = 1e-6);
assert_relative_eq!(cdf[2], 0.8, epsilon = 1e-6);
assert_relative_eq!(cdf[3], 1.0, epsilon = 1e-6);
let pmf = sketch
.pmf(&splits, SearchCriteria::Exclusive)
.expect("Operation should succeed");
assert_relative_eq!(pmf[0], 0.1, epsilon = 1e-6);
assert_relative_eq!(pmf[1], 0.4, epsilon = 1e-6);
assert_relative_eq!(pmf[2], 0.3, epsilon = 1e-6);
assert_relative_eq!(pmf[3], 0.2, epsilon = 1e-6);
}
#[test]
fn test_estimation_mode() {
let mut sketch = ReqSketch::new();
let n = 100_000;
for i in 0..n {
sketch.update(i as f32);
}
assert!(!sketch.is_empty());
assert!(sketch.is_estimation_mode());
assert_eq!(sketch.len(), n);
assert!(sketch.num_retained() < n as u32);
let r0 = sketch
.rank(&0.0, SearchCriteria::Exclusive)
.expect("Operation should succeed");
let rmax = sketch
.rank(&(n as f32), SearchCriteria::Exclusive)
.expect("Operation should succeed");
let rmid = sketch
.rank(&(n as f32 / 2.0), SearchCriteria::Exclusive)
.expect("Operation should succeed");
let rend = sketch
.rank(&((n - 1) as f32), SearchCriteria::Exclusive)
.expect("Operation should succeed");
assert!(
(r0 - 0.0).abs() <= 1e-3,
"Rank of 0.0 should be ~0.0, got {}",
r0
);
assert!(
(rmax - 1.0).abs() <= 1e-3,
"Rank of {} should be ~1.0, got {}",
n as f32,
rmax
);
assert!(
(rmid - 0.5).abs() <= 0.01,
"Rank of {} should be ~0.5, got {} (error: {:.1}%)",
n as f32 / 2.0,
rmid,
(rmid - 0.5).abs() * 200.0
);
assert!(
(rend - 1.0).abs() <= 0.05,
"Rank of {} should be ~1.0, got {}",
(n - 1) as f32,
rend
);
assert_eq!(sketch.min_item(), Some(&0.0));
assert_eq!(sketch.max_item(), Some(&((n - 1) as f32)));
}
#[test]
fn test_merge_into_empty() {
let mut sketch1: ReqSketch<f32> = ReqSketch::builder()
.k(40)
.expect("Operation should succeed")
.build()
.expect("Operation should succeed");
let mut sketch2: ReqSketch<f32> = ReqSketch::builder()
.k(40)
.expect("Operation should succeed")
.build()
.expect("Operation should succeed");
for i in 0..1000 {
sketch2.update(i as f32);
}
sketch1.merge(&sketch2).expect("Operation should succeed");
assert_eq!(sketch1.min_item(), Some(&0.0));
assert_eq!(sketch1.max_item(), Some(&999.0));
let q25 = sketch1
.quantile(0.25, SearchCriteria::Inclusive)
.expect("Operation should succeed");
let q50 = sketch1
.quantile(0.5, SearchCriteria::Inclusive)
.expect("Operation should succeed");
let q75 = sketch1
.quantile(0.75, SearchCriteria::Inclusive)
.expect("Operation should succeed");
let r50 = sketch1
.rank(&500.0, SearchCriteria::Inclusive)
.expect("Operation should succeed");
let q25_error = (q25 - 250.0).abs() / 250.0;
let q50_error = (q50 - 500.0).abs() / 500.0;
let q75_error = (q75 - 750.0).abs() / 750.0;
let r50_error = (r50 - 0.5).abs() / 0.5;
assert!(
q25_error <= 0.03,
"25th percentile error too high: {} > 3%",
q25_error * 100.0
);
assert!(
q50_error <= 0.03,
"50th percentile error too high: {} > 3%",
q50_error * 100.0
);
assert!(
q75_error <= 0.03,
"75th percentile error too high: {} > 3%",
q75_error * 100.0
);
assert!(
r50_error <= 0.03,
"Rank error too high: {} > 3%",
r50_error * 100.0
);
}
#[test]
fn test_merge_two_ranges() {
let mut sketch1: ReqSketch<f32> = ReqSketch::builder()
.k(100)
.expect("Operation should succeed")
.build()
.expect("Operation should succeed");
let mut sketch2: ReqSketch<f32> = ReqSketch::builder()
.k(100)
.expect("Operation should succeed")
.build()
.expect("Operation should succeed");
for i in 0..1000 {
sketch1.update(i as f32);
}
for i in 1000..2000 {
sketch2.update(i as f32);
}
sketch1.merge(&sketch2).expect("Operation should succeed");
assert_eq!(sketch1.min_item(), Some(&0.0));
assert_eq!(sketch1.max_item(), Some(&1999.0));
let q25 = sketch1
.quantile(0.25, SearchCriteria::Inclusive)
.expect("Operation should succeed");
let q50 = sketch1
.quantile(0.5, SearchCriteria::Inclusive)
.expect("Operation should succeed");
let q75 = sketch1
.quantile(0.75, SearchCriteria::Inclusive)
.expect("Operation should succeed");
let r50 = sketch1
.rank(&1000.0, SearchCriteria::Inclusive)
.expect("Operation should succeed");
let q25_error = (q25 - 500.0).abs() / 500.0;
let q50_error = (q50 - 1000.0).abs() / 1000.0;
let q75_error = (q75 - 1500.0).abs() / 1500.0;
let r50_error = (r50 - 0.5).abs() / 0.5;
assert!(
q25_error <= 0.05,
"25th percentile error too high: {:.1}% > 5%",
q25_error * 100.0
);
assert!(
q50_error <= 0.03,
"50th percentile error too high: {:.1}% > 3%",
q50_error * 100.0
);
assert!(
q75_error <= 0.05,
"75th percentile error too high: {:.1}% > 5%",
q75_error * 100.0
);
assert!(
r50_error <= 0.03,
"Rank error too high: {:.1}% > 3%",
r50_error * 100.0
);
}
#[test]
fn test_merge_incompatible_accuracy_modes() {
let mut sketch1 = ReqSketch::new(); let sketch2: ReqSketch<f32> = ReqSketch::builder()
.rank_accuracy(RankAccuracy::LowRank)
.build()
.expect("Operation should succeed");
sketch1.update(1.0);
assert!(sketch1.merge(&sketch2).is_err());
}
}
mod statistical_tests {
use super::*;
#[test]
fn test_weight_conservation() {
for &n in &[10u64, 100, 1_000, 10_000] {
let mut sketch = ReqSketch::new();
for i in 0..n {
sketch.update(i as f64);
}
let total_weight: u64 = sketch.iter().map(|(_, w)| w).sum();
assert_eq!(total_weight, n, "Weight conservation violated for n={}", n);
}
}
#[test]
fn test_high_rank_accuracy_in_hra_mode() {
let mut sketch = ReqSketch::new(); let n = 50_000u64;
for i in 0..n {
sketch.update(i as f64);
}
for &rank in &[0.9, 0.95, 0.99] {
let true_quantile = rank * (n - 1) as f64;
let estimated_rank = sketch
.rank(&true_quantile, SearchCriteria::Inclusive)
.expect("rank should succeed");
let rank_error = (estimated_rank - rank).abs();
assert!(
rank_error < 0.05,
"HRA high-rank error too large at rank {}: error={:.4}",
rank,
rank_error
);
}
}
#[test]
fn test_rank_accuracy_bounds() {
let mut sketch = ReqSketch::new();
let n = 10_000;
for i in 0..n {
sketch.update(i as f64);
}
let test_values: Vec<f64> = (0..n).step_by(1000).map(|i| i as f64).collect();
let mut last_rank = 0.0;
for value in test_values {
let rank = sketch
.rank(&value, SearchCriteria::Inclusive)
.expect("Operation should succeed");
assert!(rank >= last_rank, "Ranks should be monotonic");
assert!((0.0..=1.0).contains(&rank), "Ranks should be in [0,1]");
last_rank = rank;
}
}
#[test]
fn test_pmf_cdf_consistency() {
let mut sketch = ReqSketch::new();
for i in 0..1000 {
sketch.update(i as f64);
}
let split_points = [100.0, 300.0, 500.0, 700.0, 900.0];
let pmf = sketch
.pmf(&split_points, SearchCriteria::Inclusive)
.expect("Operation should succeed");
let cdf = sketch
.cdf(&split_points, SearchCriteria::Inclusive)
.expect("Operation should succeed");
let pmf_sum: f64 = pmf.iter().sum();
assert_relative_eq!(pmf_sum, 1.0, epsilon = 1e-10);
let mut cumulative = 0.0;
for i in 0..pmf.len() {
cumulative += pmf[i];
assert_relative_eq!(cdf[i], cumulative, epsilon = 1e-10);
}
assert_relative_eq!(cdf[cdf.len() - 1], 1.0, epsilon = 1e-10);
}
#[test]
fn test_rank_matches_sorted_view() {
for &n in &[100u64, 10_000] {
for accuracy in [RankAccuracy::HighRank, RankAccuracy::LowRank] {
let mut sketch: ReqSketch<f64> = ReqSketch::builder()
.rank_accuracy(accuracy)
.build()
.expect("builder");
for i in 0..n {
sketch.update(i as f64);
}
let probes: Vec<f64> = (0..11).map(|i| i as f64 * (n - 1) as f64 / 10.0).collect();
for criteria in [SearchCriteria::Inclusive, SearchCriteria::Exclusive] {
for &v in &probes {
let direct = sketch.rank(&v, criteria).expect("rank");
let via_view = sketch
.sorted_view()
.expect("sorted_view")
.rank_no_interpolation(&v, criteria)
.expect("rank_no_interpolation");
assert_eq!(
direct, via_view,
"rank/sorted_view disagree at n={}, accuracy={:?}, criteria={:?}, v={}",
n, accuracy, criteria, v
);
}
}
}
}
}
#[test]
fn test_iterator_weight_consistency() {
let mut sketch = ReqSketch::new();
for i in 0..1000 {
sketch.update(i as f64);
}
let total_weight: u64 = sketch.iter().map(|(_, weight)| weight).sum();
assert_eq!(total_weight, sketch.len());
for (item, weight) in sketch.iter() {
assert!(weight >= 1);
if let (Some(min), Some(max)) = (sketch.min_item(), sketch.max_item()) {
assert!(item >= *min && item <= *max);
}
}
}
}
mod property_tests {
use super::*;
proptest! {
#[test]
fn prop_quantile_rank_consistency(values in prop::collection::vec(0.0f64..1000.0, 10..100)) {
let mut sketch = ReqSketch::new();
for value in values {
sketch.update(value);
}
if !sketch.is_empty() {
let test_ranks = [0.1, 0.25, 0.5, 0.75, 0.9];
for &rank in &test_ranks {
let quantile = sketch.quantile(rank, SearchCriteria::Inclusive).expect("Operation should succeed");
let computed_rank = sketch.rank(&quantile, SearchCriteria::Inclusive).expect("Operation should succeed");
let base_tolerance = 2.0 / sketch.len() as f64;
if let (Some(min_val), Some(max_val)) = (sketch.min_item(), sketch.max_item()) {
let is_degenerate = (max_val - min_val).abs() < 1e-10;
if is_degenerate {
continue;
}
let quantile_is_extreme = (quantile - min_val).abs() < 1e-10 ||
(quantile - max_val).abs() < 1e-10;
if quantile_is_extreme {
} else {
let tolerance = (base_tolerance + 0.15).max(0.2); assert!((computed_rank - rank).abs() <= tolerance,
"Rank-quantile inconsistency: rank {} -> quantile {} -> rank {}",
rank, quantile, computed_rank);
}
}
}
}
}
#[test]
fn prop_merge_commutativity(
values1 in prop::collection::vec(0.0f64..1000.0, 10..100),
values2 in prop::collection::vec(0.0f64..1000.0, 10..100)
) {
let mut sketch1a = ReqSketch::new();
let mut sketch2a = ReqSketch::new();
let mut sketch1b = ReqSketch::new();
let mut sketch2b = ReqSketch::new();
for value in &values1 {
sketch1a.update(*value);
sketch1b.update(*value);
}
for value in &values2 {
sketch2a.update(*value);
sketch2b.update(*value);
}
sketch1a.merge(&sketch2a).expect("Operation should succeed");
sketch2b.merge(&sketch1b).expect("Operation should succeed");
assert_eq!(sketch1a.len(), sketch2b.len());
assert_eq!(sketch1a.min_item(), sketch2b.min_item());
assert_eq!(sketch1a.max_item(), sketch2b.max_item());
if !sketch1a.is_empty() {
let q1 = sketch1a.quantile(0.5, SearchCriteria::Inclusive).expect("Operation should succeed");
let q2 = sketch2b.quantile(0.5, SearchCriteria::Inclusive).expect("Operation should succeed");
if q1 == q2 {
} else {
let max_val = q1.max(q2);
let min_val = q1.min(q2);
if max_val < 1e-10 {
} else if min_val == 0.0 && max_val > 0.0 {
} else {
let relative_diff = (q1 - q2).abs() / max_val;
assert!(relative_diff < 0.5, "Merge commutativity violated: {} vs {}", q1, q2);
}
}
}
}
#[test]
fn prop_sketch_bounds(values in prop::collection::vec(-1000.0f64..1000.0, 1..1000)) {
let mut sketch = ReqSketch::new();
for value in &values {
sketch.update(*value);
}
if !sketch.is_empty() {
let true_min = values.iter().fold(f64::INFINITY, |a, &b| a.min(b));
let true_max = values.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
assert_eq!(sketch.min_item(), Some(&true_min));
assert_eq!(sketch.max_item(), Some(&true_max));
for &rank in &[0.0, 0.25, 0.5, 0.75, 1.0] {
let quantile = sketch.quantile(rank, SearchCriteria::Inclusive).expect("Operation should succeed");
assert!(quantile >= true_min && quantile <= true_max,
"Quantile {} out of bounds [{}, {}]", quantile, true_min, true_max);
}
}
}
#[test]
fn prop_rank_monotonicity(values in prop::collection::vec(0.0f64..1000.0, 10..100)) {
let mut sketch = ReqSketch::new();
for value in values {
sketch.update(value);
}
if !sketch.is_empty() {
let test_values = [0.0, 100.0, 200.0, 500.0, 800.0, 1000.0];
let mut last_rank = -1.0;
for &value in &test_values {
let rank = sketch.rank(&value, SearchCriteria::Inclusive).expect("Operation should succeed");
assert!(rank >= last_rank, "Ranks not monotonic: {} after {}", rank, last_rank);
assert!((0.0..=1.0).contains(&rank), "Rank {} out of bounds", rank);
last_rank = rank;
}
}
}
}
}
mod stress_tests {
use super::*;
#[test]
#[ignore] fn stress_test_large_dataset() {
let mut sketch = ReqSketch::new();
let n = 10_000_000;
for i in 0..n {
sketch.update(i as f64);
}
assert_eq!(sketch.len(), n);
assert_eq!(sketch.min_item(), Some(&0.0));
assert_eq!(sketch.max_item(), Some(&((n - 1) as f64)));
let median = sketch
.quantile(0.5, SearchCriteria::Inclusive)
.expect("Operation should succeed");
let expected_median = (n / 2) as f64;
let relative_error = (median - expected_median).abs() / expected_median;
assert!(
relative_error < 0.01,
"Large dataset median error too high: {}",
relative_error
);
}
#[test]
fn test_many_merges() {
let mut main_sketch = ReqSketch::new();
for batch in 0..100 {
let mut batch_sketch = ReqSketch::new();
for i in 0..100 {
batch_sketch.update((batch * 100 + i) as f64);
}
main_sketch
.merge(&batch_sketch)
.expect("Operation should succeed");
}
assert_eq!(main_sketch.len(), 10_000);
assert_eq!(main_sketch.min_item(), Some(&0.0));
assert_eq!(main_sketch.max_item(), Some(&9999.0));
let median = main_sketch
.quantile(0.5, SearchCriteria::Inclusive)
.expect("Operation should succeed");
let expected_median = 4999.5; let tolerance = 500.0; assert!(
(median - expected_median).abs() < tolerance,
"Median after many merges: {}, expected: {}, actual error: {}",
median,
expected_median,
(median - expected_median).abs()
);
}
}