use super::*;
#[test]
fn hash_is_deterministic_and_injective() {
use std::collections::HashSet;
let mut seen = HashSet::new();
for k in 0_u64..10_000 {
let h = hash64(k);
assert_eq!(h, hash64(k), "hash must be deterministic for key {k}");
assert!(seen.insert(h), "hash collided on a distinct key {k}");
}
}
#[test]
fn new_allocates_two_to_the_p_registers() -> Result<(), CardinalityError> {
let hll = HyperLogLog::new(10)?;
assert_eq!(
hll.register_count(),
1 << 10,
"precision 10 -> 1024 registers"
);
assert_eq!(hll.precision(), 10, "precision must round-trip");
Ok(())
}
#[test]
fn new_rejects_out_of_range_precision() {
assert_eq!(
HyperLogLog::new(3).err(),
Some(CardinalityError::InvalidPrecision),
"precision 3 must be rejected"
);
assert_eq!(
HyperLogLog::new(19).err(),
Some(CardinalityError::InvalidPrecision),
"precision 19 must be rejected"
);
}
#[test]
fn add_sets_one_register_and_is_idempotent() -> Result<(), CardinalityError> {
let mut hll = HyperLogLog::new(10)?;
hll.add(42);
let nonzero = hll.nonzero_register_count();
assert_eq!(nonzero, 1, "one element must touch exactly one register");
hll.add(42);
assert_eq!(
hll.nonzero_register_count(),
1,
"re-adding the same element must not change any register"
);
Ok(())
}
#[test]
fn empty_estimator_counts_zero() -> Result<(), CardinalityError> {
let hll = HyperLogLog::new(12)?;
let est = hll.estimate();
assert!(est.abs() < 1e-9, "empty estimate should be 0, was {est}");
Ok(())
}
#[test]
fn estimates_distinct_count_within_error_bound() -> Result<(), CardinalityError> {
let p = 14_u8;
let n = 50_000_u64;
let mut hll = HyperLogLog::new(p)?;
for x in 0..n {
hll.add(x);
}
let est = hll.estimate();
let exact = 50_000.0;
let m = f64::from(1_u32 << p);
let std_err = 1.04 / m.sqrt();
let rel = (est - exact).abs() / exact;
assert!(
rel <= 3.0 * std_err,
"relative error {rel} exceeded 3x standard error {std_err} (est {est})"
);
Ok(())
}
#[test]
fn small_cardinality_uses_linear_counting() -> Result<(), CardinalityError> {
let p = 14_u8;
let n = 200_u64;
let hll = HyperLogLog::from_u64_iter(p, 0..n)?;
let est = hll.estimate();
let exact = 200.0;
let m = f64::from(1_u32 << p);
let std_err = 1.04 / m.sqrt();
let rel = (est - exact).abs() / exact;
assert!(
rel <= 3.0 * std_err,
"small-range relative error {rel} exceeded 3x standard error {std_err} (est {est})"
);
Ok(())
}
#[test]
fn from_u64_iter_matches_incremental_add() -> Result<(), CardinalityError> {
let p = 12_u8;
let data: Vec<u64> = (0..5_000).collect();
let built = HyperLogLog::from_u64_iter(p, data.iter().copied())?;
let mut incremental = HyperLogLog::new(p)?;
for &x in &data {
incremental.add(x);
}
assert!(
(built.estimate() - incremental.estimate()).abs() < 1e-9,
"iterator build must match incremental add: {} vs {}",
built.estimate(),
incremental.estimate()
);
Ok(())
}
#[test]
fn estimate_is_order_independent_and_deterministic() -> Result<(), CardinalityError> {
let p = 12_u8;
let a = HyperLogLog::from_u64_iter(p, 0..3_000)?;
let b = HyperLogLog::from_u64_iter(p, (0..3_000).rev())?;
assert_eq!(
a.estimate().to_bits(),
b.estimate().to_bits(),
"estimate must be identical regardless of insertion order"
);
Ok(())
}