Skip to content

Commit 1013140

Browse files
committed
Add test harness for deterministic RNG in tests
Fix issue where BitVec was required to be structurally identical rather than just semantically equivalent. This caused issues when bits in the LSB were toggled. This adds a test harness that provides seeded random number generators for tests, making failures reproducible via TEST_SEED env var.
1 parent 6a72711 commit 1013140

File tree

4 files changed

+65
-26
lines changed

4 files changed

+65
-26
lines changed

crates/geo_filters/src/diff_count.rs

Lines changed: 28 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -95,16 +95,14 @@ impl<C: GeoConfig<Diff>> GeoDiffCount<'_, C> {
9595
/// having to construct another iterator with the remaining `BitChunk`s.
9696
fn from_bit_chunks<I: Iterator<Item = BitChunk>>(config: C, chunks: I) -> Self {
9797
let mut ones = iter_ones::<C::BucketType, _>(chunks.peekable());
98-
9998
let mut msb = Vec::default();
10099
take_ref(&mut ones, config.max_msb_len() - 1).for_each(|bucket| {
101100
msb.push(bucket);
102101
});
103102
let smallest_msb = ones
104103
.next()
105-
.map(|bucket| {
106-
msb.push(bucket);
107-
bucket
104+
.inspect(|bucket| {
105+
msb.push(*bucket);
108106
})
109107
.unwrap_or_default();
110108

@@ -365,6 +363,7 @@ mod tests {
365363
use crate::{
366364
build_hasher::UnstableDefaultBuildHasher,
367365
config::{iter_ones, tests::test_estimate, FixedConfig},
366+
test_rng::prng_test_harness,
368367
};
369368

370369
use super::*;
@@ -517,28 +516,32 @@ mod tests {
517516

518517
#[test]
519518
fn test_xor_plus_mask() {
520-
let mut rnd = rand::rngs::StdRng::from_os_rng();
521-
let mask_size = 12;
522-
let mask = 0b100001100000;
523-
let mut a = GeoDiffCount7::default();
524-
for _ in 0..10000 {
525-
a.xor_bit(a.config.hash_to_bucket(rnd.next_u64()));
526-
}
527-
let mut expected = GeoDiffCount7::default();
528-
let mut b = a.clone();
529519
for _ in 0..1000 {
530-
let hash = rnd.next_u64();
531-
b.xor_bit(b.config.hash_to_bucket(hash));
532-
expected.xor_bit(expected.config.hash_to_bucket(hash));
533-
assert_eq!(expected, xor(&a, &b));
534-
535-
let masked_a = masked(&a, mask, mask_size);
536-
let masked_b = masked(&b, mask, mask_size);
537-
let masked_expected = masked(&expected, mask, mask_size);
538-
// FIXME: test failed once with:
539-
// left: ~12.37563 (msb: [390, 334, 263, 242, 222, 215, 164, 148, 100, 97, 66, 36], |lsb|: 36)
540-
// right: ~12.37563 (msb: [390, 334, 263, 242, 222, 215, 164, 148, 100, 97, 66, 36], |lsb|: 0)
541-
assert_eq!(masked_expected, xor(&masked_a, &masked_b));
520+
prng_test_harness(|mut rng| {
521+
let mask_size = 12;
522+
let mask = 0b100001100000;
523+
let mut a = GeoDiffCount7::default();
524+
for _ in 0..10000 {
525+
a.xor_bit(a.config.hash_to_bucket(rng.next_u64()));
526+
}
527+
let mut expected = GeoDiffCount7::default();
528+
let mut b = a.clone();
529+
for _ in 0..1000 {
530+
let hash = rng.next_u64();
531+
b.xor_bit(b.config.hash_to_bucket(hash));
532+
expected.xor_bit(expected.config.hash_to_bucket(hash));
533+
534+
println!("a -> {:?}", a);
535+
println!("b -> {:?}", b);
536+
println!("exp -> {:?}", expected);
537+
538+
assert_eq!(expected, xor(&a, &b));
539+
let masked_a = masked(&a, mask, mask_size);
540+
let masked_b = masked(&b, mask, mask_size);
541+
let masked_expected = masked(&expected, mask, mask_size);
542+
assert_eq!(masked_expected, xor(&masked_a, &masked_b));
543+
}
544+
});
542545
}
543546
}
544547

crates/geo_filters/src/diff_count/bitvec.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,20 @@ use crate::config::BITS_PER_BLOCK;
1212
/// bit consumes 1 byte). It only implements the minimum number of operations that we need for our
1313
/// GeoDiffCount implementation. In particular it supports xor-ing of two bit vectors and
1414
/// iterating through one bits.
15-
#[derive(Clone, Default, Debug, PartialEq, Eq)]
15+
#[derive(Clone, Default, Debug)]
1616
pub(crate) struct BitVec<'a> {
1717
num_bits: usize,
1818
blocks: Cow<'a, [u64]>,
1919
}
2020

21+
impl PartialEq for BitVec<'_> {
22+
fn eq(&self, other: &Self) -> bool {
23+
self.bit_chunks().eq(other.bit_chunks())
24+
}
25+
}
26+
27+
impl Eq for BitVec<'_> {}
28+
2129
impl Ord for BitVec<'_> {
2230
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
2331
match self.num_bits.cmp(&other.num_bits) {
@@ -93,6 +101,8 @@ impl BitVec<'_> {
93101
assert!(index < self.num_bits);
94102
let (block_idx, bit_idx) = index.into_index_and_bit();
95103
self.blocks.to_mut()[block_idx] ^= bit_idx.into_block();
104+
105+
if self.blocks.to_mut()[block_idx] & bit_idx.into_block() == 0 {}
96106
}
97107

98108
/// Returns an iterator over all blocks in reverse order.

crates/geo_filters/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ pub mod diff_count;
1313
pub mod distinct_count;
1414
#[cfg(feature = "evaluation")]
1515
pub mod evaluation;
16+
#[cfg(test)]
17+
mod test_rng;
1618

1719
use std::hash::Hash;
1820

crates/geo_filters/src/test_rng.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
use std::panic::{catch_unwind, resume_unwind, UnwindSafe};
2+
3+
use rand::{rngs::StdRng, SeedableRng as _};
4+
5+
/// Provides a seeded random number generator to tests which require some
6+
/// degree of randomization. If the test panics the harness will print the
7+
/// seed used for that run. You can then pass in this seed using the `TEST_SEED`
8+
/// environment variable when running your tests.
9+
pub fn prng_test_harness<F>(test_fn: F)
10+
where
11+
F: Fn(StdRng) -> () + UnwindSafe,
12+
{
13+
let seed = std::env::var("TEST_SEED")
14+
.map(|s| s.parse::<u64>().expect("Parse TEST_SEED to u64"))
15+
.unwrap_or_else(|_| rand::random());
16+
let rng = StdRng::seed_from_u64(seed);
17+
let maybe_panic = catch_unwind(move || {
18+
test_fn(rng);
19+
});
20+
if let Err(panic_info) = maybe_panic {
21+
eprintln!("Test failed! Reproduce with: TEST_SEED={}", seed);
22+
resume_unwind(panic_info);
23+
}
24+
}

0 commit comments

Comments
 (0)