use super::*;
use std::sync::Arc;
#[test]
#[cfg_attr(miri, ignore)]
fn test_stress_fuzz_strings() {
use rand::{RngExt, SeedableRng};
use rustc_hash::FxHashSet;
let mut rng = rand::rngs::StdRng::seed_from_u64(12345);
let mut q = QuaminaBuilder::new()
.with_arena_byte_budget(100 * 1024 * 1024)
.build()
.unwrap();
let mut pattern_names: Vec<String> = Vec::new();
let mut used: FxHashSet<String> = FxHashSet::default();
let chars = b"abcdefghijklmnopqrstuvwxyz";
let str_len = 12;
for _ in 0..10_000 {
let s: String = (0..str_len)
.map(|_| chars[rng.random_range(0..chars.len())] as char)
.collect();
pattern_names.push(s.clone());
used.insert(s);
}
for pname in &pattern_names {
let pattern = format!(r#"{{"a": ["{pname}"]}}"#);
q.add_pattern(pname.clone(), &pattern)
.expect("addPattern failed");
}
for pname in &pattern_names {
let event = format!(r#"{{"a": "{pname}"}}"#);
assert_matches!(q, event, vec![pname.clone()]);
}
let mut should_not_count = 0;
while should_not_count < 10_000 {
let s: String = (0..str_len)
.map(|_| chars[rng.random_range(0..chars.len())] as char)
.collect();
if used.contains(&s) {
continue;
}
should_not_count += 1;
let event = format!(r#"{{"a": "{s}"}}"#);
assert_no_match!(q, event);
}
}
#[test]
#[cfg_attr(miri, ignore)]
fn test_stress_fuzz_numbers() {
use rand::{RngExt, SeedableRng};
use rustc_hash::FxHashSet;
let mut rng = rand::rngs::StdRng::seed_from_u64(98543);
let mut q = QuaminaBuilder::new()
.with_arena_byte_budget(100 * 1024 * 1024)
.build()
.unwrap();
let mut pattern_names: Vec<i64> = Vec::new();
let mut used: FxHashSet<i64> = FxHashSet::default();
for _ in 0..10_000 {
let n: i64 = rng.random();
pattern_names.push(n);
used.insert(n);
}
for pname in &pattern_names {
let pattern = format!(r#"{{"a": [{pname}]}}"#);
q.add_pattern(pname.to_string(), &pattern)
.expect("addPattern failed");
}
for pname in &pattern_names {
let event = format!(r#"{{"a": {pname}}}"#);
assert_matches!(q, event, vec![pname.to_string()]);
}
let mut should_not_count = 0;
while should_not_count < 10_000 {
let n: i64 = rng.random_range(0..1_000_000);
if used.contains(&n) {
continue;
}
should_not_count += 1;
let event = format!(r#"{{"a": {n}}}"#);
assert_no_match!(q, event);
}
}
#[test]
#[cfg_attr(miri, ignore)]
fn test_stress_citylots2_operators() {
use std::fs;
use std::path::Path;
let citylots_path = Path::new("testdata/citylots2.json");
if !citylots_path.exists() {
eprintln!("Skipping citylots test - testdata/citylots2.json not found");
return;
}
let data = fs::read_to_string(citylots_path).expect("Failed to read citylots2.json");
let q = q!(
"prefix_143" => r#"{"properties": {"BLKLOT": [{"prefix": "143"}]}}"#,
"suffix_218" => r#"{"properties": {"BLKLOT": [{"suffix": "218"}]}}"#,
"wildcard_0" => r#"{"properties": {"BLKLOT": [{"wildcard": "*0*"}]}}"#
);
let _matches = q.matches_for_event(data.as_bytes());
}
#[test]
fn test_arc_snapshot_isolation() {
let mut q = Quamina::<String>::new();
q.add_pattern("p1".to_string(), r#"{"status": ["active"]}"#)
.unwrap();
let q_arc = Arc::new(q);
let matches = q_arc.matches_for_event(br#"{"status": "active"}"#).unwrap();
assert_eq!(matches, vec!["p1".to_string()]);
let mut q_clone = (*q_arc).clone();
q_clone
.add_pattern("p2".to_string(), r#"{"status": ["pending"]}"#)
.unwrap();
let matches = q_arc
.matches_for_event(br#"{"status": "pending"}"#)
.unwrap();
assert!(matches.is_empty());
let matches = q_clone
.matches_for_event(br#"{"status": "pending"}"#)
.unwrap();
assert_eq!(matches, vec!["p2".to_string()]);
}
#[test]
fn test_concurrent_miri_friendly() {
let mut q = Quamina::<String>::new();
q.add_pattern("p1".to_string(), r#"{"x": [1]}"#).unwrap();
q.add_pattern("p2".to_string(), r#"{"x": [2]}"#).unwrap();
let q_arc = Arc::new(q);
for i in 1..=5 {
let q_ref = Arc::clone(&q_arc);
let event = format!(r#"{{"x": {}}}"#, i % 2 + 1);
let matches = q_ref.matches_for_event(event.as_bytes()).unwrap();
assert!(!matches.is_empty());
}
}
#[test]
#[cfg_attr(miri, ignore)]
fn test_arc_concurrent_read_write() {
use std::thread;
let mut q = Quamina::<String>::new();
q.add_pattern("p1".to_string(), r#"{"status": ["active"]}"#)
.unwrap();
let q_arc = Arc::new(q);
let mut handles = vec![];
for i in 0..4 {
let q_clone = Arc::clone(&q_arc);
let handle = thread::spawn(move || {
for _ in 0..100 {
let matches = q_clone
.matches_for_event(br#"{"status": "active"}"#)
.unwrap();
assert!(matches.contains(&"p1".to_string()), "Thread {i} failed");
}
});
handles.push(handle);
}
for handle in handles {
handle.join().expect("Thread panicked");
}
}
#[test]
#[cfg_attr(miri, ignore)]
fn test_arc_pattern_lifecycle() {
use std::thread;
let mut q = Quamina::<String>::new();
for i in 0..10 {
q.add_pattern(format!("p{i}"), &format!(r#"{{"x": [{i}]}}"#))
.unwrap();
}
let q_arc = Arc::new(q);
let mut handles = vec![];
for _ in 0..4 {
let q_clone = Arc::clone(&q_arc);
let handle = thread::spawn(move || {
for i in 0..10 {
let event = format!(r#"{{"x": {i}}}"#);
let matches = q_clone.matches_for_event(event.as_bytes()).unwrap();
assert_eq!(matches.len(), 1);
}
});
handles.push(handle);
}
for handle in handles {
handle.join().expect("Thread panicked");
}
}
#[test]
fn test_utf16_surrogate_pairs() {
let q = q!("p1" => r#"{"emoji": ["😀💋😺"]}"#);
let event = r#"{"emoji": "\ud83d\ude00\ud83d\udc8b\ud83d\ude3a"}"#;
assert_matches!(
q,
event,
vec!["p1"],
"Multiple surrogate pairs should decode correctly"
);
}
#[test]
fn test_json_escape_all_eight() {
let mut q = Quamina::new();
q.add_pattern("p1", r#"{"x": ["hello\"world"]}"#).unwrap();
assert_matches!(
q,
r#"{"x": "hello\"world"}"#,
vec!["p1"],
"Quote escape should match"
);
q.add_pattern("p2", r#"{"x": ["a/b"]}"#).unwrap();
assert_matches!(
q,
r#"{"x": "a\/b"}"#,
vec!["p2"],
"Forward slash escape should match"
);
q.add_pattern("p3", r#"{"x": ["a\nb"]}"#).unwrap();
assert_matches!(
q,
r#"{"x": "a\nb"}"#,
vec!["p3"],
"Newline escape should match"
);
q.add_pattern("p4", r#"{"x": ["a\tb"]}"#).unwrap();
assert_matches!(q, r#"{"x": "a\tb"}"#, vec!["p4"], "Tab escape should match");
q.add_pattern("p5", r#"{"x": ["a\rb"]}"#).unwrap();
assert_matches!(
q,
r#"{"x": "a\rb"}"#,
vec!["p5"],
"Carriage return escape should match"
);
}
#[test]
fn test_unicode_member_names() {
let q = q!("p1" => r#"{"日本語": ["はい"]}"#);
let event = r#"{"日本語": "はい"}"#;
assert_matches!(q, event, vec!["p1"], "Unicode field names should work");
}
#[test]
fn test_unicode_field_names() {
let q = q!("p1" => r#"{"field": ["value"]}"#);
let event = r#"{"\u0066ield": "value"}"#; assert_matches!(
q,
event,
vec!["p1"],
"Unicode escape in field name should work"
);
}
#[test]
fn test_numbits_through_numeric_matching() {
let q = q!("p1" => r#"{"x": [{"numeric": ["=", 42]}]}"#);
for event in [r#"{"x": 42}"#, r#"{"x": 42.0}"#, r#"{"x": 4.2e1}"#] {
assert_matches!(q, event, vec!["p1"]);
}
assert_no_match!(q, r#"{"x": 43}"#);
}
#[test]
fn test_numbits_ordering_through_range() {
let q = q!("p1" => r#"{"x": [{"numeric": [">=", -100, "<=", 100]}]}"#);
assert_matches!(q, r#"{"x": -100}"#, vec!["p1"], "-100 should match");
assert_matches!(q, r#"{"x": 0}"#, vec!["p1"], "0 should match");
assert_matches!(q, r#"{"x": 100}"#, vec!["p1"], "100 should match");
assert_no_match!(q, r#"{"x": -101}"#, "-101 should not match");
assert_no_match!(q, r#"{"x": 101}"#, "101 should not match");
}
#[test]
#[cfg_attr(miri, ignore)]
fn test_memory_cleanup_miri_friendly() {
let mut q = Quamina::<String>::new();
for i in 0..5 {
q.add_pattern(format!("p{i}"), &format!(r#"{{"x": [{i}]}}"#))
.unwrap();
}
for i in 0..3 {
q.delete_patterns(&format!("p{i}")).unwrap();
}
let purged = q.rebuild();
assert_eq!(purged, 3, "Should have purged 3 patterns");
assert_matches!(q, r#"{"x": 3}"#, vec!["p3".to_string()]);
}
#[test]
#[cfg(miri)]
fn test_memory_cleanup_miri_minimal() {
let mut q = Quamina::<String>::new();
q.add_pattern("keep".to_string(), r#"{"x": ["a"]}"#)
.unwrap();
q.add_pattern("del".to_string(), r#"{"x": ["b"]}"#).unwrap();
q.delete_patterns(&"del".to_string()).unwrap();
let purged = q.rebuild();
assert_eq!(purged, 1, "Should have purged 1 pattern");
assert_matches!(q, r#"{"x": "a"}"#, vec!["keep".to_string()]);
assert_no_match!(q, r#"{"x": "b"}"#, "Deleted pattern should not match");
}
#[test]
#[cfg_attr(miri, ignore)]
fn test_arc_memory_cleanup() {
let mut q = Quamina::<String>::new();
for i in 0..100 {
q.add_pattern(format!("p{i}"), &format!(r#"{{"x": [{i}]}}"#))
.unwrap();
}
let q_arc = Arc::new(q);
for _ in 0..10 {
let q_clone = Arc::clone(&q_arc);
let matches = q_clone.matches_for_event(br#"{"x": 50}"#).unwrap();
assert_eq!(matches, vec!["p50".to_string()]);
}
let matches = q_arc.matches_for_event(br#"{"x": 99}"#).unwrap();
assert_eq!(matches, vec!["p99".to_string()]);
}
#[test]
#[cfg_attr(miri, ignore)]
fn test_concurrent_citylots_stress() {
use std::thread;
let mut q = Quamina::<String>::new();
q.add_pattern("exact".to_string(), r#"{"x": ["foo"]}"#)
.unwrap();
q.add_pattern("prefix".to_string(), r#"{"x": [{"prefix": "bar"}]}"#)
.unwrap();
q.add_pattern("suffix".to_string(), r#"{"x": [{"suffix": "baz"}]}"#)
.unwrap();
q.add_pattern("wildcard".to_string(), r#"{"x": [{"wildcard": "*qux*"}]}"#)
.unwrap();
let q_arc = Arc::new(q);
let mut handles = vec![];
for _ in 0..4 {
let q_clone = Arc::clone(&q_arc);
let handle = thread::spawn(move || {
for i in 0..100 {
let event = match i % 4 {
0 => r#"{"x": "foo"}"#,
1 => r#"{"x": "barxyz"}"#,
2 => r#"{"x": "xyzbaz"}"#,
_ => r#"{"x": "abcquxdef"}"#,
};
let matches = q_clone.matches_for_event(event.as_bytes()).unwrap();
assert!(!matches.is_empty());
}
});
handles.push(handle);
}
for handle in handles {
handle.join().expect("Thread panicked");
}
}
fn verify_bulk_add_correctness(count: usize) {
let mut q = Quamina::new();
for i in 0..count {
let pattern = format!(r#"{{"field{i}": ["value{i}"]}}"#);
q.add_pattern(format!("p{i}"), &pattern).unwrap();
}
assert_eq!(q.pattern_count(), count);
for i in 0..count {
let event = format!(r#"{{"field{i}": "value{i}"}}"#);
assert_matches!(
q,
event,
vec![format!("p{}", i)],
format!("Pattern {} should match", i)
);
}
}
#[test]
#[cfg_attr(miri, ignore)]
fn test_bulk_add_correctness() {
verify_bulk_add_correctness(50);
}
#[test]
#[cfg(miri)]
fn test_bulk_add_correctness_miri_friendly() {
verify_bulk_add_correctness(5);
}
#[test]
fn test_multiple_patterns_same_id_comprehensive() {
let q = q!(
"x" => r#"{"x": ["a"]}"#,
"x" => r#"{"x": [1]}"#,
"x" => r#"{"x": [{"prefix": "b"}]}"#
);
assert_matches!(q, r#"{"x": "a"}"#, vec!["x"], "string 'a' should match");
assert_matches!(q, r#"{"x": 1}"#, vec!["x"], "number 1 should match");
assert_matches!(q, r#"{"x": "bcd"}"#, vec!["x"], "prefix 'b' should match");
assert_no_match!(q, r#"{"x": "z"}"#, "unrelated value should not match");
}
#[test]
fn test_invalid_utf8_dot_rejection() {
let q = q!("p1" => r#"{"a": [1]}"#);
let invalid_json = b"{\"a\xF0\x28\x8C\x28\": 1}";
let result = q.matches_for_event(invalid_json);
if let Ok(matches) = result {
assert!(matches.is_empty() || matches == vec!["p1"]);
} else {
}
}
#[test]
fn test_unicode_escape_multiple_emojis() {
let q = q!("p1" => r#"{"emojis": ["😀💋😺"]}"#);
let event = r#"{"emojis": "\ud83d\ude00\ud83d\udc8b\ud83d\ude3a"}"#;
assert_matches!(
q,
event,
vec!["p1"],
"Multiple surrogate pairs should decode correctly"
);
}
#[test]
fn test_unicode_escape_mixed_codepoints() {
let q = q!("p1" => r#"{"mixed": ["Ж💋中"]}"#);
let event = r#"{"mixed": "\u0416\ud83d\udc8b\u4e2d"}"#;
assert_matches!(q, event, vec!["p1"], "Mixed codepoints should decode");
let q2 = q!("p2" => r#"{"mixed": ["x💋y"]}"#);
let event2 = r#"{"mixed": "\u0078\ud83d\udc8b\u0079"}"#;
assert_matches!(q2, event2, vec!["p2"], "ASCII + surrogate should decode");
}
#[test]
fn test_unicode_escape_standard_escapes() {
let mut q = Quamina::new();
q.add_pattern("newline", r#"{"text": ["hello\nworld"]}"#)
.unwrap();
assert_matches!(
q,
r#"{"text": "hello\nworld"}"#,
vec!["newline"],
"Newline escape should match"
);
q.add_pattern("tab", r#"{"text": ["hello\tworld"]}"#)
.unwrap();
assert_has_match!(
q,
r#"{"text": "hello\tworld"}"#,
"tab",
"Tab escape should match"
);
q.add_pattern("backslash", r#"{"text": ["hello\\world"]}"#)
.unwrap();
assert_has_match!(
q,
r#"{"text": "hello\\world"}"#,
"backslash",
"Backslash escape should match"
);
}
#[test]
fn test_multiple_shellstyle_citylots_patterns() {
let q = q!(
"pattern_143" => r#"{"x": [{"shellstyle": "143*"}]}"#,
"pattern_2017" => r#"{"x": [{"shellstyle": "2*0*1*7"}]}"#,
"pattern_218" => r#"{"x": [{"shellstyle": "*218"}]}"#,
"pattern_352" => r#"{"x": [{"shellstyle": "3*5*2"}]}"#,
"pattern_vail" => r#"{"x": [{"shellstyle": "VA*IL"}]}"#
);
let test_cases: Vec<(&str, Vec<&str>)> = vec![
("1430022", vec!["pattern_143"]), ("2607117", vec!["pattern_2017"]), ("2607218", vec!["pattern_218"]), ("3745012", vec!["pattern_352"]), ("VACSTWIL", vec!["pattern_vail"]), ("xyz", vec![]), ];
for (value, expected_patterns) in test_cases {
let event = format!(r#"{{"x": "{value}"}}"#);
if expected_patterns.is_empty() {
assert_no_match!(q, event);
} else {
for expected in &expected_patterns {
assert_has_match!(q, event, *expected);
}
}
}
}
#[test]
fn test_unicode_field_names_surrogate_pairs() {
let q = q!("p1" => r#"{"xx💋y": ["value"]}"#);
let event = r#"{"x\u0078\ud83d\udc8by": "value"}"#;
assert_matches!(
q,
event,
vec!["p1"],
"Surrogate pair in field name should decode"
);
let q2 = q!("p2" => r#"{"😀💋😺": [1]}"#);
let event2 = r#"{"\ud83d\ude00\ud83d\udc8b\ud83d\ude3a": 1}"#;
assert_matches!(
q2,
event2,
vec!["p2"],
"Multiple surrogate pairs in field name should decode"
);
}
const EXERCISE_MATCHING_EVENT: &str = r#"{
"Image": {
"Width": 800,
"Height": 600,
"Title": "View from 15th Floor",
"Thumbnail": {
"Url": "https://www.example.com/image/481989943",
"Height": 125,
"Width": 100
},
"Animated" : false,
"IDs": [116, 943, 234, 38793]
}
}"#;
const EXERCISE_MATCHING_SHOULD_MATCH: &[(&str, &str)] = &[
(
r#"{"Image": {"Title": [{"exists": true}]}}"#,
"exists true on Title",
),
(
r#"{"Foo": [{"exists": false}]}"#,
"exists false on missing Foo",
),
(r#"{"Image": {"Width": [800]}}"#, "exact number match"),
(
r#"{"Image": {"Animated": [false], "Thumbnail": {"Height": [125]}}}"#,
"nested multi-field",
),
(
r#"{"Image": {"Width": [800], "Title": [{"exists": true}], "Animated": [false]}}"#,
"three fields",
),
(
r#"{"Image": {"Width": [800], "IDs": [{"exists": true}]}}"#,
"exists on array",
),
(
r#"{"Image": {"Thumbnail": {"Url": [{"shellstyle": "*9943"}]}}}"#,
"shellstyle suffix",
),
(
r#"{"Image": {"Thumbnail": {"Url": [{"shellstyle": "https://www.example.com/*"}]}}}"#,
"shellstyle prefix",
),
(
r#"{"Image": {"Thumbnail": {"Url": [{"shellstyle": "https://www.example.com/*9943"}]}}}"#,
"shellstyle infix",
),
(
r#"{"Image": {"Title": [{"anything-but": ["Pikachu", "Eevee"]}]}}"#,
"anything-but",
),
(
r#"{"Image": {"Thumbnail": {"Url": [{"prefix": "https:"}]}}}"#,
"prefix",
),
(
r#"{"Image": {"Thumbnail": {"Url": ["a", {"prefix": "https:"}]}}}"#,
"prefix or literal",
),
(
r#"{"Image": {"Title": [{"equals-ignore-case": "VIEW FROM 15th FLOOR"}]}}"#,
"equals-ignore-case",
),
(
r#"{"Image": {"Title": [{"regex": "View from .... Floor"}]}}"#,
"regex dots",
),
(
r#"{"Image": {"Title": [{"regex": "View from [0-9][0-9][rtn][dh] Floor"}]}}"#,
"regex char class",
),
(
r#"{"Image": {"Title": [{"regex": "View from 15th (Floor|Storey)"}]}}"#,
"regex alternation",
),
];
const EXERCISE_MATCHING_SHOULD_NOT_MATCH: &[(&str, &str)] = &[
(
r#"{"Image": {"Animated": [{"exists": false}]}}"#,
"exists false on present field",
),
(
r#"{"Image": {"NotThere": [{"exists": true}]}}"#,
"exists true on missing field",
),
(
r#"{"Image": {"IDs": [{"exists": false}], "Animated": [false]}}"#,
"exists false on array",
),
(
r#"{"Image": {"Thumbnail": {"Url": [{"prefix": "http:"}]}}}"#,
"wrong prefix",
),
];
#[test]
#[cfg_attr(miri, ignore)]
fn test_exercise_matching_comprehensive() {
let event = EXERCISE_MATCHING_EVENT;
for (pattern, desc) in EXERCISE_MATCHING_SHOULD_MATCH {
let q = q!(*desc => pattern);
assert_has_match!(
q,
event,
*desc,
format!("Pattern '{}' should match: {}", desc, pattern)
);
}
for (pattern, desc) in EXERCISE_MATCHING_SHOULD_NOT_MATCH {
let q = q!(*desc => pattern);
assert_no_match!(
q,
event,
format!("Pattern '{}' should NOT match: {}", desc, pattern)
);
}
let mut combined = Quamina::new();
for (pattern, desc) in EXERCISE_MATCHING_SHOULD_MATCH {
combined.add_pattern(*desc, pattern).unwrap();
}
assert_match_count!(
combined,
event,
EXERCISE_MATCHING_SHOULD_MATCH.len(),
"All should_match patterns should match when combined"
);
}
#[test]
fn test_exercise_matching_miri_friendly() {
let event = r#"{
"Image": {
"Width": 800,
"Height": 600,
"Title": "View from 15th Floor",
"Thumbnail": {
"Url": "https://www.example.com/image/481989943",
"Height": 125,
"Width": 100
},
"Animated" : false,
"IDs": [116, 943, 234, 38793]
}
}"#;
let patterns: Vec<(&str, &str)> = vec![
(r#"{"Image": {"Width": [800]}}"#, "exact number match"),
(
r#"{"Image": {"Title": [{"exists": true}]}}"#,
"exists true on Title",
),
(
r#"{"Image": {"Thumbnail": {"Url": [{"prefix": "https:"}]}}}"#,
"prefix",
),
(
r#"{"Image": {"Thumbnail": {"Url": [{"shellstyle": "*9943"}]}}}"#,
"shellstyle suffix",
),
];
for (pattern, desc) in &patterns {
let q = q!(*desc => pattern);
assert_has_match!(
q,
event,
*desc,
format!("Pattern '{}' should match: {}", desc, pattern)
);
}
}
#[test]
#[cfg_attr(miri, ignore)]
fn test_concurrent_update_during_matching() {
use flate2::read::GzDecoder;
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::sync::Arc;
use std::sync::mpsc;
use std::thread;
const UPDATE_INTERVAL: usize = 250;
fn add_pattern_concurrent(
q: Arc<std::sync::RwLock<Quamina<String>>>,
idx: usize,
tx: mpsc::Sender<String>,
) {
let val = format!("CONCURRENT_STREET_{idx}");
let pattern = format!(r#"{{"properties": {{"STREET": ["{val}"]}}}}"#);
{
let mut q_write = q.write().unwrap();
q_write
.add_pattern(val.clone(), &pattern)
.expect("add_pattern failed");
}
let _ = tx.send(val); }
let path = "testdata/citylots2.json.gz";
let file = File::open(path).expect("Failed to open citylots2.json.gz");
let decoder = GzDecoder::new(file);
let reader = BufReader::new(decoder);
let lines: Vec<Vec<u8>> = reader
.lines()
.map(|l| l.expect("Failed to read line").into_bytes())
.collect();
let patterns = [
("CRANLEIGH", r#"{"properties": {"STREET": ["CRANLEIGH"]}}"#),
(
"shellstyle",
r#"{"properties": {"STREET": [{"shellstyle": "B*K"}]}}"#,
),
];
let q = Arc::new(std::sync::RwLock::new(Quamina::new()));
{
let mut q_write = q.write().unwrap();
for (name, pattern) in &patterns {
q_write.add_pattern(name.to_string(), pattern).unwrap();
}
}
let (tx, rx) = mpsc::channel::<String>();
let mut total_matches = 0usize;
let mut sent = 0;
let start = std::time::Instant::now();
for (i, line) in lines.iter().enumerate() {
let matches = {
let q_read = q.read().unwrap();
q_read
.matches_for_event(line)
.expect("matches_for_event failed")
};
total_matches += matches.len();
if (i + 1) % UPDATE_INTERVAL == 0 {
sent += 1;
let q_clone = Arc::clone(&q);
let tx_clone = tx.clone();
let idx = sent;
thread::spawn(move || {
add_pattern_concurrent(q_clone, idx, tx_clone);
});
}
}
let elapsed = start.elapsed();
drop(tx);
std::thread::sleep(std::time::Duration::from_millis(100));
let mut verified = 0;
for val in &rx {
let event = format!(r#"{{"properties": {{"STREET": "{val}"}}}}"#);
let q_read = q.read().unwrap();
let matches = q_read
.matches_for_event(event.as_bytes())
.expect("matches_for_event failed");
assert!(
matches.contains(&val),
"Concurrent pattern {val} not found in matches: {matches:?}"
);
verified += 1;
}
let events_per_sec = u128::try_from(lines.len())
.ok()
.and_then(|n| n.checked_mul(1_000_000_000))
.map_or(0, |numer| numer / elapsed.as_nanos().max(1));
println!(
"Concurrent update test: {events_per_sec:.0} events/sec, {total_matches} total matches, {sent} patterns added concurrently, {verified} verified"
);
assert_eq!(sent, verified, "Not all concurrent patterns were verified");
assert!(sent > 0, "Should have added some patterns concurrently");
assert!(total_matches > 0, "Should have gotten some matches");
}
#[test]
fn test_arc_field_matcher_sharing() {
let mut q = Quamina::new();
q.add_pattern("id1", r#"{"status": ["active"]}"#).unwrap();
q.add_pattern("id2", r#"{"status": ["active"]}"#).unwrap();
q.add_pattern("id3", r#"{"status": ["active"]}"#).unwrap();
let event = r#"{"status": "active"}"#;
let mut matches = q.matches_for_event(event.as_bytes()).unwrap();
matches.sort_unstable();
assert_eq!(matches, vec!["id1", "id2", "id3"]);
q.delete_patterns(&"id2").unwrap();
let mut matches2 = q.matches_for_event(event.as_bytes()).unwrap();
matches2.sort_unstable();
assert_eq!(matches2, vec!["id1", "id3"]);
let q2 = q.clone();
let mut matches3 = q2.matches_for_event(event.as_bytes()).unwrap();
matches3.sort_unstable();
assert_eq!(matches3, vec!["id1", "id3"]);
}
#[test]
#[cfg_attr(miri, ignore)]
fn test_pattern_insertion_scales_linearly() {
use std::time::Instant;
let layers: &[usize] = &[50, 500, 5000, 20_000];
{
let mut warmup = QuaminaBuilder::new()
.with_arena_byte_budget(100 * 1024 * 1024)
.build()
.unwrap();
for i in 0..100 {
let pattern = format!(r#"{{"key": ["warmup_{i}"]}}"#);
warmup.add_pattern(format!("w{i}"), &pattern).unwrap();
}
let _ = warmup.matches_for_event(br#"{"key": "warmup_0"}"#);
}
let mut costs: Vec<(usize, u128)> = Vec::new();
for &n in layers {
let mut q = QuaminaBuilder::new()
.with_arena_byte_budget(100 * 1024 * 1024)
.build()
.unwrap();
let start = Instant::now();
for i in 0..n {
let pattern = format!(r#"{{"key": ["value_{i}"]}}"#);
q.add_pattern(format!("p{i}"), &pattern).unwrap();
}
let matches = q.matches_for_event(br#"{"key": "value_0"}"#).unwrap();
let elapsed = start.elapsed();
assert!(
matches.contains(&"p0".to_string()),
"Pattern p0 should match after adding {n} patterns",
);
let cost_per_pattern_ns =
elapsed.as_nanos() / u128::try_from(n).expect("n is small and non-negative");
costs.push((n, cost_per_pattern_ns));
}
for i in 1..costs.len() {
let (small_n, small_cost) = costs[i - 1];
let (large_n, large_cost) = costs[i];
let denom = small_cost.max(1);
let too_steep = large_cost > denom.saturating_mul(6);
let ratio_tenths = large_cost.saturating_mul(10) / denom;
let small_us_tenths = small_cost / 100; let large_us_tenths = large_cost / 100;
assert!(
!too_steep,
"Pattern insertion scales poorly between n={small_n} and n={large_n}: \
{}.{}x (n={small_n}={}.{}µs/pattern, n={large_n}={}.{}µs/pattern). \
This suggests O(n²) regression.",
ratio_tenths / 10,
ratio_tenths % 10,
small_us_tenths / 10,
small_us_tenths % 10,
large_us_tenths / 10,
large_us_tenths % 10,
);
}
}
#[test]
#[cfg_attr(miri, ignore)]
fn test_pathological_correctness() {
let mut q = Quamina::new();
let shell_patterns: &[(&str, &str)] = &[
("shell0", "*a*b*c*"),
("shell1", "*x*y*z*"),
("shell2", "*e*f*g*"),
("shell3", "*m*n*o*"),
("shell4", "*p*q*r*"),
("shell5", "*s*t*u*"),
("shell6", "*a*e*i*"),
("shell7", "*b*d*f*"),
("shell8", "*c*g*k*"),
("shell9", "*d*h*l*"),
("shell10", "*i*o*u*"),
("shell11", "*r*s*t*"),
];
for (name, glob) in shell_patterns {
let pattern = format!(r#"{{"val": [{{"shellstyle": "{glob}"}}]}}"#);
q.add_pattern(name.to_string(), &pattern).unwrap();
}
let re_patterns: &[(&str, &str)] = &[
("re0", "(([abc]?)*)+"),
("re1", "([abc]+)*d"),
("re2", "(a*)*b"),
("re3", "([xyz]?)*end"),
("re4", "(([mno]?)*)+"),
("re5", "([pqr]+)*s"),
];
for (name, re) in re_patterns {
let pattern = format!(r#"{{"val": [{{"regexp": "{re}"}}]}}"#);
q.add_pattern(name.to_string(), &pattern).unwrap();
}
let cases: &[(&str, &[&str])] = &[
(r#"{"val": "abc"}"#, &["re0", "shell0"]),
(r#"{"val": "abcd"}"#, &["re1", "shell0"]),
(r#"{"val": "aaab"}"#, &["re0", "re2"]),
(r#"{"val": "mno"}"#, &["re4", "shell3"]),
(r#"{"val": "pqrs"}"#, &["re5", "shell4"]),
(r#"{"val": "xyzend"}"#, &["re3", "shell1"]),
(r#"{"val": "abcxyz"}"#, &["shell0", "shell1"]),
(r#"{"val": "mnopqr"}"#, &["shell3", "shell4"]),
(r#"{"val": "aeiou"}"#, &["shell10", "shell6"]),
(r#"{"val": "rstuvwxyz"}"#, &["shell1", "shell11", "shell5"]),
(
r#"{"val": "abcdefghijklmno"}"#,
&[
"shell0", "shell2", "shell3", "shell6", "shell7", "shell8", "shell9",
],
),
(r#"{"val": "abcabcabcd"}"#, &["re1", "shell0"]),
(r#"{"val": "aaaaaab"}"#, &["re0", "re2"]),
];
for (event, want) in cases {
let mut got = q.matches_for_event(event.as_bytes()).unwrap();
got.sort();
let want: Vec<String> = want.iter().map(std::string::ToString::to_string).collect();
assert_eq!(got, want, "Event: {event}");
}
}
#[test]
fn test_pathological_correctness_miri_friendly() {
let q = q!(
"sh0" => r#"{"val": [{"shellstyle": "*a*b*"}]}"#,
"sh1" => r#"{"val": [{"shellstyle": "*x*y*"}]}"#,
"re0" => r#"{"val": [{"regexp": "[abc]+d"}]}"#,
"re1" => r#"{"val": [{"regexp": "x.*z"}]}"#
);
let cases: &[(&str, &[&str])] = &[
(r#"{"val": "ab"}"#, &["sh0"]),
(r#"{"val": "abcd"}"#, &["re0", "sh0"]),
(r#"{"val": "xyz"}"#, &["re1", "sh1"]),
(r#"{"val": "abxy"}"#, &["sh0", "sh1"]),
(r#"{"val": "nope"}"#, &[]),
];
for (event, want) in cases {
let mut got = q.matches_for_event(event.as_bytes()).unwrap();
got.sort_unstable();
let want: Vec<String> = want.iter().map(std::string::ToString::to_string).collect();
assert_eq!(got, want, "Event: {event}");
}
}
#[test]
#[ignore]
fn test_break_500_limit() {
let letters = b"abcdefghijklmnopqrstuvwxyz";
let mut q = QuaminaBuilder::new()
.with_arena_byte_budget(100 * 1024 * 1024)
.build()
.unwrap();
let mut pat_count = 0u32;
for i in 0..letters.len() {
for j in (i + 1)..letters.len() {
let ss = format!("*{}*{}*", letters[i] as char, letters[j] as char);
let pat = format!(r#"{{"val": [{{"shellstyle": "{ss}"}}]}}"#);
q.add_pattern(format!("p{pat_count}"), &pat).unwrap();
pat_count += 1;
}
}
for i in 0..letters.len() {
for j in (i + 1)..letters.len() {
for k in (j + 1)..letters.len() {
let ss = format!(
"*{}*{}*{}*",
letters[i] as char, letters[j] as char, letters[k] as char
);
let pat = format!(r#"{{"val": [{{"shellstyle": "{ss}"}}]}}"#);
q.add_pattern(format!("p{pat_count}"), &pat).unwrap();
pat_count += 1;
}
}
}
assert_eq!(pat_count, 2925);
let events: &[(&str, &str)] = &[
(
"alpha-repeat",
&format!(r#"{{"val": "{}"}}"#, "abcdefghijklmnopqrstuvwxyz".repeat(4)),
),
(
"early-only",
&format!(r#"{{"val": "{}"}}"#, "abcabc".repeat(30)),
),
(
"interleaved",
&format!(r#"{{"val": "{}"}}"#, "azbyxcwdveu".repeat(16)),
),
(
"near-miss",
&format!(
r#"{{"val": "{}"}}"#,
format!(
"{}b{}d{}f{}h",
"a".repeat(50),
"c".repeat(50),
"e".repeat(50),
"g".repeat(50),
)
),
),
(
"single-repeat",
&format!(r#"{{"val": "{}"}}"#, "m".repeat(200)),
),
];
for (name, event) in events {
let start = std::time::Instant::now();
let matches = q.matches_for_event(event.as_bytes()).unwrap();
let elapsed = start.elapsed();
eprintln!(
"{name:<16} {count} matches in {elapsed:?}",
count = matches.len()
);
}
}