Skip to content

Conversation

@itsibitzi
Copy link
Contributor

@itsibitzi itsibitzi commented Jul 24, 2025

Why?

There have been random test failures caused by some invariants not being maintained in the LSB BitVec. This presents two problems.

  1. There seem to be bugs in our code, at least according to the tests.
  2. The tests are random and thus failures are hard to reproduce

How?

To address this I've done two things.

Firstly, I implement a test harness for running tests which require a random number generator (RNG). This harness accepts a lambda function which has a single argument, a RNG. Under normal conditions the RNG is seeded randomly as a typical thread local RNG. The difference occurs when you get a test failure, at which point the harness captures the panic and prints out the RNG seed. You can then rerun your tests with TEST_SEED=... set as an environment variable in order to start the test with the seed that failed. This allows developers to rerun the test and see the failure immediately, a generally useful thing to be able to do :)

E.g. you run some tests with cargo test and get the following error message from a flakey test:

---- diff_count::tests::test_xor_plus_mask stdout ----

thread 'diff_count::tests::test_xor_plus_mask' panicked at crates/geo_filters/src/diff_count.rs:535:21:
assertion `left == right` failed
  left: ~12.37563 (msb: [419, 328, 284, 251, 237, 180, 148, 117, 108, 67, 60, 26], |lsb|: 26)
 right: ~12.37563 (msb: [419, 328, 284, 251, 237, 180, 148, 117, 108, 67, 60, 26], |lsb|: 0)
Test failed! Reproduce with: TEST_SEED=16501082297235867130

You can then re-run with TEST_SEED=16501082297235867130 cargo test and the test will fail reliably.

I rewrote all our tests to use this since it's good to be able to reproduce issues. I did not roll it out to the evaluation code.

Secondly, I fix issue where BitVec was required to be structurally identical rather than just semantically equivalent to pass equality checks. Specifically, an empty BitVec with no blocks is semantically the same as a BitVec with >=1 block where all the bits are zeros, but the PartialEq function didn't consider them the same.

This caused issues when bits in the LSB were toggled directly. An item is pushed, hashed and assigned a bucket. If the bucket is in the MSB range then the LSB is updated and potentially resized. If the bucket is in the LSB range the bit is toggled but the bucket is not resized. This is because we expect most items to toggle bits in the LSB range so not resizing the buffer every time this happen saves us on some performance.

This issue would only happen relatively rarely, given that it needed the last item inserted to toggle the last bit in the LSB before the equality check is performed. Usually there is more than 1 bit in the LSB set before the final item is inserted.

I addressed this issue by changing how PartialEq::eq is implemented to check that the set bits are equivalent using the existing bit_chunks iterator.

There may be some situations where it makes sense to manually trigger a resize (shrink_to_fit?). For example, in the serialization code (PR not yet merged) it would make sense to resize the LSB before serialization to prevent writing empty bytes to disk.

Alternative approaches

For the deterministic RNG I could have used something like proptest, but it felt like too big/powerful a dependency for this limited use case.

For the BitVec stuff....

Rather than make the Eq function check the semantic equivalence we could spend more effort maintaining the structural equivalence of the BitVec.

The first thing I tried was to make toggling the LSB check the size of the vectors and resize if needed - this fixed the issue, but was bad for performance.

I tried changing how the LSB is updated when the MSB has an insert, but this caused quite a few knock on effects due to the fact that the most-significant-bit in the LSB range is truncated when it's resized. Removing this resize code had a load of knock-on issues that, to me, seemed harder to solve.

If anyone feels strongly that the BitVec should do it's comparisons based on the specific state and that the semantic equivalence is not good enough then we can rework this PR.

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.
Copilot AI review requested due to automatic review settings July 24, 2025 20:07
@itsibitzi itsibitzi requested a review from a team as a code owner July 24, 2025 20:07
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR addresses random test failures in the BitVec implementation by introducing a deterministic RNG test harness and fixing a semantic equality bug. The PR implements two main solutions: a test harness that captures and allows reproduction of random test failures using environment variables, and a fix to BitVec's equality comparison to check semantic equivalence rather than structural identity.

  • Implements a deterministic RNG test harness for reproducible random test failures
  • Fixes BitVec equality to compare semantic equivalence instead of structural identity
  • Migrates all existing tests to use the new test harness

Reviewed Changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
crates/geo_filters/src/test_rng.rs New test harness module providing seeded RNG with failure reproduction capability
crates/geo_filters/src/lib.rs Adds test_rng module import for test configuration
crates/geo_filters/src/diff_count/bitvec.rs Implements custom PartialEq for BitVec using semantic comparison via bit_chunks
crates/geo_filters/src/distinct_count.rs Migrates tests to use prng_test_harness
crates/geo_filters/src/diff_count.rs Migrates tests to use prng_test_harness and removes old FIXME comment
crates/geo_filters/src/config/lookup.rs Migrates tests to use prng_test_harness
crates/geo_filters/src/config.rs Migrates test_estimate function to use prng_test_harness

Comment on lines 22 to 24
fn eq(&self, other: &Self) -> bool {
self.bit_chunks().eq(other.bit_chunks())
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

creative solution :)
The main issue I have is that we cannot rely on the BitVec being sufficiently sized and instead have to ensure its size all the time. That's rather ugly IMO

itsibitzi and others added 3 commits July 25, 2025 10:05
@itsibitzi
Copy link
Contributor Author

Following a discussion on slack I've cherry-picked Alex's (better) solution from #73

) -> Self {
// if there are no chunks, we keep the size zero
let num_bits = chunks.peek().map(|_| num_bits).unwrap_or_default();
pub fn from_bit_chunks<I: Iterator<Item = BitChunk>>(chunks: I, num_bits: usize) -> Self {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

did you check whether any callsites are unnecessarily calling peekable?

Copy link
Contributor Author

@itsibitzi itsibitzi Jul 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There weren't any for the BitVec::from_bit_chunks but I did find some for other functions 😅

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is because BitChunksOnes iterator already has peekable iterators internally. They're required for next, but not required externally, so the Peekable wrapper here in the from_bit_chunks function is not needed.

Copy link
Collaborator

@aneubeck aneubeck left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@itsibitzi itsibitzi merged commit 08316ba into main Jul 25, 2025
7 checks passed
@itsibitzi itsibitzi deleted the sc-20250724-geofilter-deterministic-tests branch July 25, 2025 09:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants