use super::*;
#[test]
fn test_exact_match() {
let q = q!("p1" => r#"{"status": ["active"]}"#);
assert_matches!(q, r#"{"status": "active"}"#, vec!["p1"]);
}
#[test]
fn test_no_match() {
let q = q!("p1" => r#"{"status": ["active"]}"#);
assert_no_match!(q, r#"{"status": "inactive"}"#);
}
#[test]
fn test_numeric_match() {
let q = q!("p1" => r#"{"count": [42]}"#);
assert_matches!(
q,
r#"{"count": 42}"#,
vec!["p1"],
"Should match numeric value 42"
);
}
#[test]
fn test_numeric_variant_matching() {
let q = q!("p1" => r#"{"x": [35]}"#);
for event in [
r#"{"x": 35}"#,
r#"{"x": 35.0}"#,
r#"{"x": 3.5e1}"#,
r#"{"x": 35.000}"#,
r#"{"x": 0.000035e6}"#,
] {
assert_matches!(q, event, vec!["p1"]);
}
}
#[test]
fn test_boolean_match() {
let q = q!("p1" => r#"{"enabled": [true]}"#);
assert_matches!(
q,
r#"{"enabled": true}"#,
vec!["p1"],
"Should match boolean true"
);
}
#[test]
fn test_null_match() {
let q = q!("p1" => r#"{"value": [null]}"#);
assert_matches!(
q,
r#"{"value": null}"#,
vec!["p1"],
"Should match null value"
);
}
#[test]
fn test_exists_true() {
let q = q!("p1" => r#"{"name": [{"exists": true}]}"#);
assert_matches!(
q,
r#"{"name": "anything", "other": 1}"#,
vec!["p1"],
"Should match when field exists"
);
assert_no_match!(
q,
r#"{"other": 1}"#,
"Should not match when field is missing"
);
}
#[test]
fn test_exists_false() {
let q = q!("p1" => r#"{"name": [{"exists": false}]}"#);
assert_matches!(q, r#"{"other": 1}"#, vec!["p1"]);
assert_no_match!(q, r#"{"name": "value"}"#);
}
#[test]
fn test_exists_with_empty_array() {
let q_true = q!("p1" => r#"{"a": [{"exists": true}]}"#);
let q_false = q!("p2" => r#"{"a": [{"exists": false}]}"#);
let event = r#"{"a": []}"#;
assert_no_match!(q_true, event, "exists:true should not match empty array");
assert_matches!(
q_false,
event,
vec!["p2"],
"exists:false should match empty array"
);
}
#[test]
fn test_nested_object_pattern() {
let q = q!("p1" => r#"{"user": {"role": ["admin"]}}"#);
assert_matches!(
q,
r#"{"user": {"role": "admin", "name": "alice"}}"#,
vec!["p1"],
"Should match nested field"
);
assert_no_match!(q, r#"{"user": {"role": "guest"}}"#);
}
#[test]
fn test_deeply_nested() {
let q = q!("p1" => r#"{"a": {"b": {"c": ["value"]}}}"#);
assert_matches!(q, r#"{"a": {"b": {"c": "value"}}}"#, vec!["p1"]);
}
#[test]
fn test_array_element_matching() {
let q = q!("p1" => r#"{"ids": [943]}"#);
let event = r#"{"ids": [116, 943, 234]}"#;
assert_matches!(
q,
event,
vec!["p1"],
"Should match when pattern value is in event array"
);
}
#[test]
fn test_array_cross_element_matching() {
let q = q!("cross" => r#"{"members": {"given": ["Mick"], "surname": ["Strummer"]}}"#);
let event = r#"{"members": [
{"given": "Joe", "surname": "Strummer"},
{"given": "Mick", "surname": "Jones"}
]}"#;
assert_no_match!(q, event, "Should not match across different array elements");
}
#[test]
fn test_array_cross_element_comprehensive() {
let bands = r#"{
"bands": [
{
"name": "The Clash",
"members": [
{"given": "Joe", "surname": "Strummer", "role": ["guitar", "vocals"]},
{"given": "Mick", "surname": "Jones", "role": ["guitar", "vocals"]},
{"given": "Paul", "surname": "Simonon", "role": ["bass"]},
{"given": "Topper", "surname": "Headon", "role": ["drums"]}
]
},
{
"name": "Boris",
"members": [
{"given": "Wata", "role": ["guitar", "vocals"]},
{"given": "Atsuo", "role": ["drums"]},
{"given": "Takeshi", "role": ["bass", "vocals"]}
]
}
]
}"#;
let q = q!(
"mick_strummer" => r#"{"bands": {"members": {"given": ["Mick"], "surname": ["Strummer"]}}}"#,
"wata_drums" => r#"{"bands": {"members": {"given": ["Wata"], "role": ["drums"]}}}"#,
"wata_guitar" => r#"{"bands": {"members": {"given": ["Wata"], "role": ["guitar"]}}}"#
);
assert_match_count!(q, bands, 1);
assert_has_match!(q, bands, "wata_guitar");
assert_no_has_match!(q, bands, "mick_strummer");
assert_no_has_match!(q, bands, "wata_drums");
}
#[test]
fn test_multiple_patterns_same_id() {
let q = q!(
"p1" => r#"{"status": ["active"]}"#,
"p1" => r#"{"status": ["pending"]}"#
);
assert_matches!(q, r#"{"status": "active"}"#, vec!["p1"]);
assert_matches!(q, r#"{"status": "pending"}"#, vec!["p1"]);
}
#[test]
fn test_or_within_field() {
let q = q!("p1" => r#"{"status": ["active", "pending", "review"]}"#);
for status in &["active", "pending", "review"] {
let event = format!(r#"{{"status": "{}"}}"#, status);
assert_matches!(q, event, vec!["p1"]);
}
assert_no_match!(q, r#"{"status": "deleted"}"#);
}
#[test]
fn test_and_across_fields() {
let q = q!(
"p1" => r#"{"type": ["order"], "status": ["pending"], "priority": ["high"]}"#
);
assert_matches!(
q,
r#"{"type": "order", "status": "pending", "priority": "high"}"#,
vec!["p1"]
);
assert_no_match!(q, r#"{"type": "order", "status": "pending"}"#);
}
#[test]
fn test_delete_patterns() {
let mut q = Quamina::new();
q.add_pattern("p1", r#"{"status": ["active"]}"#).unwrap();
q.add_pattern("p2", r#"{"status": ["pending"]}"#).unwrap();
assert_has_match!(q, r#"{"status": "active"}"#, "p1");
q.delete_patterns(&"p1").unwrap();
assert_no_match!(q, r#"{"status": "active"}"#);
assert_has_match!(q, r#"{"status": "pending"}"#, "p2");
}
#[test]
fn test_rebuild_after_delete() {
let mut q = Quamina::new();
q.add_pattern("p1", r#"{"status": ["active"]}"#).unwrap();
q.add_pattern("p2", r#"{"status": ["pending"]}"#).unwrap();
q.add_pattern("p3", r#"{"status": ["review"]}"#).unwrap();
assert_eq!(q.pattern_count(), 3);
q.delete_patterns(&"p1").unwrap();
assert_eq!(q.pattern_count(), 2);
assert!(q.deleted_patterns.contains(&"p1"));
let purged = q.rebuild();
assert_eq!(purged, 1);
assert!(q.deleted_patterns.is_empty());
assert_eq!(q.pattern_count(), 2);
assert_has_match!(q, r#"{"status": "pending"}"#, "p2");
assert_has_match!(q, r#"{"status": "review"}"#, "p3");
assert_no_match!(q, r#"{"status": "active"}"#);
}
#[test]
fn test_pruner_stats() {
let mut q = Quamina::new();
q.add_pattern("p1", r#"{"status": ["active"]}"#).unwrap();
q.add_pattern("p2", r#"{"status": ["pending"]}"#).unwrap();
assert_eq!(q.pruner_stats().emitted(), 0);
assert_eq!(q.pruner_stats().filtered(), 0);
let _ = q
.matches_for_event(r#"{"status": "active"}"#.as_bytes())
.unwrap();
assert_eq!(q.pruner_stats().emitted(), 1);
assert_eq!(q.pruner_stats().filtered(), 0);
q.delete_patterns(&"p1").unwrap();
let _ = q
.matches_for_event(r#"{"status": "active"}"#.as_bytes())
.unwrap();
assert_eq!(q.pruner_stats().emitted(), 1);
assert_eq!(q.pruner_stats().filtered(), 1);
let _ = q
.matches_for_event(r#"{"status": "pending"}"#.as_bytes())
.unwrap();
assert_eq!(q.pruner_stats().emitted(), 2);
assert_eq!(q.pruner_stats().filtered(), 1);
q.rebuild();
assert_eq!(q.pruner_stats().emitted(), 0);
assert_eq!(q.pruner_stats().filtered(), 0);
}
#[test]
#[cfg_attr(miri, ignore)]
fn test_should_rebuild_threshold() {
let mut q = Quamina::new();
q.add_pattern("p1", r#"{"x": ["a"]}"#).unwrap();
q.add_pattern("p2", r#"{"x": ["a"]}"#).unwrap();
q.add_pattern("p3", r#"{"x": ["a"]}"#).unwrap();
q.add_pattern("p4", r#"{"x": ["a"]}"#).unwrap();
q.add_pattern("p5", r#"{"x": ["a"]}"#).unwrap();
q.delete_patterns(&"p1").unwrap();
q.delete_patterns(&"p2").unwrap();
assert!(!q.should_rebuild());
let event = br#"{"x": "a"}"#;
for _ in 0..500 {
let _ = q.matches_for_event(event).unwrap();
}
assert!(q.should_rebuild());
let purged = q.maybe_rebuild();
assert_eq!(purged, 2);
assert!(!q.should_rebuild());
}
#[test]
#[cfg(miri)]
fn test_should_rebuild_threshold_miri_friendly() {
let mut q = Quamina::new();
q.add_pattern("p1", r#"{"x": ["a"]}"#).unwrap();
q.add_pattern("p2", r#"{"x": ["a"]}"#).unwrap();
q.add_pattern("p3", r#"{"x": ["a"]}"#).unwrap();
q.delete_patterns(&"p1").unwrap();
q.delete_patterns(&"p2").unwrap();
assert!(!q.should_rebuild());
let event = br#"{"x": "a"}"#;
for _ in 0..400 {
let _ = q.matches_for_event(event).unwrap();
}
assert!(q.should_rebuild());
let purged = q.maybe_rebuild();
assert_eq!(purged, 2);
assert!(!q.should_rebuild());
}
#[test]
fn test_should_rebuild_boundary() {
use super::PrunerStats;
let stats = PrunerStats::new();
stats.add_emitted(599);
stats.add_filtered(400);
assert!(!stats.should_rebuild());
stats.add_emitted(1); assert!(stats.should_rebuild());
stats.reset();
stats.add_emitted(601);
stats.add_filtered(400);
assert!(stats.should_rebuild());
stats.reset();
stats.add_emitted(900);
stats.add_filtered(100);
assert!(!stats.should_rebuild());
stats.reset();
stats.add_emitted(1000);
stats.add_filtered(200);
assert!(!stats.should_rebuild());
}
#[test]
fn test_pruner_stats_clone() {
use super::PrunerStats;
let stats = PrunerStats::new();
stats.add_emitted(42);
stats.add_filtered(17);
let cloned = stats.clone();
assert_eq!(cloned.emitted(), 42);
assert_eq!(cloned.filtered(), 17);
}
#[test]
#[cfg_attr(miri, ignore)]
fn test_auto_rebuild_disabled() {
let mut q = Quamina::new();
q.set_auto_rebuild(false);
q.add_pattern("p1", r#"{"x": ["a"]}"#).unwrap();
q.add_pattern("p2", r#"{"x": ["a"]}"#).unwrap();
q.delete_patterns(&"p1").unwrap();
let event = br#"{"x": "a"}"#;
for _ in 0..2000 {
let _ = q.matches_for_event(event).unwrap();
}
assert!(q.should_rebuild());
let purged = q.maybe_rebuild();
assert_eq!(purged, 0);
}
#[test]
fn test_auto_rebuild_disabled_miri_friendly() {
let mut q = Quamina::new();
q.set_auto_rebuild(false);
q.add_pattern("p1", r#"{"x": ["a"]}"#).unwrap();
q.add_pattern("p2", r#"{"x": ["a"]}"#).unwrap();
q.delete_patterns(&"p1").unwrap();
let event = br#"{"x": "a"}"#;
for _ in 0..5 {
let _ = q.matches_for_event(event).unwrap();
}
let purged = q.maybe_rebuild();
assert_eq!(purged, 0);
}
#[test]
fn test_clone_for_snapshot() {
let mut q = Quamina::new();
q.add_pattern("p1", r#"{"status": ["active"]}"#).unwrap();
let snapshot = q.clone();
q.add_pattern("p2", r#"{"status": ["pending"]}"#).unwrap();
assert_no_match!(snapshot, r#"{"status": "pending"}"#);
assert_has_match!(q, r#"{"status": "pending"}"#, "p2");
}
#[test]
fn test_send_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<Quamina<String>>();
}
#[test]
fn test_has_matches() {
let q = q!("p1" => r#"{"status": ["active"]}"#);
assert!(q.has_matches(r#"{"status": "active"}"#.as_bytes()).unwrap());
assert!(!q
.has_matches(r#"{"status": "inactive"}"#.as_bytes())
.unwrap());
}
#[test]
fn test_count_matches() {
let q = q!(
"p1" => r#"{"status": ["active"]}"#,
"p2" => r#"{"status": ["active"]}"#,
"p3" => r#"{"status": ["pending"]}"#
);
assert_eq!(
q.count_matches(r#"{"status": "active"}"#.as_bytes())
.unwrap(),
2
);
assert_eq!(
q.count_matches(r#"{"status": "pending"}"#.as_bytes())
.unwrap(),
1
);
assert_eq!(
q.count_matches(r#"{"status": "deleted"}"#.as_bytes())
.unwrap(),
0
);
}
#[test]
fn test_pattern_count_and_clear() {
let mut q = Quamina::new();
assert!(q.is_empty());
assert_eq!(q.pattern_count(), 0);
q.add_pattern("p1", r#"{"a": ["1"]}"#).unwrap();
q.add_pattern("p2", r#"{"b": ["2"]}"#).unwrap();
assert!(!q.is_empty());
assert_eq!(q.pattern_count(), 2);
q.clear();
assert!(q.is_empty());
assert_eq!(q.pattern_count(), 0);
}
#[test]
fn test_invalid_json_events() {
let q = q!("p1" => r#"{"a": [1]}"#);
let bad_events: &[(&[u8], &str)] = &[
(br#"{"a"#, "Truncated JSON"),
(br#"{"a": "#, "Truncated value"),
(br#"{"a": ["#, "Truncated array"),
(b"", "Empty input"),
(br#""string""#, "String at top level"),
(br#"[1, 2]"#, "Array at top level"),
(b"123", "Number at top level"),
(br#"{ "a" : }"#, "Missing value"),
(br#"{"a": "a\zb"}"#, "Invalid escape \\z in value"),
(br#"{"a\zb": 2}"#, "Invalid escape in field name"),
(br#"{"a": xx}"#, "Invalid value xx"),
(br#"{"a": tru}"#, "Truncated 'tru'"),
(br#"{"a": truse}"#, "Invalid 'truse'"),
];
for (event, desc) in bad_events {
assert!(q.matches_for_event(event).is_err(), "{} should error", desc);
}
}
#[test]
fn test_invalid_pattern_handling() {
let mut q = Quamina::new();
assert!(q.add_pattern("p1", "").is_err());
assert!(q.add_pattern("p2", "33").is_err());
assert!(q.add_pattern("p3", "[1,2]").is_err());
assert!(q.add_pattern("p4", "{").is_err());
assert!(q.add_pattern("p5", r#"{"foo": }"#).is_err());
assert!(q.add_pattern("p6", r#"{"foo": "string"}"#).is_err());
assert!(q.add_pattern("p7", r#"{"foo": 123}"#).is_err());
assert!(q.add_pattern("p8", r#"{"foo": true}"#).is_err());
assert!(q.add_pattern("valid1", r#"{"x": [1]}"#).is_ok());
assert!(q.add_pattern("valid2", r#"{"x": ["string"]}"#).is_ok());
assert!(q.add_pattern("valid3", r#"{"x": {"y": [1]}}"#).is_ok());
}
#[test]
fn test_bad_pattern_error_handling() {
let mut q = Quamina::new();
assert!(q
.add_pattern("p1", r#"{"x": [{"anything-but": []}]}"#)
.is_err());
assert!(q
.add_pattern("p2", r#"{"x": [{"anything-but": ["a", 1]}]}"#)
.is_err());
}
#[test]
fn test_bad_event_error_handling() {
let q = q!("p1" => r#"{"x": [1]}"#);
assert!(q.matches_for_event(b"not json").is_err());
assert!(q.matches_for_event(b"{").is_err());
assert!(q.matches_for_event(b"").is_err());
}
#[test]
fn test_rebuild_zero_filtered_denominator() {
let mut q = Quamina::new();
q.add_pattern("p1", r#"{"likes": ["tacos"]}"#).unwrap();
q.delete_patterns(&"p1").unwrap();
let result = q.matches_for_event(r#"{"likes": "tacos"}"#.as_bytes());
assert!(result.is_ok(), "Should not panic with empty matcher");
assert!(result.unwrap().is_empty(), "No matches expected");
}
#[test]
fn test_builder_basic() {
let q = QuaminaBuilder::<String>::new().build().unwrap();
assert!(q.is_empty(), "New builder should create empty matcher");
assert!(
q.auto_rebuild_enabled(),
"Auto-rebuild should be enabled by default"
);
}
#[test]
fn test_builder_with_media_type_json() {
let q = QuaminaBuilder::<String>::new()
.with_media_type("application/json")
.unwrap()
.build()
.unwrap();
assert!(q.is_empty());
}
#[test]
fn test_builder_with_invalid_media_type() {
let result = QuaminaBuilder::<String>::new().with_media_type("text/html");
assert!(result.is_err(), "Should reject text/html");
if let Err(QuaminaError::UnsupportedMediaType(mt)) = result {
assert_eq!(mt, "text/html");
} else {
panic!("Expected UnsupportedMediaType error");
}
let result = QuaminaBuilder::<String>::new().with_media_type("application/xml");
assert!(result.is_err(), "Should reject application/xml");
let result = QuaminaBuilder::<String>::new().with_media_type("");
assert!(result.is_err(), "Should reject empty media type");
}
#[test]
fn test_builder_with_auto_rebuild() {
let q = QuaminaBuilder::<String>::new()
.with_auto_rebuild(false)
.build()
.unwrap();
assert!(!q.auto_rebuild_enabled(), "Auto-rebuild should be disabled");
let q = QuaminaBuilder::<String>::new()
.with_auto_rebuild(true)
.build()
.unwrap();
assert!(q.auto_rebuild_enabled(), "Auto-rebuild should be enabled");
}
#[test]
fn test_builder_combined_options() {
let mut q = QuaminaBuilder::<String>::new()
.with_media_type("application/json")
.unwrap()
.with_auto_rebuild(false)
.build()
.unwrap();
q.add_pattern("p1".to_string(), r#"{"status": ["active"]}"#)
.unwrap();
let matches = q
.matches_for_event(r#"{"status": "active"}"#.as_bytes())
.unwrap();
assert_eq!(matches, vec!["p1".to_string()]);
assert!(!q.auto_rebuild_enabled());
}
#[test]
fn test_builder_default() {
let q = QuaminaBuilder::<String>::default().build().unwrap();
assert!(q.is_empty());
assert!(q.auto_rebuild_enabled());
}
#[test]
fn test_builder_generic_type() {
let mut q = QuaminaBuilder::<i32>::new().build().unwrap();
q.add_pattern(42, r#"{"x": [1]}"#).unwrap();
let matches = q.matches_for_event(r#"{"x": 1}"#.as_bytes()).unwrap();
assert_eq!(matches, vec![42]);
let mut q = QuaminaBuilder::<&str>::new().build().unwrap();
q.add_pattern("test", r#"{"x": [1]}"#).unwrap();
let matches = q.matches_for_event(r#"{"x": 1}"#.as_bytes()).unwrap();
assert_eq!(matches, vec!["test"]);
}
struct MockFlattener {
fields: Vec<OwnedField>,
}
impl MockFlattener {
fn new(fields: Vec<OwnedField>) -> Self {
Self { fields }
}
}
impl Flattener for MockFlattener {
fn flatten(
&mut self,
_event: &[u8],
_tracker: &dyn SegmentsTreeTracker,
) -> Result<Vec<OwnedField>, QuaminaError> {
Ok(self.fields.clone())
}
fn copy(&self) -> Box<dyn Flattener> {
Box::new(MockFlattener {
fields: self.fields.clone(),
})
}
}
#[test]
fn test_custom_flattener_basic() {
let flattener = MockFlattener::new(vec![OwnedField {
path: b"status".to_vec(),
val: b"\"active\"".to_vec(),
array_trail: vec![],
is_number: false,
}]);
let mut q = QuaminaBuilder::<String>::new()
.with_flattener(Box::new(flattener))
.unwrap()
.build()
.unwrap();
q.add_pattern("p1".to_string(), r#"{"status": ["active"]}"#)
.unwrap();
let matches = q.matches_for_event(b"ignored event data").unwrap();
assert_eq!(matches, vec!["p1".to_string()]);
}
#[test]
fn test_custom_flattener_no_match() {
let flattener = MockFlattener::new(vec![OwnedField {
path: b"status".to_vec(),
val: b"\"inactive\"".to_vec(),
array_trail: vec![],
is_number: false,
}]);
let mut q = QuaminaBuilder::<String>::new()
.with_flattener(Box::new(flattener))
.unwrap()
.build()
.unwrap();
q.add_pattern("p1".to_string(), r#"{"status": ["active"]}"#)
.unwrap();
let matches = q.matches_for_event(b"ignored").unwrap();
assert!(matches.is_empty());
}
#[test]
fn test_custom_flattener_with_numbers() {
let flattener = MockFlattener::new(vec![OwnedField {
path: b"count".to_vec(),
val: b"42".to_vec(),
array_trail: vec![],
is_number: true,
}]);
let mut q = QuaminaBuilder::<String>::new()
.with_flattener(Box::new(flattener))
.unwrap()
.build()
.unwrap();
q.add_pattern("p1".to_string(), r#"{"count": [42]}"#)
.unwrap();
let matches = q.matches_for_event(b"ignored").unwrap();
assert_eq!(matches, vec!["p1".to_string()]);
}
#[test]
fn test_custom_flattener_clone() {
let flattener = MockFlattener::new(vec![OwnedField {
path: b"x".to_vec(),
val: b"\"y\"".to_vec(),
array_trail: vec![],
is_number: false,
}]);
let mut q = QuaminaBuilder::<String>::new()
.with_flattener(Box::new(flattener))
.unwrap()
.build()
.unwrap();
q.add_pattern("p1".to_string(), r#"{"x": ["y"]}"#).unwrap();
let q_clone = q.clone();
let m1 = q.matches_for_event(b"ignored").unwrap();
let m2 = q_clone.matches_for_event(b"ignored").unwrap();
assert_eq!(m1, vec!["p1".to_string()]);
assert_eq!(m2, vec!["p1".to_string()]);
}
#[test]
fn test_with_flattener_conflicts_with_media_type() {
let flattener = MockFlattener::new(vec![]);
let result = QuaminaBuilder::<String>::new()
.with_media_type("application/json")
.unwrap()
.with_flattener(Box::new(flattener));
assert!(result.is_err());
}
#[test]
fn test_with_flattener_cannot_be_set_twice() {
let flattener1 = MockFlattener::new(vec![]);
let flattener2 = MockFlattener::new(vec![]);
let result = QuaminaBuilder::<String>::new()
.with_flattener(Box::new(flattener1))
.unwrap()
.with_flattener(Box::new(flattener2));
assert!(result.is_err());
}
#[test]
fn test_json_flattener_through_trait() {
use crate::flattener::JsonFlattener;
let mut q = QuaminaBuilder::<String>::new()
.with_flattener(Box::new(JsonFlattener::new()))
.unwrap()
.build()
.unwrap();
q.add_pattern("p1".to_string(), r#"{"status": ["active"]}"#)
.unwrap();
let matches = q
.matches_for_event(r#"{"status": "active"}"#.as_bytes())
.unwrap();
assert_eq!(matches, vec!["p1"]);
}
#[test]
fn test_same_pattern_id_multiple_value_types() {
let q = q!("x" => r#"{"x": ["a"]}"#, "x" => r#"{"x": [1]}"#);
assert_matches!(q, r#"{"x": 1}"#, vec!["x"], "number 1 should match");
assert_matches!(q, r#"{"x": "a"}"#, vec!["x"], "string 'a' should match");
let q2 = q!("x" => r#"{"x": [{"wildcard": "x*y"}]}"#, "x" => r#"{"x": [3]}"#);
assert_matches!(q2, r#"{"x": 3}"#, vec!["x"], "number 3 should match");
assert_matches!(
q2,
r#"{"x": "xasdfy"}"#,
vec!["x"],
"wildcard pattern should match"
);
}
#[test]
fn test_field_name_ordering_with_exists() {
let event = r#"{"b": 1}"#;
let patterns = [
(r#"{"b": [1], "a": [{"exists": false}]}"#, "p0"),
(r#"{"b": [1], "c": [{"exists": false}]}"#, "p1"),
(r#"{"b": [1]}"#, "p2"),
(r#"{"a": [{"exists": false}]}"#, "p3"),
];
let mut q = Quamina::new();
for (pattern, name) in &patterns {
q.add_pattern(*name, pattern).unwrap();
}
assert_match_count!(q, event, patterns.len());
for (_, name) in &patterns {
assert_has_match!(q, event, *name);
}
}
#[test]
fn test_invalid_pattern_validation() {
let invalid_patterns = [
(r#"{"foo": 11}"#, "number not in array"),
(r#"{"foo": "x"}"#, "string not in array"),
(r#"{"foo": true}"#, "boolean not in array"),
(r#"{"foo": null}"#, "null not in array"),
(r#"{"x": [{"exists": 23}]}"#, "exists with number"),
(r#"{"x": [{"exists": "yes"}]}"#, "exists with string"),
(r#"{"x": [{"shellstyle": 15}]}"#, "shellstyle with number"),
(r#"{"x": [{"shellstyle": "a**b"}]}"#, "shellstyle with **"),
(r#"{"x": [{"prefix": 23}]}"#, "prefix with number"),
(r#"{"x": [{"suffix": 23}]}"#, "suffix with number"),
(
r#"{"x": [{"equals-ignore-case": 5}]}"#,
"equals-ignore-case with number",
),
(r#"{"x": [{"numeric": ">=5"}]}"#, "numeric with string"),
(
r#"{"x": [{"regex": "[invalid"}]}"#,
"regex with invalid pattern",
),
(r#"{"x": [{"unknown-op": "val"}]}"#, "unknown operator"),
];
for (pattern, desc) in &invalid_patterns {
let mut q = Quamina::new();
let result = q.add_pattern("test", pattern);
assert!(result.is_err(), "{} should be rejected: {}", desc, pattern);
}
}
#[test]
fn test_numbits_boundary_values() {
use crate::numbits::{numbits_from_f64, q_num_from_f64, to_q_number};
let nb_zero = numbits_from_f64(0.0);
let q_zero = q_num_from_f64(0.0);
assert!(nb_zero > 0, "Zero should have non-zero numbits");
assert!(!q_zero.is_empty(), "Zero should have non-empty Q-number");
let smallest_subnormal = 5e-324_f64;
let nb_small = numbits_from_f64(smallest_subnormal);
let q_small = q_num_from_f64(smallest_subnormal);
assert!(nb_small > nb_zero, "Smallest subnormal > 0");
assert!(
q_small > q_zero,
"Smallest subnormal Q-number > zero Q-number"
);
let smallest_normal = f64::MIN_POSITIVE;
let nb_min_normal = numbits_from_f64(smallest_normal);
let q_min_normal = q_num_from_f64(smallest_normal);
assert!(
nb_min_normal > nb_small,
"Smallest normal > smallest subnormal"
);
assert!(q_min_normal > q_small, "Q-number ordering preserved");
let largest_normal = f64::MAX;
let nb_max = numbits_from_f64(largest_normal);
let q_max = q_num_from_f64(largest_normal);
assert!(nb_max > nb_min_normal, "Max > min positive");
assert!(q_max > q_min_normal, "Q-number ordering preserved");
let nb_neg_max = numbits_from_f64(-f64::MAX);
let nb_neg_min = numbits_from_f64(-f64::MIN_POSITIVE);
let nb_neg_small = numbits_from_f64(-5e-324_f64);
assert!(nb_neg_max < nb_neg_min, "-MAX < -MIN_POSITIVE");
assert!(nb_neg_min < nb_neg_small, "-MIN_POSITIVE < -subnormal");
assert!(nb_neg_small < nb_zero, "-subnormal < 0");
let test_values = [
0.0,
1.0,
-1.0,
f64::MIN_POSITIVE,
f64::MAX,
-f64::MAX,
5e-324,
-5e-324,
1e100,
-1e100,
0.5,
-0.5,
];
for &val in &test_values {
let q = q_num_from_f64(val);
assert_eq!(
q[0],
crate::numbits::Q_NUMBER_PREFIX,
"Q-number should start with prefix for value {}",
val
);
for &byte in &q[1..] {
assert!(
byte < 128,
"Q-number content byte {} >= 128 for value {}",
byte,
val
);
}
}
for &val in &test_values {
let nb = numbits_from_f64(val);
let q1 = q_num_from_f64(val);
let q2 = to_q_number(nb);
assert_eq!(q1, q2, "Q-number should match via both paths for {}", val);
}
}
#[test]
#[cfg_attr(miri, ignore)]
fn test_numbits_to_qnumber_utf8() {
use crate::numbits::q_num_from_f64;
let mut rng_state = 0xDEADBEEF_u64;
for i in 0..10_000 {
rng_state = rng_state
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
let sign = if rng_state & 1 == 0 { 1.0 } else { -1.0 };
let exp = ((rng_state >> 1) % 600) as i32 - 300; let mantissa = ((rng_state >> 10) as f64) / (1u64 << 54) as f64;
let val = sign * (1.0 + mantissa) * 10f64.powi(exp);
if !val.is_finite() {
continue;
}
let q = q_num_from_f64(val);
assert!(
!q.is_empty(),
"Q-number should be non-empty for value at index {}",
i
);
assert_eq!(
q[0],
crate::numbits::Q_NUMBER_PREFIX,
"Q-number should start with prefix for value at index {}",
i
);
for (j, &byte) in q[1..].iter().enumerate() {
assert!(
byte < 128,
"Q-number content byte {} at pos {} >= 128 for value at index {}",
byte,
j + 1,
i
);
}
assert!(
std::str::from_utf8(&q[1..]).is_ok(),
"Q-number content should be valid UTF-8 for value at index {}",
i
);
assert!(
q.len() <= 11,
"Q-number length {} exceeds max 11 for value at index {}",
q.len(),
i
);
}
let mut prev_val = f64::NEG_INFINITY;
let mut prev_q = q_num_from_f64(-1e308);
rng_state = 0x12345678_u64;
let mut ordered_vals: Vec<f64> = Vec::new();
for _ in 0..1000 {
rng_state = rng_state.wrapping_mul(6364136223846793005).wrapping_add(1);
let val = ((rng_state as f64) / (u64::MAX as f64)) * 2e100 - 1e100;
if val.is_finite() {
ordered_vals.push(val);
}
}
ordered_vals.sort_by(|a, b| a.partial_cmp(b).unwrap());
for val in ordered_vals {
let q = q_num_from_f64(val);
if prev_val < val {
assert!(
prev_q <= q,
"Q-number ordering violated: {} ({:?}) should be <= {} ({:?})",
prev_val,
prev_q,
val,
q
);
}
prev_val = val;
prev_q = q;
}
}
#[test]
fn test_multi_condition_pattern_fields() {
use crate::json::{LookaroundCondition, MultiConditionPattern};
use crate::regexp::parse_regexp;
let primary = parse_regexp("foo").unwrap();
let combined = parse_regexp("foobar").unwrap();
let conditions = vec![LookaroundCondition::PositiveLookahead(combined)];
let mc = MultiConditionPattern::new(primary, conditions);
assert_eq!(mc.primary.len(), 1, "Primary should have 1 branch");
assert_eq!(mc.conditions.len(), 1, "Should have 1 condition");
assert!(!mc.conditions[0].is_negative(), "Should be positive");
assert!(
!mc.conditions[0].is_lookbehind(),
"Should not be lookbehind"
);
}
#[test]
fn test_condition_cost_ordering() {
use crate::json::{LookaroundCondition, MultiConditionPattern};
use crate::regexp::parse_regexp;
let primary = parse_regexp("test").unwrap();
let pattern1 = parse_regexp("a").unwrap();
let pattern2 = parse_regexp("b").unwrap();
let pattern3 = parse_regexp("c").unwrap();
let conditions = vec![
LookaroundCondition::NegativeLookbehind {
pattern: pattern3.clone(),
byte_length: 1,
}, LookaroundCondition::PositiveLookbehind {
pattern: pattern2.clone(),
byte_length: 1,
}, LookaroundCondition::NegativeLookahead(pattern1.clone()), ];
let mc = MultiConditionPattern::new(primary, conditions);
assert_eq!(
mc.conditions[0].cost_estimate(),
20,
"First should be cost 20"
);
assert_eq!(
mc.conditions[1].cost_estimate(),
30,
"Second should be cost 30"
);
assert_eq!(
mc.conditions[2].cost_estimate(),
40,
"Third should be cost 40"
);
}
#[test]
fn test_string_number_type_distinction() {
let q = q!("string_pat" => r#"{"key": ["123"]}"#);
assert_matches!(
q,
r#"{"key": "123"}"#,
vec!["string_pat"],
"String '123' should match string pattern '123'"
);
assert_no_match!(
q,
r#"{"key": 123}"#,
"Number 123 should NOT match string pattern '123' - type distinction must be preserved"
);
}
#[test]
fn test_numeric_pattern_should_not_match_string_event() {
let q = q!("num_pat" => r#"{"key": [42]}"#);
assert_matches!(
q,
r#"{"key": 42}"#,
vec!["num_pat"],
"Number 42 should match numeric pattern"
);
assert_no_match!(
q,
r#"{"key": "42"}"#,
"String '42' should NOT match numeric pattern 42"
);
}
#[test]
fn test_mixed_string_and_number_patterns_same_digits() {
let q = q!("str" => r#"{"key": ["123"]}"#, "num" => r#"{"key": [123]}"#);
assert_matches!(
q,
r#"{"key": "123"}"#,
vec!["str"],
"String '123' should match only string pattern"
);
assert_matches!(
q,
r#"{"key": 123}"#,
vec!["num"],
"Number 123 should match only numeric pattern"
);
}
#[test]
fn test_mixed_number_and_string_in_same_value_array() {
let q = q!("p1" => r#"{"a": [1, 2], "b": [1, "3"]}"#);
assert_matches!(
q,
r#"{"a": 1, "b": "3"}"#,
vec!["p1"],
"String '3' should match the string literal in [1, \"3\"]"
);
assert_matches!(
q,
r#"{"a": 2, "b": 1}"#,
vec!["p1"],
"Number 1 should match the numeric literal in [1, \"3\"]"
);
assert_no_match!(
q,
r#"{"a": 1, "b": 3}"#,
"Number 3 should NOT match string '3' in [1, \"3\"]"
);
assert_matches!(
q,
r#"{"b": "3", "a": 1}"#,
vec!["p1"],
"Reversed field order should still match"
);
assert_matches!(
q,
r#"{"a": 2, "b": "3", "x": 99}"#,
vec!["p1"],
"Extra fields should not prevent match"
);
assert_no_match!(q, r#"{"a": 1}"#, "Missing field b should not match");
assert_no_match!(q, r#"{"b": "3"}"#, "Missing field a should not match");
assert_no_match!(
q,
r#"{"b": "3", "a": 6}"#,
"Wrong value on field a should not match"
);
}
#[test]
fn test_numeric_range_should_not_match_string_event() {
let mut q = Quamina::<&str>::new();
q.add_pattern("gt10", r#"{"val": [{"numeric": [">", 10]}]}"#)
.unwrap();
assert_no_match!(
q,
r#"{"val": "hello"}"#,
"String 'hello' should NOT match numeric range > 10"
);
assert_no_match!(
q,
r#"{"val": "999"}"#,
"String '999' should NOT match numeric range > 10"
);
}
#[test]
fn test_numeric_range_should_match_numeric_event() {
let mut q = Quamina::<&str>::new();
q.add_pattern("gt10", r#"{"val": [{"numeric": [">", 10]}]}"#)
.unwrap();
assert_matches!(q, r#"{"val": 50}"#, vec!["gt10"]);
assert_matches!(q, r#"{"val": 100.5}"#, vec!["gt10"]);
assert_no_match!(q, r#"{"val": 5}"#);
assert_no_match!(q, r#"{"val": 10}"#); }
#[test]
fn test_numeric_exact_still_works_with_prefix() {
let q = q!("n42" => r#"{"key": [42]}"#);
assert_matches!(q, r#"{"key": 42}"#, vec!["n42"]);
assert_no_match!(q, r#"{"key": "42"}"#);
assert_no_match!(q, r#"{"key": 43}"#);
}
#[test]
fn test_empty_matcher_returns_no_matches() {
let q = Quamina::<&str>::new();
assert_no_match!(
q,
r#"{"status": "active"}"#,
"Empty matcher should return no matches"
);
assert_no_match!(
q,
r#"{"a": 1, "b": "hello"}"#,
"Empty matcher should return no matches for any event"
);
}
#[test]
fn test_idempotent_add_and_delete() {
let mut q = Quamina::new();
q.add_pattern("p1", r#"{"x": ["a"]}"#).unwrap();
q.add_pattern("p1", r#"{"x": ["a"]}"#).unwrap();
assert_matches!(
q,
r#"{"x": "a"}"#,
vec!["p1"],
"Duplicate add should still match"
);
q.delete_patterns(&"p1").unwrap();
assert_no_match!(q, r#"{"x": "a"}"#, "After delete, should not match");
q.delete_patterns(&"p1").unwrap();
assert_no_match!(q, r#"{"x": "a"}"#, "Second delete should be idempotent");
let purged = q.rebuild();
assert!(purged >= 1, "Rebuild should purge the deleted pattern(s)");
}
#[test]
fn test_delete_multi_pattern_id_removes_all() {
let mut q = Quamina::new();
q.add_pattern("shared", r#"{"x": ["a"]}"#).unwrap();
q.add_pattern("shared", r#"{"x": [1]}"#).unwrap();
q.add_pattern("shared", r#"{"y": [{"prefix": "b"}]}"#)
.unwrap();
assert_matches!(
q,
r#"{"x": "a"}"#,
vec!["shared"],
"String pattern should match"
);
assert_matches!(
q,
r#"{"x": 1}"#,
vec!["shared"],
"Numeric pattern should match"
);
assert_matches!(
q,
r#"{"y": "bcd"}"#,
vec!["shared"],
"Prefix pattern should match"
);
q.delete_patterns(&"shared").unwrap();
assert_no_match!(
q,
r#"{"x": "a"}"#,
"String pattern should be gone after delete"
);
assert_no_match!(
q,
r#"{"x": 1}"#,
"Numeric pattern should be gone after delete"
);
assert_no_match!(
q,
r#"{"y": "bcd"}"#,
"Prefix pattern should be gone after delete"
);
let purged = q.rebuild();
assert_eq!(purged, 1, "Rebuild should purge 1 deleted ID");
assert_no_match!(
q,
r#"{"x": "a"}"#,
"String pattern should stay gone after rebuild"
);
assert_no_match!(
q,
r#"{"x": 1}"#,
"Numeric pattern should stay gone after rebuild"
);
assert_no_match!(
q,
r#"{"y": "bcd"}"#,
"Prefix pattern should stay gone after rebuild"
);
}
#[test]
fn test_list_pattern_ids_basic() {
let mut q: Quamina<String> = Quamina::new();
assert!(q.list_pattern_ids().is_empty());
q.add_pattern("p1".into(), r#"{"x": [1]}"#).unwrap();
q.add_pattern("p2".into(), r#"{"y": [2]}"#).unwrap();
let mut ids: Vec<&String> = q.list_pattern_ids();
ids.sort();
assert_eq!(ids, vec!["p1", "p2"]);
}
#[test]
fn test_list_pattern_ids_excludes_deleted() {
let mut q: Quamina<String> = Quamina::new();
q.add_pattern("p1".into(), r#"{"x": [1]}"#).unwrap();
q.add_pattern("p2".into(), r#"{"y": [2]}"#).unwrap();
q.add_pattern("p3".into(), r#"{"z": [3]}"#).unwrap();
q.delete_patterns(&"p2".into()).unwrap();
let mut ids: Vec<&String> = q.list_pattern_ids();
ids.sort();
assert_eq!(ids, vec!["p1", "p3"]);
}
#[test]
fn test_contains_pattern_basic() {
let mut q: Quamina<String> = Quamina::new();
let p1: String = "p1".into();
let p2: String = "p2".into();
let missing: String = "missing".into();
assert!(!q.contains_pattern(&p1));
q.add_pattern(p1.clone(), r#"{"x": [1]}"#).unwrap();
q.add_pattern(p2.clone(), r#"{"y": [2]}"#).unwrap();
assert!(q.contains_pattern(&p1));
assert!(q.contains_pattern(&p2));
assert!(!q.contains_pattern(&missing));
}
#[test]
fn test_contains_pattern_after_delete() {
let mut q: Quamina<String> = Quamina::new();
let p1: String = "p1".into();
q.add_pattern(p1.clone(), r#"{"x": [1]}"#).unwrap();
assert!(q.contains_pattern(&p1));
q.delete_patterns(&p1).unwrap();
assert!(!q.contains_pattern(&p1));
}
#[test]
fn test_delete_nonexistent_pattern_is_noop() {
let mut q: Quamina<String> = Quamina::new();
q.add_pattern("p1".into(), r#"{"x": [1]}"#).unwrap();
q.delete_patterns(&"ghost".into()).unwrap();
assert!(q.contains_pattern(&"p1".into()));
assert_eq!(q.list_pattern_ids().len(), 1);
assert_eq!(q.rebuild(), 0);
}
#[test]
fn test_delete_already_deleted_pattern_is_noop() {
let mut q: Quamina<String> = Quamina::new();
q.add_pattern("p1".into(), r#"{"x": [1]}"#).unwrap();
q.delete_patterns(&"p1".into()).unwrap();
assert!(!q.contains_pattern(&"p1".into()));
q.delete_patterns(&"p1".into()).unwrap();
assert!(!q.contains_pattern(&"p1".into()));
assert!(q.list_pattern_ids().is_empty());
}
#[test]
fn test_pattern_depth_at_limit() {
let mut q = Quamina::new();
let mut pattern = String::new();
let mut closing = String::new();
for i in 0..256 {
pattern.push_str(&format!("{{\"f{}\": ", i));
closing.push('}');
}
pattern.push_str("[\"val\"]");
pattern.push_str(&closing);
assert!(
q.add_pattern("deep", &pattern).is_ok(),
"Pattern at exactly max depth (256) should succeed"
);
}
#[test]
fn test_pattern_depth_exceeds_limit() {
let mut q = Quamina::new();
let mut pattern = String::new();
let mut closing = String::new();
for i in 0..257 {
pattern.push_str(&format!("{{\"f{}\": ", i));
closing.push('}');
}
pattern.push_str("[\"val\"]");
pattern.push_str(&closing);
let result = q.add_pattern("deep", &pattern);
assert!(result.is_err(), "Pattern exceeding max depth should fail");
let err_msg = format!("{}", result.unwrap_err());
assert!(
err_msg.contains("depth"),
"Error should mention depth: {}",
err_msg
);
assert!(
err_msg.contains("257"),
"Error should mention actual depth 257: {}",
err_msg
);
assert!(
err_msg.contains("256"),
"Error should mention max depth 256: {}",
err_msg
);
}
#[test]
fn test_pattern_depth_custom_limit() {
let mut q = QuaminaBuilder::<&str>::new()
.with_max_pattern_depth(5)
.build()
.unwrap();
let pattern = r#"{"a": {"b": {"c": {"d": {"e": {"f": ["val"]}}}}}}"#;
let result = q.add_pattern("deep", pattern);
assert!(
result.is_err(),
"Pattern at depth 6 should fail with max_depth=5"
);
let err_msg = format!("{}", result.unwrap_err());
assert!(err_msg.contains("depth"), "Error should mention depth");
}
#[test]
fn test_pattern_depth_shallow_ok() {
let mut q = Quamina::new();
let result = q.add_pattern("p1", r#"{"a": {"b": {"c": ["value"]}}}"#);
assert!(
result.is_ok(),
"Normal 3-level nesting should succeed with defaults"
);
}
#[test]
#[cfg_attr(miri, ignore)]
fn test_pattern_fields_at_limit() {
let mut q = Quamina::new();
let mut fields: Vec<String> = Vec::new();
for i in 0..256 {
fields.push(format!("\"f{}\": [\"v\"]", i));
}
let pattern = format!("{{{}}}", fields.join(", "));
assert!(
q.add_pattern("wide", &pattern).is_ok(),
"Pattern with exactly 256 fields should succeed"
);
}
#[test]
fn test_pattern_fields_at_limit_miri_friendly() {
let mut q = Quamina::new();
let mut fields: Vec<String> = Vec::new();
for i in 0..8 {
fields.push(format!("\"f{}\": [\"v\"]", i));
}
let pattern = format!("{{{}}}", fields.join(", "));
assert!(
q.add_pattern("wide", &pattern).is_ok(),
"Pattern with 8 fields should succeed"
);
}
#[test]
#[cfg_attr(miri, ignore)]
fn test_pattern_fields_exceeds_limit() {
let mut q = Quamina::new();
let mut fields: Vec<String> = Vec::new();
for i in 0..257 {
fields.push(format!("\"f{}\": [\"v\"]", i));
}
let pattern = format!("{{{}}}", fields.join(", "));
let result = q.add_pattern("wide", &pattern);
assert!(
result.is_err(),
"Pattern with 257 fields should exceed limit"
);
let err_msg = format!("{}", result.unwrap_err());
assert!(
err_msg.contains("257"),
"Error should mention actual count 257: {}",
err_msg
);
assert!(
err_msg.contains("256"),
"Error should mention max count 256: {}",
err_msg
);
}
#[test]
fn test_pattern_fields_custom_limit() {
let mut q = QuaminaBuilder::<&str>::new()
.with_max_fields_per_pattern(3)
.build()
.unwrap();
let pattern = r#"{"a": ["1"], "b": ["2"], "c": ["3"], "d": ["4"]}"#;
let result = q.add_pattern("wide", pattern);
assert!(
result.is_err(),
"Pattern with 4 fields should fail with max_fields=3"
);
let err_msg = format!("{}", result.unwrap_err());
assert!(
err_msg.contains("fields"),
"Error should mention fields: {}",
err_msg
);
}
#[test]
fn test_arena_budget_exceeded() {
let mut q = QuaminaBuilder::<&str>::new()
.with_arena_byte_budget(1024)
.build()
.unwrap();
let _ = q.add_pattern("p1", r#"{"x": ["a"]}"#);
let _ = q.add_pattern("p2", r#"{"x": ["b"]}"#);
let mut exceeded = false;
for i in 0..100 {
let pattern = format!("{{\"x\": [\"value_that_is_long_enough_{}\"]}}", i);
if q.add_pattern("px", &pattern).is_err() {
exceeded = true;
break;
}
}
assert!(exceeded, "Arena budget should be exceeded with 1KB limit");
}
#[test]
fn test_arena_budget_sufficient() {
let mut q = Quamina::new();
for i in 0..50 {
let pattern = format!("{{\"field{}\": [\"value{}\"]}}", i, i);
assert!(
q.add_pattern("p1", &pattern).is_ok(),
"Normal patterns should work within default 10MB budget"
);
}
}
#[test]
fn test_arena_budget_custom() {
let mut q = QuaminaBuilder::<&str>::new()
.with_arena_byte_budget(1024 * 1024)
.build()
.unwrap();
for i in 0..20 {
let pattern = format!("{{\"field{}\": [\"value{}\"]}}", i, i);
assert!(
q.add_pattern("p1", &pattern).is_ok(),
"Moderate patterns should work within 1MB budget"
);
}
}
#[test]
fn test_depth_error_includes_path() {
let mut q = QuaminaBuilder::<&str>::new()
.with_max_pattern_depth(2)
.build()
.unwrap();
let pattern = r#"{"a": {"b": {"c": ["val"]}}}"#;
let result = q.add_pattern("deep", pattern);
assert!(result.is_err());
let err_msg = format!("{}", result.unwrap_err());
assert!(
err_msg.contains("pattern too complex"),
"Error should start with 'pattern too complex': {}",
err_msg
);
}
#[test]
fn test_field_count_error_includes_count() {
let mut q = QuaminaBuilder::<&str>::new()
.with_max_fields_per_pattern(2)
.build()
.unwrap();
let pattern = r#"{"a": ["1"], "b": ["2"], "c": ["3"]}"#;
let result = q.add_pattern("wide", pattern);
assert!(result.is_err());
let err_msg = format!("{}", result.unwrap_err());
assert!(
err_msg.contains("3"),
"Error should contain actual field count: {}",
err_msg
);
assert!(
err_msg.contains("2"),
"Error should contain max field count: {}",
err_msg
);
}
#[test]
fn test_arena_error_includes_bytes() {
let mut q = QuaminaBuilder::<&str>::new()
.with_arena_byte_budget(1)
.build()
.unwrap();
let _ = q.add_pattern("p1", r#"{"x": ["a"]}"#);
let result = q.add_pattern("p2", r#"{"x": ["b"]}"#);
if let Err(e) = result {
let err_msg = format!("{}", e);
assert!(
err_msg.contains("bytes") && err_msg.contains("budget"),
"Error should mention bytes and budget: {}",
err_msg
);
}
}
#[test]
#[cfg_attr(miri, ignore)]
fn test_default_limits_allow_normal_patterns() {
let mut q = Quamina::new();
assert!(q.add_pattern("exact", r#"{"x": ["hello"]}"#).is_ok());
assert!(q.add_pattern("num", r#"{"x": [42]}"#).is_ok());
assert!(q
.add_pattern("prefix", r#"{"x": [{"prefix": "he"}]}"#)
.is_ok());
assert!(q
.add_pattern("suffix", r#"{"x": [{"suffix": "lo"}]}"#)
.is_ok());
assert!(q
.add_pattern("shell", r#"{"x": [{"shellstyle": "h*o"}]}"#)
.is_ok());
assert!(q
.add_pattern("wild", r#"{"x": [{"wildcard": "h*o"}]}"#)
.is_ok());
assert!(q
.add_pattern("ab", r#"{"x": [{"anything-but": ["no"]}]}"#)
.is_ok());
assert!(q
.add_pattern("eic", r#"{"x": [{"equals-ignore-case": "HELLO"}]}"#)
.is_ok());
assert!(q
.add_pattern("re", r#"{"x": [{"regex": "[a-z]+"}]}"#)
.is_ok());
assert!(q
.add_pattern("numr", r#"{"x": [{"numeric": [">=", 1, "<", 100]}]}"#)
.is_ok());
assert!(q
.add_pattern("cidr", r#"{"x": [{"cidr": "10.0.0.0/8"}]}"#)
.is_ok());
assert!(q
.add_pattern("exists", r#"{"x": [{"exists": true}]}"#)
.is_ok());
}
#[test]
fn test_default_limits_allow_normal_patterns_miri_friendly() {
let mut q = Quamina::new();
assert!(q.add_pattern("exact", r#"{"x": ["hello"]}"#).is_ok());
assert!(q.add_pattern("num", r#"{"x": [42]}"#).is_ok());
assert!(q
.add_pattern("prefix", r#"{"x": [{"prefix": "he"}]}"#)
.is_ok());
assert!(q
.add_pattern("suffix", r#"{"x": [{"suffix": "lo"}]}"#)
.is_ok());
assert!(q
.add_pattern("shell", r#"{"x": [{"shellstyle": "h*o"}]}"#)
.is_ok());
assert!(q
.add_pattern("wild", r#"{"x": [{"wildcard": "h*o"}]}"#)
.is_ok());
assert!(q
.add_pattern("ab", r#"{"x": [{"anything-but": ["no"]}]}"#)
.is_ok());
assert!(q
.add_pattern("eic", r#"{"x": [{"equals-ignore-case": "HELLO"}]}"#)
.is_ok());
assert!(q
.add_pattern("numr", r#"{"x": [{"numeric": [">=", 1, "<", 100]}]}"#)
.is_ok());
assert!(q
.add_pattern("exists", r#"{"x": [{"exists": true}]}"#)
.is_ok());
}
#[test]
fn test_arena_budget_enforced_on_repeated_exact_strings() {
let mut q = QuaminaBuilder::<&str>::new()
.with_arena_byte_budget(4096)
.build()
.unwrap();
let mut rejected = false;
for i in 0..500 {
let pattern = format!(r#"{{"x": ["long_value_string_number_{}"]}}"#, i);
if q.add_pattern("p", &pattern).is_err() {
rejected = true;
break;
}
}
assert!(
rejected,
"Budget should be enforced when many exact strings are added to the same field"
);
}
#[test]
fn test_matcher_correct_after_rejected_pattern() {
let mut q = QuaminaBuilder::<&str>::new()
.with_arena_byte_budget(4096)
.build()
.unwrap();
q.add_pattern("good", r#"{"x": ["hello"]}"#).unwrap();
let mut rejected = false;
for i in 0..500 {
let pattern = format!(r#"{{"x": ["overflow_value_{}"]}}"#, i);
if q.add_pattern("bad", &pattern).is_err() {
rejected = true;
break;
}
}
assert!(rejected, "Should have hit budget limit");
assert_has_match!(q, r#"{"x": "hello"}"#, "good");
assert_no_match!(
q,
r#"{"x": "nope"}"#,
"Non-matching event must not produce false positives"
);
}
#[test]
fn test_clone_preserves_arena_budget() {
let mut q = QuaminaBuilder::<String>::new()
.with_arena_byte_budget(4096)
.build()
.unwrap();
q.add_pattern("a".into(), r#"{"x": ["val"]}"#).unwrap();
let mut cloned = q.clone();
let mut rejected = false;
for i in 0..500 {
let pattern = format!(r#"{{"x": ["clone_test_value_{}"]}}"#, i);
if cloned.add_pattern("b".into(), &pattern).is_err() {
rejected = true;
break;
}
}
assert!(
rejected,
"Cloned instance must enforce the original arena budget"
);
}
#[test]
fn test_errors_return_pattern_too_complex_variant() {
let mut q = QuaminaBuilder::<&str>::new()
.with_max_pattern_depth(1)
.build()
.unwrap();
let result = q.add_pattern("deep", r#"{"a": {"b": ["val"]}}"#);
assert!(
matches!(result, Err(QuaminaError::PatternTooComplex(_))),
"Depth violation must return PatternTooComplex, got {:?}",
result
);
let mut q2 = QuaminaBuilder::<&str>::new()
.with_max_fields_per_pattern(1)
.build()
.unwrap();
let result = q2.add_pattern("wide", r#"{"a": ["1"], "b": ["2"]}"#);
assert!(
matches!(result, Err(QuaminaError::PatternTooComplex(_))),
"Field count violation must return PatternTooComplex, got {:?}",
result
);
}
#[test]
#[should_panic(expected = "max_pattern_depth must be at least 1")]
fn test_zero_depth_panics() {
QuaminaBuilder::<&str>::new().with_max_pattern_depth(0);
}
#[test]
#[should_panic(expected = "max_fields_per_pattern must be at least 1")]
fn test_zero_fields_panics() {
QuaminaBuilder::<&str>::new().with_max_fields_per_pattern(0);
}
#[test]
#[should_panic(expected = "arena_byte_budget must be at least 1")]
fn test_zero_budget_panics() {
QuaminaBuilder::<&str>::new().with_arena_byte_budget(0);
}
#[test]
#[should_panic(expected = "max_states_per_pattern must be at least 1")]
fn test_zero_states_panics() {
QuaminaBuilder::<&str>::new().with_max_states_per_pattern(0);
}
#[test]
fn test_state_limit_exceeded() {
let mut q = QuaminaBuilder::<&str>::new()
.with_max_states_per_pattern(2)
.build()
.unwrap();
let r1 = q.add_pattern("ok", r#"{"a": ["x", {"prefix": "y"}]}"#);
assert!(r1.is_ok(), "2 states should be within limit of 2");
let r2 = q.add_pattern(
"bad",
r#"{"a": ["x", {"prefix": "y"}], "b": ["m", {"prefix": "n"}]}"#,
);
assert!(r2.is_err(), "4 states should exceed limit of 2");
assert!(
r2.unwrap_err()
.to_string()
.contains("field-matcher state count"),
"error should mention state count"
);
}
#[test]
fn test_state_limit_default_allows_normal_patterns() {
let mut q = Quamina::new();
assert!(q
.add_pattern("p1", r#"{"status": ["active", {"prefix": "pend"}]}"#)
.is_ok());
assert!(q
.add_pattern("p2", r#"{"a": ["1"], "b": ["2"], "c": ["3"]}"#)
.is_ok());
let matches = q
.matches_for_event(r#"{"status": "active"}"#.as_bytes())
.unwrap();
assert!(matches.contains(&&"p1"));
let matches = q
.matches_for_event(r#"{"status": "pending"}"#.as_bytes())
.unwrap();
assert!(matches.contains(&&"p1"));
}
#[test]
fn test_flatten_only_returns_field_count() {
let q = q!("p1" => r#"{"x": [1], "y": [2]}"#);
let count = q.flatten_only(br#"{"x": 1, "y": 2}"#).unwrap();
assert_eq!(count, 2, "two tracked fields should produce count 2");
}
#[test]
fn test_flatten_only_single_field() {
let q = q!("p1" => r#"{"status": ["ok"]}"#);
let count = q.flatten_only(br#"{"status": "ok"}"#).unwrap();
assert_eq!(count, 1, "single tracked field should produce count 1");
}
#[test]
fn test_flatten_only_untracked_fields_ignored() {
let q = q!("p1" => r#"{"x": [1]}"#);
let count = q.flatten_only(br#"{"x": 1, "y": 2}"#).unwrap();
assert_eq!(count, 1, "untracked field y should not be counted");
}
#[test]
fn test_pruner_stats_unit_add_and_read() {
use super::PrunerStats;
let stats = PrunerStats::new();
assert_eq!(stats.emitted(), 0, "initial emitted must be 0");
assert_eq!(stats.filtered(), 0, "initial filtered must be 0");
stats.add_emitted(5);
assert_eq!(stats.emitted(), 5, "emitted must be 5 after add_emitted(5)");
assert_eq!(stats.filtered(), 0, "filtered must still be 0");
stats.add_filtered(3);
assert_eq!(
stats.filtered(),
3,
"filtered must be 3 after add_filtered(3)"
);
assert_eq!(stats.emitted(), 5, "emitted must still be 5");
stats.add_emitted(2);
assert_eq!(stats.emitted(), 7, "emitted must accumulate to 7");
stats.add_filtered(4);
assert_eq!(stats.filtered(), 7, "filtered must accumulate to 7");
stats.reset();
assert_eq!(stats.emitted(), 0, "emitted must be 0 after reset");
assert_eq!(stats.filtered(), 0, "filtered must be 0 after reset");
}
#[test]
fn test_should_rebuild_boundary_cases() {
use super::PrunerStats;
let s = PrunerStats::new();
assert!(!s.should_rebuild(), "no activity: must not rebuild");
let s = PrunerStats::new();
s.add_emitted(500);
s.add_filtered(499);
assert!(
!s.should_rebuild(),
"total=999 below threshold: must not rebuild"
);
let s = PrunerStats::new();
s.add_emitted(600);
s.add_filtered(400);
assert!(s.should_rebuild(), "total=1000, ratio=0.67: must rebuild");
let s = PrunerStats::new();
s.add_emitted(900);
s.add_filtered(100);
assert!(
!s.should_rebuild(),
"ratio=0.111 below 0.2: must not rebuild"
);
let s = PrunerStats::new();
s.add_emitted(1000);
s.add_filtered(200);
assert!(
!s.should_rebuild(),
"ratio=0.2 exactly: strict > means no rebuild"
);
let s = PrunerStats::new();
s.add_emitted(1000);
s.add_filtered(201);
assert!(s.should_rebuild(), "ratio=0.201 above 0.2: must rebuild");
let s = PrunerStats::new();
s.add_filtered(1500);
assert!(
!s.should_rebuild(),
"emitted=0: must not rebuild (div-by-zero guard)"
);
let s = PrunerStats::new();
s.add_emitted(600);
s.add_filtered(500);
assert!(s.should_rebuild(), "total=1100, ratio=0.83: must rebuild");
let s = PrunerStats::new();
s.add_emitted(1);
s.add_filtered(999);
assert!(s.should_rebuild(), "total=1000, emitted=1: must rebuild");
}
#[test]
fn test_pruner_stats_clone_preserves_values() {
use super::PrunerStats;
let stats = PrunerStats::new();
stats.add_emitted(100);
stats.add_filtered(50);
let cloned = stats.clone();
assert_eq!(cloned.emitted(), 100, "clone must preserve emitted");
assert_eq!(cloned.filtered(), 50, "clone must preserve filtered");
}
#[test]
fn test_builder_with_media_type_sets_validated_flag() {
let flattener = MockFlattener::new(vec![]);
let result = QuaminaBuilder::<String>::new()
.with_media_type("application/json")
.unwrap()
.with_flattener(Box::new(flattener));
assert!(
result.is_err(),
"with_flattener must fail after with_media_type"
);
}
#[test]
fn test_builder_with_flattener_is_used() {
let flattener = MockFlattener::new(vec![OwnedField {
path: b"k".to_vec(),
val: b"\"v\"".to_vec(),
array_trail: vec![],
is_number: false,
}]);
let mut q = QuaminaBuilder::<String>::new()
.with_flattener(Box::new(flattener))
.unwrap()
.build()
.unwrap();
q.add_pattern("p".to_string(), r#"{"k": ["v"]}"#).unwrap();
let matches = q.matches_for_event(b"not json").unwrap();
assert_eq!(
matches,
vec!["p".to_string()],
"custom flattener must be used"
);
}
#[test]
fn test_builder_with_flattener_blocks_second_call() {
let f1 = MockFlattener::new(vec![]);
let f2 = MockFlattener::new(vec![]);
let result = QuaminaBuilder::<String>::new()
.with_flattener(Box::new(f1))
.unwrap()
.with_flattener(Box::new(f2));
assert!(result.is_err(), "second with_flattener must fail");
}
#[test]
fn test_builder_with_max_pattern_depth_is_applied() {
let mut q = QuaminaBuilder::<&str>::new()
.with_max_pattern_depth(2)
.build()
.unwrap();
let result = q.add_pattern("p", r#"{"a": {"b": {"c": ["v"]}}}"#);
assert!(
result.is_err(),
"depth-3 pattern must be rejected with max_depth=2"
);
let result = q.add_pattern("p", r#"{"a": {"b": ["v"]}}"#);
assert!(
result.is_ok(),
"depth-2 pattern must succeed with max_depth=2"
);
}
#[test]
fn test_builder_with_max_fields_per_pattern_is_applied() {
let mut q = QuaminaBuilder::<&str>::new()
.with_max_fields_per_pattern(2)
.build()
.unwrap();
let result = q.add_pattern("p", r#"{"a": ["1"], "b": ["2"], "c": ["3"]}"#);
assert!(
result.is_err(),
"3-field pattern must be rejected with max_fields=2"
);
let result = q.add_pattern("p", r#"{"a": ["1"], "b": ["2"]}"#);
assert!(
result.is_ok(),
"2-field pattern must succeed with max_fields=2"
);
}
#[test]
fn test_builder_with_arena_byte_budget_is_applied() {
let mut q = QuaminaBuilder::<&str>::new()
.with_arena_byte_budget(1)
.build()
.unwrap();
let r1 = q.add_pattern("p1", r#"{"x": ["a"]}"#);
let r2 = q.add_pattern("p2", r#"{"x": [{"prefix": "b"}]}"#);
assert!(
r1.is_err() || r2.is_err(),
"at least one pattern must be rejected with 1-byte arena budget"
);
}
#[test]
fn test_builder_with_max_states_per_pattern_is_applied() {
let mut q = QuaminaBuilder::<&str>::new()
.with_max_states_per_pattern(1)
.build()
.unwrap();
let result = q.add_pattern("p", r#"{"a": ["x", {"prefix": "y"}]}"#);
assert!(
result.is_err(),
"mixed-type pattern must be rejected with max_states=1"
);
}
#[test]
fn test_builder_with_auto_rebuild_is_applied() {
let q = QuaminaBuilder::<String>::new()
.with_auto_rebuild(false)
.build()
.unwrap();
assert!(!q.auto_rebuild_enabled(), "auto_rebuild must be false");
}
struct StatsWorkload {
name: &'static str,
patterns: &'static [&'static str], regexps: &'static [&'static str], state_count: u32,
total_closure_entries: u32,
max_closure_len: u16,
matches: [usize; 3], }
const STATS_WORKLOADS: &[StatsWorkload] = &[
StatsWorkload {
name: "6-regexps-12-shell",
patterns: &[
"*a*b*c*", "*x*y*z*", "*e*f*g*", "*m*n*o*", "*p*q*r*", "*s*t*u*", "*a*e*i*", "*b*d*f*",
"*c*g*k*", "*d*h*l*", "*i*o*u*", "*r*s*t*",
],
regexps: &[
"(([abc]?)*)+",
"([abc]+)*d",
"(a*)*b",
"([xyz]?)*end",
"(([mno]?)*)+",
"([pqr]+)*s",
],
state_count: 152,
total_closure_entries: 335,
max_closure_len: 31,
matches: [3, 2, 7],
},
StatsWorkload {
name: "20-nested-regexps",
patterns: &[],
regexps: &[
"(([abc]?)*)+",
"([abc]+)*d",
"(a*)*b",
"([xyz]?)*end",
"(([mno]?)*)+",
"([pqr]+)*s",
"(([def]?)*)+",
"([ghi]+)*j",
"(([stu]?)*)+",
"([vwx]+)*y",
"(b*)*c",
"(d*)*e",
"(([fg]?)*)+",
"([hi]+)*k",
"(([jk]?)*)+",
"([lm]+)*n",
"(([op]?)*)+",
"([qr]+)*t",
"(e*)*f",
"(g*)*h",
],
state_count: 137,
total_closure_entries: 398,
max_closure_len: 61,
matches: [0, 0, 0],
},
StatsWorkload {
name: "deeply-nested",
patterns: &[],
regexps: &[
"(((a?)*b?)*c?)*",
"(((x?)*y?)*z?)*",
"(((d?)*e?)*f?)*",
"(((m?)*n?)*o?)*",
"((((a?)*b?)*c?)*d?)*",
"((((x?)*y?)*z?)*w?)*",
],
state_count: 61,
total_closure_entries: 416,
max_closure_len: 47,
matches: [0, 0, 0],
},
StatsWorkload {
name: "overlapping-char-classes",
patterns: &[],
regexps: &[
"(([abc]?)*)+",
"(([bcd]?)*)+",
"(([cde]?)*)+",
"(([def]?)*)+",
"(([efg]?)*)+",
"(([fgh]?)*)+",
"(([ghi]?)*)+",
"(([hij]?)*)+",
"(([ijk]?)*)+",
"(([jkl]?)*)+",
"(([klm]?)*)+",
"(([lmn]?)*)+",
],
state_count: 75,
total_closure_entries: 275,
max_closure_len: 49,
matches: [0, 0, 0],
},
StatsWorkload {
name: "shell+deep-overlap",
patterns: &[
"*a*b*", "*b*c*", "*c*d*", "*d*e*", "*e*f*", "*a*c*", "*b*d*", "*c*e*", "*d*f*",
"*a*d*",
],
regexps: &[
"(((a?)*b?)*c?)*",
"(((b?)*c?)*d?)*",
"(((c?)*d?)*e?)*",
"(((d?)*e?)*f?)*",
"(([abcd]?)*)+",
"(([cdef]?)*)+",
],
state_count: 121,
total_closure_entries: 421,
max_closure_len: 47,
matches: [10, 10, 10],
},
];
fn stats_events() -> Vec<Vec<u8>> {
vec![
br#"{"val": "abcdefgh"}"#.to_vec(),
format!(r#"{{"val": "{}"}}"#, "abcdef".repeat(5)).into_bytes(),
format!(r#"{{"val": "{}"}}"#, "abcdefghijklmnop".repeat(3)).into_bytes(),
]
}
fn build_stats_matcher(wl: &StatsWorkload) -> Quamina<String> {
let mut q = Quamina::new();
let mut i = 0;
for ss in wl.patterns {
let pattern = format!(r#"{{"val": [{{"shellstyle": "{ss}"}}]}}"#);
q.add_pattern(format!("s{i}"), &pattern).unwrap();
i += 1;
}
for re in wl.regexps {
let pattern = format!(r#"{{"val": [{{"regexp": "{re}"}}]}}"#);
q.add_pattern(format!("r{i}"), &pattern).unwrap();
i += 1;
}
q
}
#[test]
#[cfg_attr(miri, ignore)]
fn test_arena_stats_workloads() {
let events = stats_events();
for wl in STATS_WORKLOADS {
let q = build_stats_matcher(wl);
let stats = q.arena_stats();
assert_eq!(
stats.state_count, wl.state_count,
"{}: state_count = {}, want {}",
wl.name, stats.state_count, wl.state_count,
);
assert_eq!(
stats.total_closure_entries, wl.total_closure_entries,
"{}: total_closure_entries = {}, want {}",
wl.name, stats.total_closure_entries, wl.total_closure_entries,
);
assert_eq!(
stats.max_closure_len, wl.max_closure_len,
"{}: max_closure_len = {}, want {}",
wl.name, stats.max_closure_len, wl.max_closure_len,
);
for (ei, event) in events.iter().enumerate() {
let matches = q.matches_for_event(event).unwrap();
assert_eq!(
matches.len(),
wl.matches[ei],
"{}: event[{ei}] expected {} matches, got {}",
wl.name,
wl.matches[ei],
matches.len()
);
}
}
}