//! Tests for the `BloomFilter` data structure. use crate::proto::bloom::{BloomError, BloomFilter, DEFAULT_FILTER_SIZE_BITS, DEFAULT_HASH_COUNT}; use crate::testutil::make_node_addr; #[test] fn test_bloom_filter_new() { let filter = BloomFilter::new(); assert_eq!(filter.num_bits(), DEFAULT_FILTER_SIZE_BITS); assert_eq!(filter.hash_count(), DEFAULT_HASH_COUNT); assert_eq!(filter.count_ones(), 0); assert!(filter.is_empty()); } #[test] fn test_bloom_filter_insert_contains() { let mut filter = BloomFilter::new(); let node1 = make_node_addr(1); let node2 = make_node_addr(2); assert!(!filter.contains(&node1)); assert!(!filter.contains(&node2)); filter.insert(&node1); assert!(filter.contains(&node1)); // node2 might have false positive, but very unlikely with single insert assert!(!filter.is_empty()); } #[test] fn test_bloom_filter_multiple_inserts() { let mut filter = BloomFilter::new(); for i in 0..100 { let node = make_node_addr(i); filter.insert(&node); } // All inserted items should be found for i in 0..100 { let node = make_node_addr(i); assert!(filter.contains(&node), "Node {} not found", i); } // Fill ratio should be reasonable let fill = filter.fill_ratio(); assert!(fill > 0.0 && fill < 0.5, "Unexpected fill ratio: {}", fill); } #[test] fn test_bloom_filter_merge() { let mut filter1 = BloomFilter::new(); let mut filter2 = BloomFilter::new(); let node1 = make_node_addr(1); let node2 = make_node_addr(2); filter1.insert(&node1); filter2.insert(&node2); filter1.merge(&filter2).unwrap(); assert!(filter1.contains(&node1)); assert!(filter1.contains(&node2)); } #[test] fn test_bloom_filter_union() { let mut filter1 = BloomFilter::new(); let mut filter2 = BloomFilter::new(); let node1 = make_node_addr(1); let node2 = make_node_addr(2); filter1.insert(&node1); filter2.insert(&node2); let union = filter1.union(&filter2).unwrap(); assert!(union.contains(&node1)); assert!(union.contains(&node2)); // Original filters unchanged assert!(!filter1.contains(&node2)); assert!(!filter2.contains(&node1)); } #[test] fn test_bloom_filter_clear() { let mut filter = BloomFilter::new(); let node = make_node_addr(1); filter.insert(&node); assert!(!filter.is_empty()); filter.clear(); assert!(filter.is_empty()); assert_eq!(filter.count_ones(), 0); assert!(!filter.contains(&node)); } #[test] fn test_bloom_filter_merge_size_mismatch() { let mut filter1 = BloomFilter::with_params(1024, 7).unwrap(); let filter2 = BloomFilter::with_params(2048, 7).unwrap(); let result = filter1.merge(&filter2); assert!(matches!(result, Err(BloomError::InvalidSize { .. }))); } #[test] fn test_bloom_filter_custom_params() { let filter = BloomFilter::with_params(1024, 5).unwrap(); assert_eq!(filter.num_bits(), 1024); assert_eq!(filter.num_bytes(), 128); assert_eq!(filter.hash_count(), 5); } #[test] fn test_bloom_filter_invalid_params() { // Not byte-aligned (1001 is not divisible by 8) assert!(matches!( BloomFilter::with_params(1001, 7), Err(BloomError::SizeNotByteAligned(1001)) )); // Zero size assert!(matches!( BloomFilter::with_params(0, 7), Err(BloomError::SizeNotByteAligned(0)) )); // Zero hash count assert!(matches!( BloomFilter::with_params(1024, 0), Err(BloomError::ZeroHashCount) )); } #[test] fn test_bloom_filter_from_bytes() { let original = BloomFilter::new(); let bytes = original.as_bytes().to_vec(); let restored = BloomFilter::from_bytes(bytes, original.hash_count()).unwrap(); assert_eq!(original, restored); } #[test] fn test_bloom_filter_estimated_count() { let mut filter = BloomFilter::new(); // Empty filter assert_eq!(filter.estimated_count(f64::INFINITY), Some(0.0)); // Insert some items for i in 0..50 { filter.insert(&make_node_addr(i)); } // Estimate should be reasonably close to 50 let estimate = filter.estimated_count(f64::INFINITY).unwrap(); assert!( estimate > 30.0 && estimate < 100.0, "Unexpected estimate: {}", estimate ); } #[test] fn test_bloom_filter_equality() { let mut filter1 = BloomFilter::new(); let mut filter2 = BloomFilter::new(); assert_eq!(filter1, filter2); filter1.insert(&make_node_addr(1)); assert_ne!(filter1, filter2); filter2.insert(&make_node_addr(1)); assert_eq!(filter1, filter2); } #[test] fn test_bloom_filter_from_bytes_empty() { let result = BloomFilter::from_bytes(vec![], 5); assert!(matches!(result, Err(BloomError::SizeNotByteAligned(0)))); } #[test] fn test_bloom_filter_from_bytes_zero_hash_count() { let result = BloomFilter::from_bytes(vec![0u8; 128], 0); assert!(matches!(result, Err(BloomError::ZeroHashCount))); } #[test] fn test_bloom_filter_from_slice() { let mut original = BloomFilter::new(); original.insert(&make_node_addr(42)); let bytes = original.as_bytes(); let restored = BloomFilter::from_slice(bytes, original.hash_count()).unwrap(); assert_eq!(original, restored); } #[test] fn test_bloom_filter_insert_bytes_contains_bytes() { let mut filter = BloomFilter::new(); let data1 = b"hello world"; let data2 = b"goodbye"; assert!(!filter.contains_bytes(data1)); filter.insert_bytes(data1); assert!(filter.contains_bytes(data1)); assert!(!filter.contains_bytes(data2)); filter.insert_bytes(data2); assert!(filter.contains_bytes(data1)); assert!(filter.contains_bytes(data2)); } #[test] fn test_bloom_filter_estimated_count_saturated() { // Create a small filter with all bits set let bytes = vec![0xFF; 8]; // all bits set let filter = BloomFilter::from_bytes(bytes, 3).unwrap(); // Saturated filter returns None regardless of cap (defense in depth). // Previously returned f64::INFINITY. assert_eq!(filter.estimated_count(f64::INFINITY), None); assert_eq!(filter.estimated_count(0.05), None); } #[test] fn test_bloom_filter_estimated_count_fpr_cap_boundary() { // Cap boundary: FPR = fill^k = 0.05 at k=5 ⇒ fill ≈ 0.5493 // 1KB filter (8192 bits). 560 bytes of 0xFF = 4480 bits set = // fill 0.5469, FPR ≈ 0.04877 — just below cap. // 564 bytes of 0xFF = 4512 bits set = fill 0.5508, FPR ≈ 0.05060 — // just above cap. let mut below = vec![0x00u8; 1024]; below[..560].fill(0xFF); let below_filter = BloomFilter::from_bytes(below, DEFAULT_HASH_COUNT).unwrap(); assert!( below_filter.estimated_count(0.05).is_some(), "fill 0.5469 (FPR ≈ 0.049) must be accepted by cap 0.05" ); let mut above = vec![0x00u8; 1024]; above[..564].fill(0xFF); let above_filter = BloomFilter::from_bytes(above, DEFAULT_HASH_COUNT).unwrap(); assert_eq!( above_filter.estimated_count(0.05), None, "fill 0.5508 (FPR ≈ 0.051) must be rejected by cap 0.05" ); // Same above-cap filter with a looser cap is accepted. assert!( above_filter.estimated_count(0.10).is_some(), "fill 0.5508 (FPR ≈ 0.051) must be accepted by cap 0.10" ); } #[test] fn test_bloom_filter_default() { let default: BloomFilter = Default::default(); let explicit = BloomFilter::new(); assert_eq!(default, explicit); } #[test] fn test_bloom_filter_debug_format() { let mut filter = BloomFilter::new(); let debug = format!("{:?}", filter); assert!(debug.contains("BloomFilter")); assert!(debug.contains("8192")); assert!(debug.contains("hash_count")); // With some entries for i in 0..10 { filter.insert(&make_node_addr(i)); } let debug = format!("{:?}", filter); assert!(debug.contains("fill_ratio")); assert!(debug.contains("est_count")); } #[test] fn test_bloom_filter_bit_indices_match_double_hashing_formula() { use sha2::{Digest, Sha256}; // Independently recompute the documented double-hashing bit indices: // one SHA-256 digest of the input, h1 = bytes[0..8] LE, h2 = bytes[8..16] // LE, then for k in 0..hash_count: (h1 + k*h2) mod num_bits. This pins // bit-identical behavior regardless of the internal implementation. fn expected_indices(data: &[u8], num_bits: usize, hash_count: u8) -> Vec { let digest = Sha256::digest(data); let h1 = u64::from_le_bytes(digest[0..8].try_into().unwrap()); let h2 = u64::from_le_bytes(digest[8..16].try_into().unwrap()); (0..hash_count) .map(|k| { let combined = h1.wrapping_add((k as u64).wrapping_mul(h2)); (combined as usize) % num_bits }) .collect() } fn bit_is_set(filter: &BloomFilter, index: usize) -> bool { let byte = filter.as_bytes()[index / 8]; (byte >> (index % 8)) & 1 == 1 } let configs = [(1024usize, 5u8), (8192usize, 7u8)]; let inputs: [&[u8]; 4] = [b"", b"alpha", b"the quick brown fox", &[0u8, 1, 2, 3, 255]]; for (num_bits, hash_count) in configs { for data in inputs { let mut filter = BloomFilter::with_params(num_bits, hash_count).unwrap(); let expected = expected_indices(data, num_bits, hash_count); filter.insert_bytes(data); // Every expected bit is set. for &idx in &expected { assert!( bit_is_set(&filter, idx), "expected bit {} set for input {:?} (num_bits={}, k={})", idx, data, num_bits, hash_count ); } // No unexpected bits are set: the set-bit count never exceeds the // number of distinct expected indices. let distinct: alloc::collections::BTreeSet = expected.iter().copied().collect(); assert_eq!( filter.count_ones(), distinct.len(), "unexpected bits set for input {:?}", data ); // contains reports the inserted item as present. assert!(filter.contains_bytes(data)); } } // NodeAddr path uses the same formula over its byte view. let node = make_node_addr(7); let mut filter = BloomFilter::with_params(1024, 5).unwrap(); let expected = expected_indices(node.as_bytes(), 1024, 5); filter.insert(&node); for &idx in &expected { assert!(bit_is_set(&filter, idx)); } assert!(filter.contains(&node)); // Spot-check a definitely-absent item is reported absent. let absent = make_node_addr(200); assert!(!filter.contains(&absent)); }