#![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
use flashsieve::NgramBloom;
#[test]
fn insert_all_65536_ngrams_no_false_negative() {
let mut bloom = NgramBloom::new(131_072).unwrap();
for a in 0_u8..=255 {
for b in 0_u8..=255 {
bloom.insert_ngram(a, b);
}
}
for a in 0_u8..=255 {
for b in 0_u8..=255 {
assert!(
bloom.maybe_contains(a, b),
"false negative for ngram ({a}, {b}) after full insertion"
);
}
}
}
#[test]
fn single_insert_single_query() {
let mut bloom = NgramBloom::new(4096).unwrap();
bloom.insert_ngram(0x41, 0x42);
assert!(bloom.maybe_contains(0x41, 0x42));
}
#[test]
fn insert_same_ngram_twice_idempotent() {
let mut bloom = NgramBloom::new(4096).unwrap();
bloom.insert_ngram(0x41, 0x42);
bloom.insert_ngram(0x41, 0x42);
assert!(bloom.maybe_contains(0x41, 0x42));
}
#[test]
fn query_without_insert_may_return_false() {
let bloom = NgramBloom::new(4096).unwrap();
assert!(!bloom.maybe_contains(0x41, 0x42));
}
#[test]
fn ngram_0x00_0x00_round_trip() {
let mut bloom = NgramBloom::new(4096).unwrap();
bloom.insert_ngram(0x00, 0x00);
assert!(bloom.maybe_contains(0x00, 0x00));
}
#[test]
fn ngram_0xff_0xff_round_trip() {
let mut bloom = NgramBloom::new(4096).unwrap();
bloom.insert_ngram(0xFF, 0xFF);
assert!(bloom.maybe_contains(0xFF, 0xFF));
}
#[test]
fn ngram_0x00_0xff_round_trip() {
let mut bloom = NgramBloom::new(4096).unwrap();
bloom.insert_ngram(0x00, 0xFF);
assert!(bloom.maybe_contains(0x00, 0xFF));
}
#[test]
fn ngram_0xff_0x00_round_trip() {
let mut bloom = NgramBloom::new(4096).unwrap();
bloom.insert_ngram(0xFF, 0x00);
assert!(bloom.maybe_contains(0xFF, 0x00));
}
#[test]
fn fully_saturated_bloom_all_queries_true() {
let mut bloom = NgramBloom::new(64).unwrap();
for a in 0_u8..=255 {
bloom.insert_ngram(a, a);
}
let fpr = bloom.estimated_false_positive_rate();
assert!(
fpr > 0.5,
"tiny bloom with 256 insertions should be heavily saturated, FPR: {fpr}"
);
}
#[test]
fn estimated_fpr_zero_for_empty_bloom() {
let bloom = NgramBloom::new(4096).unwrap();
let fpr = bloom.estimated_false_positive_rate();
assert!(
fpr.abs() < f64::EPSILON,
"empty bloom should have zero FPR, got: {fpr}"
);
}
#[test]
fn estimated_fpr_increases_with_insertions() {
let mut bloom = NgramBloom::new(4096).unwrap();
let fpr_0 = bloom.estimated_false_positive_rate();
bloom.insert_ngram(0x41, 0x42);
let fpr_1 = bloom.estimated_false_positive_rate();
assert!(
fpr_1 >= fpr_0,
"FPR must not decrease after insertion: {fpr_0} -> {fpr_1}"
);
}
#[test]
fn estimated_fpr_approaches_1_when_saturated() {
let mut bloom = NgramBloom::new(64).unwrap();
for a in 0_u8..=255 {
for b in 0_u8..16 {
bloom.insert_ngram(a, b);
}
}
let fpr = bloom.estimated_false_positive_rate();
assert!(
fpr > 0.9,
"heavily saturated bloom should have FPR near 1.0, got: {fpr}"
);
}
#[test]
fn maybe_contains_pattern_empty_always_true() {
let bloom = NgramBloom::new(4096).unwrap();
assert!(bloom.maybe_contains_pattern(&[]));
}
#[test]
fn maybe_contains_pattern_single_byte_always_true() {
let bloom = NgramBloom::new(4096).unwrap();
assert!(bloom.maybe_contains_pattern(&[0x41]));
}
#[test]
fn maybe_contains_pattern_two_bytes_round_trip() {
let bloom = NgramBloom::from_block(b"abcdef", 4096).unwrap();
assert!(bloom.maybe_contains_pattern(b"ab"));
assert!(bloom.maybe_contains_pattern(b"cd"));
assert!(bloom.maybe_contains_pattern(b"ef"));
}
#[test]
fn maybe_contains_pattern_not_found() {
let bloom = NgramBloom::from_block(b"abcdef", 4096).unwrap();
assert!(!bloom.maybe_contains_pattern(b"zx"));
}
#[test]
fn maybe_contains_pattern_partial_match_rejected() {
let bloom = NgramBloom::from_block(b"abcdef", 4096).unwrap();
assert!(!bloom.maybe_contains_pattern(b"az"));
}
#[test]
fn zero_bits_rejected() {
let result = NgramBloom::new(0);
assert!(result.is_err());
}
#[test]
fn from_block_zero_bits_rejected() {
let result = NgramBloom::from_block(b"data", 0);
assert!(result.is_err());
}
#[test]
fn from_block_empty_data_produces_empty_bloom() {
let bloom = NgramBloom::from_block(&[], 4096).unwrap();
assert!(!bloom.maybe_contains(0x41, 0x42));
}
#[test]
fn from_block_single_byte_data_produces_empty_bloom() {
let bloom = NgramBloom::from_block(&[0x41], 4096).unwrap();
assert!(!bloom.maybe_contains(0x41, 0x42));
}
#[test]
fn exact_pairs_no_false_positive_for_pair_lookup() {
let mut bloom = NgramBloom::new(4096).unwrap();
bloom.insert_ngram(0x41, 0x42);
assert!(bloom.maybe_contains(0x41, 0x42));
assert!(!bloom.maybe_contains(0x42, 0x41));
assert!(!bloom.maybe_contains(0x00, 0x00));
}
#[test]
fn below_exact_pair_threshold_uses_bloom_only() {
let mut bloom = NgramBloom::new(2048).unwrap();
bloom.insert_ngram(0x41, 0x42);
assert!(bloom.maybe_contains(0x41, 0x42));
}
#[test]
fn clone_preserves_exact_pairs() {
let mut bloom = NgramBloom::new(4096).unwrap();
bloom.insert_ngram(0x41, 0x42);
let cloned = bloom.clone();
assert!(cloned.maybe_contains(0x41, 0x42));
assert!(!cloned.maybe_contains(0x43, 0x44));
}