Skip to content

Commit 33fabbd

Browse files
committed
chore: add proptest for newly introduced bitmap functions
This commit introduced [proptests](https://docs.rs/proptest) for newly introduced: - bitmap_contains - bitmap_min - bitmap_max - bitmap_has_any - bitmap_has_all Each compares the result against RoaringTreemap as ground truth. Strategy design inspired by roaring as: - Store (random bits) - Container (50% Array, 50% Bitmap) - Bitmap (0 to 16 containers) - Tree (0 to 16 Bitmaps) - Serialization (80% Hybrid, 10% Legacy, 10% Empty)
1 parent 8db2099 commit 33fabbd

3 files changed

Lines changed: 195 additions & 0 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/common/io/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ wkt = { workspace = true }
3434
[dev-dependencies]
3535
aho-corasick = { workspace = true }
3636
anyhow = { workspace = true }
37+
proptest = { workspace = true }
3738
rand = { workspace = true }
3839
rmp-serde = { workspace = true }
3940

src/common/io/src/bitmap.rs

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2131,4 +2131,197 @@ mod tests {
21312131
assert_eq!(actual, expected, "bitmap_has_all: fixture={name}");
21322132
});
21332133
}
2134+
2135+
/// proptests for bitmap functions:
2136+
///
2137+
/// - [x] bitmap_contains
2138+
/// - [x] bitmap_min
2139+
/// - [x] bitmap_max
2140+
/// - [x] bitmap_has_any
2141+
/// - [x] bitmap_has_all
2142+
///
2143+
/// According to comment in src/common/column/tests/it/bitmap/assign_ops.rs,
2144+
/// following prop tests are ignored when using `miri`.
2145+
#[cfg_attr(miri, ignore)]
2146+
mod proptests {
2147+
use proptest::bits::BitSetLike;
2148+
use proptest::bits::SampledBitSetStrategy;
2149+
use proptest::collection::SizeRange;
2150+
use proptest::collection::btree_map;
2151+
use proptest::prelude::*;
2152+
use roaring::RoaringBitmap;
2153+
2154+
use super::*;
2155+
2156+
/// The random bits strategy.
2157+
///
2158+
/// Generate random bits with [`Store::sampled`].
2159+
#[derive(Clone, Debug)]
2160+
struct Store(Vec<u16>);
2161+
2162+
impl Store {
2163+
fn sampled(
2164+
size: impl Into<SizeRange>,
2165+
bits: impl Into<SizeRange>,
2166+
) -> SampledBitSetStrategy<Self> {
2167+
SampledBitSetStrategy::new(size.into(), bits.into())
2168+
}
2169+
}
2170+
2171+
/// Implement BitSetLike as required by SampledBitSetStrategy.
2172+
impl BitSetLike for Store {
2173+
fn new_bitset(max: usize) -> Self {
2174+
assert!(max <= u16::MAX as usize + 1);
2175+
Store(Vec::new())
2176+
}
2177+
fn len(&self) -> usize {
2178+
u16::MAX as usize + 1
2179+
}
2180+
fn test(&self, bit: usize) -> bool {
2181+
self.0.binary_search(&(bit as u16)).is_ok()
2182+
}
2183+
fn set(&mut self, bit: usize) {
2184+
let v = bit as u16;
2185+
if let Err(pos) = self.0.binary_search(&v) {
2186+
self.0.insert(pos, v);
2187+
}
2188+
}
2189+
fn clear(&mut self, bit: usize) {
2190+
let v = bit as u16;
2191+
if let Ok(pos) = self.0.binary_search(&v) {
2192+
self.0.remove(pos);
2193+
}
2194+
}
2195+
fn count(&self) -> usize {
2196+
self.0.len()
2197+
}
2198+
}
2199+
2200+
/// The container strategy.
2201+
///
2202+
/// Generates:
2203+
/// 50% array container with 1 to 4096 values
2204+
/// 50% bitmap container with 4097 to 65536 values
2205+
fn container_strategy() -> impl Strategy<Value = Vec<u16>> {
2206+
prop_oneof![
2207+
Store::sampled(1..=4096, ..=u16::MAX as usize).prop_map(|bs| bs.0),
2208+
Store::sampled(4097..u16::MAX as usize, ..=u16::MAX as usize).prop_map(|bs| bs.0),
2209+
]
2210+
}
2211+
2212+
/// The RoaringBitmap strategy.
2213+
///
2214+
/// Generate 0 to 16 containers, with containers from [`container_strategy`].
2215+
fn bitmap_strategy() -> impl Strategy<Value = RoaringBitmap> {
2216+
btree_map(0u16..=16, container_strategy(), 0usize..=16).prop_map(|map| {
2217+
let mut bitmap = RoaringBitmap::new();
2218+
for (key, values) in map {
2219+
for v in values {
2220+
bitmap.insert((key as u32) << 16 | v as u32);
2221+
}
2222+
}
2223+
bitmap
2224+
})
2225+
}
2226+
2227+
/// The RoaringTreemap strategy.
2228+
///
2229+
/// Generate 0 to 16 RoaringBitmaps, each generated by [`bitmap_strategy`].
2230+
fn tree_strategy() -> impl Strategy<Value = RoaringTreemap> {
2231+
btree_map(0u32..=16, bitmap_strategy(), 0usize..=16).prop_map(|map| {
2232+
let mut treemap = RoaringTreemap::new();
2233+
for (key, bitmap) in map {
2234+
if !bitmap.is_empty() {
2235+
for v in bitmap.iter() {
2236+
treemap.insert((key as u64) << 32 | v as u64);
2237+
}
2238+
}
2239+
}
2240+
treemap
2241+
})
2242+
}
2243+
2244+
/// The serialization strategy.
2245+
///
2246+
/// Serialize RoaringTreemap from [`tree_strategy`] into `Vec<u8>`:
2247+
/// 80% HybridBitmap
2248+
/// 10% Legacy
2249+
/// 10% Empty
2250+
fn serialization_strategy() -> impl Strategy<Value = (Vec<u8>, RoaringTreemap)> {
2251+
prop_oneof![
2252+
8 => tree_strategy().prop_map(|tree| {
2253+
let bm = HybridBitmap::from_iter(tree.iter());
2254+
let mut buf = Vec::new();
2255+
bm.serialize_into(&mut buf).unwrap();
2256+
(buf, tree)
2257+
}),
2258+
1 => tree_strategy().prop_map(|tree| {
2259+
let mut buf = Vec::new();
2260+
tree.serialize_into(&mut buf).unwrap();
2261+
(buf, tree)
2262+
}),
2263+
1 => Just((Vec::new(), RoaringTreemap::new())),
2264+
]
2265+
}
2266+
2267+
/// The probe strategy, picks random value for bitmap_contains to probe.
2268+
///
2269+
/// 50% hit: probe = random value from tree (via nth)
2270+
/// 50% miss: probe = random u64
2271+
fn probe_strategy() -> impl Strategy<Value = (Vec<u8>, RoaringTreemap, u64)> {
2272+
(serialization_strategy(), any::<bool>(), any::<u64>()).prop_map(
2273+
|((buf, tree), is_hit, random_value)| {
2274+
let probe = if is_hit && !tree.is_empty() {
2275+
let idx = (random_value % tree.len()) as usize;
2276+
tree.iter().nth(idx).unwrap_or(random_value)
2277+
} else {
2278+
random_value
2279+
};
2280+
(buf, tree, probe)
2281+
},
2282+
)
2283+
}
2284+
2285+
proptest! {
2286+
// Make the test run faster by limiting the number of cases, running 32 cases
2287+
// took ~16 seconds in release, and ~160 in debug.
2288+
// One can override this by setting the `PROPTEST_CASES` environment variable.
2289+
#![proptest_config(ProptestConfig::with_cases(32))]
2290+
2291+
#[test]
2292+
fn prop_bitmap_contains((buf, tree, value) in probe_strategy()) {
2293+
let expected = tree.contains(value);
2294+
let actual = bitmap_contains(&buf, value).unwrap();
2295+
assert_eq!(actual, expected);
2296+
}
2297+
2298+
#[test]
2299+
fn prop_bitmap_min((buf, tree) in serialization_strategy()) {
2300+
assert_eq!(bitmap_min(&buf).unwrap(), tree.min());
2301+
}
2302+
2303+
#[test]
2304+
fn prop_bitmap_max((buf, tree) in serialization_strategy()) {
2305+
assert_eq!(bitmap_max(&buf).unwrap(), tree.max());
2306+
}
2307+
2308+
#[test]
2309+
fn prop_bitmap_has_any(
2310+
((lhs_buf, lhs_tree), (rhs_buf, rhs_tree)) in (serialization_strategy(), serialization_strategy())
2311+
) {
2312+
let expected = !(lhs_tree & rhs_tree).is_empty();
2313+
let actual = bitmap_has_any(&lhs_buf, &rhs_buf).unwrap();
2314+
assert_eq!(actual, expected);
2315+
}
2316+
2317+
#[test]
2318+
fn prop_bitmap_has_all(
2319+
((lhs_buf, lhs_tree), (rhs_buf, rhs_tree)) in (serialization_strategy(), serialization_strategy())
2320+
) {
2321+
let expected = lhs_tree.is_superset(&rhs_tree);
2322+
let actual = bitmap_has_all(&lhs_buf, &rhs_buf).unwrap();
2323+
assert_eq!(actual, expected);
2324+
}
2325+
}
2326+
}
21342327
}

0 commit comments

Comments
 (0)