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(br#"{"status": "active"}"#).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(br#"{"status": "active"}"#).unwrap();
assert_eq!(q.pruner_stats().emitted(), 1);
assert_eq!(q.pruner_stats().filtered(), 1);
let _ = q.matches_for_event(br#"{"status": "pending"}"#).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);
assert_eq!(stats.emitted(), 42);
assert_eq!(stats.filtered(), 17);
stats.add_emitted(10);
stats.add_filtered(5);
assert_eq!(
cloned.emitted(),
42,
"clone must not see mutations to original"
);
assert_eq!(
cloned.filtered(),
17,
"clone must not see mutations to original"
);
}
#[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(br#"{"status": "active"}"#).unwrap());
assert!(!q.has_matches(br#"{"status": "inactive"}"#).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(br#"{"status": "active"}"#).unwrap(), 2);
assert_eq!(q.count_matches(br#"{"status": "pending"}"#).unwrap(), 1);
assert_eq!(q.count_matches(br#"{"status": "deleted"}"#).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);
assert!(q.list_pattern_ids().is_empty(), "clear must drop all ids");
}
#[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(), "{desc} should error");
}
}
#[test]
fn test_invalid_pattern_handling() {
let mut q = Quamina::new();
assert_add_err!(q, "p1", "");
assert_add_err!(q, "p2", "33");
assert_add_err!(q, "p3", "[1,2]");
assert_add_err!(q, "p4", "{");
assert_add_err!(q, "p5", r#"{"foo": }"#);
assert_add_err!(q, "p6", r#"{"foo": "string"}"#);
assert_add_err!(q, "p7", r#"{"foo": 123}"#);
assert_add_err!(q, "p8", r#"{"foo": true}"#);
assert_add_ok!(q, "valid1", r#"{"x": [1]}"#);
assert_add_ok!(q, "valid2", r#"{"x": ["string"]}"#);
assert_add_ok!(q, "valid3", r#"{"x": {"y": [1]}}"#);
}
#[test]
fn test_bad_pattern_error_handling() {
let mut q = Quamina::new();
assert_add_err!(q, "p1", r#"{"x": [{"anything-but": []}]}"#);
assert_add_err!(q, "p2", r#"{"x": [{"anything-but": ["a", 1]}]}"#);
}
#[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(br#"{"likes": "tacos"}"#);
assert!(result.is_ok(), "Should not panic with empty matcher");
assert!(result.unwrap().is_empty(), "No pattern_ids 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 pattern_ids = q.matches_for_event(br#"{"status": "active"}"#).unwrap();
assert_eq!(pattern_ids, 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 pattern_ids = q.matches_for_event(br#"{"x": 1}"#).unwrap();
assert_eq!(pattern_ids, vec![42]);
let mut q = QuaminaBuilder::<&str>::new().build().unwrap();
q.add_pattern("test", r#"{"x": [1]}"#).unwrap();
let pattern_ids = q.matches_for_event(br#"{"x": 1}"#).unwrap();
assert_eq!(pattern_ids, 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(Self {
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 pattern_ids = q.matches_for_event(b"ignored event data").unwrap();
assert_eq!(pattern_ids, 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 pattern_ids = q.matches_for_event(b"ignored").unwrap();
assert!(pattern_ids.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 pattern_ids = q.matches_for_event(b"ignored").unwrap();
assert_eq!(pattern_ids, 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 pattern_ids = q.matches_for_event(br#"{"status": "active"}"#).unwrap();
assert_eq!(pattern_ids, 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(), "{desc} should be rejected: {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 {byte} >= 128 for value {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 = i32::try_from((rng_state >> 1) % 600).expect("range mod 600 fits in i32") - 300;
let mantissa = f64::from_bits((rng_state >> 12) | 0x3FF0_0000_0000_0000) - 1.0;
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 unit = f64::from_bits((rng_state >> 12) | 0x3FF0_0000_0000_0000) - 1.0;
let val = unit.mul_add(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: {prev_val} ({prev_q:?}) should be <= {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,
byte_length: 1,
}, LookaroundCondition::PositiveLookbehind {
pattern: pattern2,
byte_length: 1,
}, LookaroundCondition::NegativeLookahead(pattern1), ];
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 pattern_ids"
);
assert_no_match!(
q,
r#"{"a": 1, "b": "hello"}"#,
"Empty matcher should return no pattern_ids 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{i}\": [\"v\"]"));
}
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{i}\": [\"v\"]"));
}
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{i}\": [\"v\"]"));
}
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{i}\": [\"value{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{i}\": [\"value{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_add_ok!(q, "exact", r#"{"x": ["hello"]}"#);
assert_add_ok!(q, "num", r#"{"x": [42]}"#);
assert_add_ok!(q, "prefix", r#"{"x": [{"prefix": "he"}]}"#);
assert_add_ok!(q, "suffix", r#"{"x": [{"suffix": "lo"}]}"#);
assert_add_ok!(q, "shell", r#"{"x": [{"shellstyle": "h*o"}]}"#);
assert_add_ok!(q, "wild", r#"{"x": [{"wildcard": "h*o"}]}"#);
assert_add_ok!(q, "ab", r#"{"x": [{"anything-but": ["no"]}]}"#);
assert_add_ok!(q, "eic", r#"{"x": [{"equals-ignore-case": "HELLO"}]}"#);
assert_add_ok!(q, "re", r#"{"x": [{"regex": "[a-z]+"}]}"#);
assert_add_ok!(q, "numr", r#"{"x": [{"numeric": [">=", 1, "<", 100]}]}"#);
assert_add_ok!(q, "cidr", r#"{"x": [{"cidr": "10.0.0.0/8"}]}"#);
assert_add_ok!(q, "exists", r#"{"x": [{"exists": true}]}"#);
}
#[test]
fn test_default_limits_allow_normal_patterns_miri_friendly() {
let mut q = Quamina::new();
assert_add_ok!(q, "exact", r#"{"x": ["hello"]}"#);
assert_add_ok!(q, "num", r#"{"x": [42]}"#);
assert_add_ok!(q, "prefix", r#"{"x": [{"prefix": "he"}]}"#);
assert_add_ok!(q, "suffix", r#"{"x": [{"suffix": "lo"}]}"#);
assert_add_ok!(q, "shell", r#"{"x": [{"shellstyle": "h*o"}]}"#);
assert_add_ok!(q, "wild", r#"{"x": [{"wildcard": "h*o"}]}"#);
assert_add_ok!(q, "ab", r#"{"x": [{"anything-but": ["no"]}]}"#);
assert_add_ok!(q, "eic", r#"{"x": [{"equals-ignore-case": "HELLO"}]}"#);
assert_add_ok!(q, "numr", r#"{"x": [{"numeric": [">=", 1, "<", 100]}]}"#);
assert_add_ok!(q, "exists", r#"{"x": [{"exists": true}]}"#);
}
#[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() {
let _ = 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() {
let _ = 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() {
let _ = 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() {
let _ = 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_add_ok!(q, "p1", r#"{"status": ["active", {"prefix": "pend"}]}"#);
assert_add_ok!(q, "p2", r#"{"a": ["1"], "b": ["2"], "c": ["3"]}"#);
let pattern_ids = q.matches_for_event(br#"{"status": "active"}"#).unwrap();
assert!(pattern_ids.contains(&"p1"));
let pattern_ids = q.matches_for_event(br#"{"status": "pending"}"#).unwrap();
assert!(pattern_ids.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_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 pattern_ids = q.matches_for_event(b"not json").unwrap();
assert_eq!(
pattern_ids,
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,
pattern_ids: [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,
pattern_ids: [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: 112,
total_closure_entries: 112,
max_closure_len: 1,
pattern_ids: [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: 25,
total_closure_entries: 25,
max_closure_len: 1,
pattern_ids: [0, 0, 0],
},
StatsWorkload {
name: "overlapping-char-classes",
patterns: &[],
regexps: &[
"(([abc]?)*)+",
"(([bcd]?)*)+",
"(([cde]?)*)+",
"(([def]?)*)+",
"(([efg]?)*)+",
"(([fgh]?)*)+",
"(([ghi]?)*)+",
"(([hij]?)*)+",
"(([ijk]?)*)+",
"(([jkl]?)*)+",
"(([klm]?)*)+",
"(([lmn]?)*)+",
],
state_count: 103,
total_closure_entries: 103,
max_closure_len: 1,
pattern_ids: [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,
pattern_ids: [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 pattern_ids = q.matches_for_event(event).unwrap();
assert_eq!(
pattern_ids.len(),
wl.pattern_ids[ei],
"{}: event[{ei}] expected {} pattern_ids, got {}",
wl.name,
wl.pattern_ids[ei],
pattern_ids.len()
);
}
}
}
#[allow(clippy::similar_names)]
#[test]
fn test_lookaround_condition_is_negative_true() {
use crate::json::LookaroundCondition;
use crate::regexp::parse_regexp;
let pat = parse_regexp("x").unwrap();
let neg_la = LookaroundCondition::NegativeLookahead(pat.clone());
assert!(neg_la.is_negative());
let neg_lb = LookaroundCondition::NegativeLookbehind {
pattern: pat.clone(),
byte_length: 1,
};
assert!(neg_lb.is_negative());
let pos_la = LookaroundCondition::PositiveLookahead(pat.clone());
assert!(!pos_la.is_negative());
let pos_lb = LookaroundCondition::PositiveLookbehind {
pattern: pat,
byte_length: 1,
};
assert!(!pos_lb.is_negative());
}
#[allow(clippy::similar_names)]
#[test]
fn test_lookaround_condition_is_lookbehind_true() {
use crate::json::LookaroundCondition;
use crate::regexp::parse_regexp;
let pat = parse_regexp("x").unwrap();
let pos_lb = LookaroundCondition::PositiveLookbehind {
pattern: pat.clone(),
byte_length: 1,
};
assert!(pos_lb.is_lookbehind());
let neg_lb = LookaroundCondition::NegativeLookbehind {
pattern: pat.clone(),
byte_length: 1,
};
assert!(neg_lb.is_lookbehind());
let pos_la = LookaroundCondition::PositiveLookahead(pat.clone());
assert!(!pos_la.is_lookbehind());
let neg_la = LookaroundCondition::NegativeLookahead(pat);
assert!(!neg_la.is_lookbehind());
}
#[test]
fn test_transform_lookaround_lookbehind_byte_length() {
use crate::json::LookaroundCondition;
use crate::json::transform_lookaround_pattern;
use crate::regexp::parse_regexp;
let tree = parse_regexp("(?<=abc)d").unwrap();
let mc = transform_lookaround_pattern(&tree).unwrap();
assert_eq!(mc.conditions.len(), 1);
match &mc.conditions[0] {
LookaroundCondition::PositiveLookbehind { byte_length, .. } => {
assert_eq!(*byte_length, 3, "abc = 3 bytes");
}
other => panic!("Expected PositiveLookbehind, got {other:?}"),
}
let tree = parse_regexp("(?<!xy)z").unwrap();
let mc = transform_lookaround_pattern(&tree).unwrap();
assert_eq!(mc.conditions.len(), 1);
match &mc.conditions[0] {
LookaroundCondition::NegativeLookbehind { byte_length, .. } => {
assert_eq!(*byte_length, 2, "xy = 2 bytes");
}
other => panic!("Expected NegativeLookbehind, got {other:?}"),
}
}
#[test]
fn test_transform_lookbehind_single_char_class() {
use crate::json::LookaroundCondition;
use crate::json::transform_lookaround_pattern;
use crate::regexp::parse_regexp;
let tree = parse_regexp("(?<=[a-z])X").unwrap();
let mc = transform_lookaround_pattern(&tree).unwrap();
match &mc.conditions[0] {
LookaroundCondition::PositiveLookbehind { byte_length, .. } => {
assert_eq!(*byte_length, 1, "[a-z] = 1 byte per char");
}
other => panic!("Expected PositiveLookbehind, got {other:?}"),
}
let tree = parse_regexp("(?<=[a-z]{3})X").unwrap();
let mc = transform_lookaround_pattern(&tree).unwrap();
match &mc.conditions[0] {
LookaroundCondition::PositiveLookbehind { byte_length, .. } => {
assert_eq!(*byte_length, 3, "[a-z]{{3}} = 3 bytes");
}
other => panic!("Expected PositiveLookbehind, got {other:?}"),
}
}
#[test]
fn test_transform_lookbehind_dot_byte_length() {
use crate::json::LookaroundCondition;
use crate::json::transform_lookaround_pattern;
use crate::regexp::parse_regexp;
let tree = parse_regexp("(?<=.)X").unwrap();
let mc = transform_lookaround_pattern(&tree).unwrap();
match &mc.conditions[0] {
LookaroundCondition::PositiveLookbehind { byte_length, .. } => {
assert_eq!(*byte_length, 4, ". = 4 bytes (UTF-8 max)");
}
other => panic!("Expected PositiveLookbehind, got {other:?}"),
}
}
#[test]
fn test_transform_lookaround_no_lookarounds_error() {
use crate::json::transform_lookaround_pattern;
use crate::regexp::parse_regexp;
let tree = parse_regexp("abc").unwrap();
let err = transform_lookaround_pattern(&tree).unwrap_err();
assert!(err.contains("no lookarounds"), "Got: {err}");
}
#[test]
fn test_transform_lookbehind_alternation_same_length() {
use crate::json::LookaroundCondition;
use crate::json::transform_lookaround_pattern;
use crate::regexp::parse_regexp;
let tree = parse_regexp("(?<=ab|cd)x").unwrap();
let mc = transform_lookaround_pattern(&tree).unwrap();
assert_eq!(mc.conditions.len(), 1);
match &mc.conditions[0] {
LookaroundCondition::PositiveLookbehind { byte_length, .. } => {
assert_eq!(*byte_length, 2, "ab|cd both = 2 bytes");
}
other => panic!("Expected PositiveLookbehind, got {other:?}"),
}
}
#[test]
fn test_transform_lookaround_alternation_rejected() {
use crate::json::transform_lookaround_pattern;
use crate::regexp::parse_regexp;
let tree = parse_regexp("a(?=b)|c(?=d)").unwrap();
let err = transform_lookaround_pattern(&tree).unwrap_err();
assert!(err.contains("alternation"), "Got: {err}");
}
#[test]
fn test_numeric_comparison_missing_value_after_operator() {
let mut q = Quamina::<&str>::new();
let result = q.add_pattern("bad", r#"{"x": [{"numeric": [">"]}]}"#);
assert!(result.is_err(), "Single-element numeric array should fail");
}
#[test]
fn test_numeric_comparison_all_operators() {
let mut q = Quamina::<&str>::new();
q.add_pattern("gt", r#"{"x": [{"numeric": [">", 10]}]}"#)
.unwrap();
q.add_pattern("gte", r#"{"x": [{"numeric": [">=", 10]}]}"#)
.unwrap();
q.add_pattern("lt", r#"{"x": [{"numeric": ["<", 10]}]}"#)
.unwrap();
q.add_pattern("lte", r#"{"x": [{"numeric": ["<=", 10]}]}"#)
.unwrap();
q.add_pattern("eq", r#"{"x": [{"numeric": ["=", 10]}]}"#)
.unwrap();
let m = q.matches_for_event(br#"{"x": 10}"#).unwrap();
assert!(m.contains(&"gte"), "10 should match >= 10");
assert!(m.contains(&"lte"), "10 should match <= 10");
assert!(m.contains(&"eq"), "10 should match = 10");
assert!(!m.contains(&"gt"), "10 should NOT match > 10");
assert!(!m.contains(&"lt"), "10 should NOT match < 10");
let m = q.matches_for_event(br#"{"x": 11}"#).unwrap();
assert!(m.contains(&"gt"), "11 should match > 10");
assert!(m.contains(&"gte"), "11 should match >= 10");
assert!(!m.contains(&"lt"), "11 should NOT match < 10");
assert!(!m.contains(&"lte"), "11 should NOT match <= 10");
assert!(!m.contains(&"eq"), "11 should NOT match = 10");
let m = q.matches_for_event(br#"{"x": 9}"#).unwrap();
assert!(m.contains(&"lt"), "9 should match < 10");
assert!(m.contains(&"lte"), "9 should match <= 10");
assert!(!m.contains(&"gt"), "9 should NOT match > 10");
assert!(!m.contains(&"gte"), "9 should NOT match >= 10");
}
#[test]
fn test_numeric_comparison_range() {
let mut q = Quamina::<&str>::new();
q.add_pattern("range", r#"{"x": [{"numeric": [">", 5, "<=", 100]}]}"#)
.unwrap();
assert_matches!(q, r#"{"x": 50}"#, vec!["range"]);
assert_matches!(q, r#"{"x": 100}"#, vec!["range"]);
assert_matches!(q, r#"{"x": 6}"#, vec!["range"]);
assert_no_match!(q, r#"{"x": 5}"#); assert_no_match!(q, r#"{"x": 101}"#); assert_no_match!(q, r#"{"x": 4}"#); }
#[test]
fn test_numeric_comparison_exclusive_upper_bound_boundary() {
let mut q = Quamina::<&str>::new();
q.add_pattern("r", r#"{"x": [{"numeric": [">=", 1, "<", 100]}]}"#)
.unwrap();
assert_matches!(q, r#"{"x": 99}"#, vec!["r"]);
assert_matches!(q, r#"{"x": 1}"#, vec!["r"]);
assert_no_match!(
q,
r#"{"x": 100}"#,
"exclusive upper bound must reject value == upper"
);
}
#[test]
fn test_numeric_comparison_exclusive_lower_bound_boundary() {
let mut q = Quamina::<&str>::new();
q.add_pattern("r", r#"{"x": [{"numeric": [">", 1, "<=", 100]}]}"#)
.unwrap();
assert_matches!(q, r#"{"x": 2}"#, vec!["r"]);
assert_matches!(q, r#"{"x": 100}"#, vec!["r"]);
assert_no_match!(
q,
r#"{"x": 1}"#,
"exclusive lower bound must reject value == lower"
);
}
#[test]
fn test_numeric_comparison_both_bounds_exclusive() {
let mut q = Quamina::<&str>::new();
q.add_pattern("r", r#"{"x": [{"numeric": [">", 1, "<", 100]}]}"#)
.unwrap();
assert_matches!(q, r#"{"x": 2}"#, vec!["r"]);
assert_matches!(q, r#"{"x": 99}"#, vec!["r"]);
assert_no_match!(
q,
r#"{"x": 1}"#,
"exclusive lower must reject value == lower"
);
assert_no_match!(
q,
r#"{"x": 100}"#,
"exclusive upper must reject value == upper"
);
}
#[test]
fn test_numeric_comparison_invalid_patterns() {
let mut q = Quamina::<&str>::new();
assert_add_err!(q, "bad", r#"{"x": [{"numeric": ["!=", 5]}]}"#);
assert_add_err!(q, "bad", r#"{"x": [{"numeric": [">", "five"]}]}"#);
assert_add_err!(q, "bad", r#"{"x": [{"numeric": [5, 10]}]}"#);
}
#[test]
fn test_value_to_string_types() {
let q = q!(
"str" => r#"{"x": ["hello"]}"#,
"num" => r#"{"x": [42]}"#,
"bool_t" => r#"{"x": [true]}"#,
"bool_f" => r#"{"x": [false]}"#,
"null_v" => r#"{"x": [null]}"#
);
assert_matches!(q, r#"{"x": "hello"}"#, vec!["str"]);
assert_matches!(q, r#"{"x": 42}"#, vec!["num"]);
assert_matches!(q, r#"{"x": true}"#, vec!["bool_t"]);
assert_matches!(q, r#"{"x": false}"#, vec!["bool_f"]);
assert_matches!(q, r#"{"x": null}"#, vec!["null_v"]);
assert_no_match!(q, r#"{"x": "true"}"#);
assert_no_match!(q, r#"{"x": "null"}"#);
assert_no_match!(q, r#"{"x": "42"}"#);
}
#[test]
fn test_validate_wildcard_escapes() {
let mut q = Quamina::<&str>::new();
assert_add_ok!(q, "esc", r#"{"x": [{"wildcard": "a\\*b"}]}"#);
assert_add_err!(q, "bad", r#"{"x": [{"wildcard": "a\\"}]}"#);
assert_add_err!(q, "bad2", r#"{"x": [{"wildcard": "a\\nb"}]}"#);
assert_add_err!(q, "bad3", r#"{"x": [{"wildcard": "a**b"}]}"#);
}
#[test]
fn test_parse_value_rejects_invalid_value_start() {
let mut q = crate::Quamina::new();
assert_add_err!(q, "bad", r#"{"x": [{"numeric": [">", .5]}]}"#);
}
#[test]
fn test_numeric_pattern_with_scientific_notation() {
let mut q = crate::Quamina::new();
q.add_pattern("sci", r#"{"x": [{"numeric": [">=", 1e2, "<=", 1e2]}]}"#)
.unwrap();
assert_matches!(q, r#"{"x": 100}"#, vec!["sci"]);
assert_no_match!(q, r#"{"x": 99}"#);
q.add_pattern("plus", r#"{"x": [{"numeric": [">=", 1e+2, "<=", 1e+2]}]}"#)
.unwrap();
assert_matches!(q, r#"{"x": 100}"#, vec!["sci", "plus"]);
q.add_pattern("neg", r#"{"y": [{"numeric": [">=", 1e-1, "<=", 1e-1]}]}"#)
.unwrap();
assert_matches!(q, r#"{"y": 0.1}"#, vec!["neg"]);
}
fn i_string(n: usize) -> String {
"i".repeat(n)
}
#[test]
fn test_memory_budget_basic() {
let q = QuaminaBuilder::<&str>::new().build().unwrap();
let (budget, used) = q.get_memory_budget();
assert_eq!(budget, 10 * 1024 * 1024);
assert_eq!(used, 0);
let used_after = q.set_memory_budget(64 * 1024).unwrap();
assert_eq!(used_after, 0);
assert_eq!(q.get_memory_budget().0, 64 * 1024);
let mut q = q;
q.add_pattern("x", r#"{"x": [{"prefix": "abc"}]}"#).unwrap();
let (_, used_now) = q.get_memory_budget();
assert!(used_now > 0);
let err = q.set_memory_budget(used_now - 1).unwrap_err();
assert!(matches!(err, QuaminaError::PatternTooComplex(_)));
assert_eq!(q.get_memory_budget().0, 64 * 1024);
q.set_memory_budget(0).unwrap();
assert_eq!(q.get_memory_budget().0, 0);
let mut q = QuaminaBuilder::<&str>::new()
.with_arena_byte_budget(1)
.build()
.unwrap();
let big = i_string(200);
let pat = format!(r#"{{"x": [{{"prefix": "{big}"}}]}}"#);
assert!(q.add_pattern("big", &pat).is_err());
}
#[test]
fn test_string_fa_memory_budget() {
let mut q = QuaminaBuilder::<&str>::new().build().unwrap();
q.set_memory_budget(10_000).unwrap();
q.add_pattern("seed", r#"{"x": ["x"]}"#).unwrap();
let big = i_string(100);
let big_pat = format!(r#"{{"x": ["{big}"]}}"#);
assert!(
q.add_pattern("big", &big_pat).is_err(),
"100-byte pattern must be rejected under a 10 KB budget"
);
q.set_memory_budget(10_000_000).unwrap();
q.add_pattern("big", &big_pat).unwrap();
let (_, used) = q.get_memory_budget();
let mut q = QuaminaBuilder::<&str>::new().build().unwrap();
q.set_memory_budget(used).unwrap();
q.add_pattern("seed", r#"{"x": ["x"]}"#).unwrap();
q.add_pattern("big", &big_pat).unwrap();
let (_, used_after) = q.get_memory_budget();
let mut q = QuaminaBuilder::<&str>::new().build().unwrap();
q.set_memory_budget(used_after.saturating_sub(big.len()))
.unwrap();
q.add_pattern("seed", r#"{"x": ["x"]}"#).unwrap();
assert!(
q.add_pattern("big", &big_pat).is_err(),
"pattern should be rejected once its arena exceeds the tightened budget"
);
}
#[test]
fn test_memory_stress() {
const WORDS: &[&str] = &[
"alpha", "bravo", "charlie", "delta", "echo", "foxtrot", "golf", "hotel", "india",
"juliet", "kilo", "lima", "mike", "november", "oscar", "papa", "quebec", "romeo", "sierra",
"tango",
];
let mut record = Vec::with_capacity(WORDS.len());
let mut q = QuaminaBuilder::<&str>::new().build().unwrap();
q.set_memory_budget(0).unwrap();
for (i, word) in WORDS.iter().enumerate() {
let star_at = i % word.len();
let starred = format!("{}*{}", &word[..star_at], &word[star_at..]);
let pat = format!(r#"{{"x": ["{starred}"]}}"#);
q.add_pattern("x", &pat).unwrap();
let (_, mem) = q.get_memory_budget();
record.push((pat, mem));
}
let mut q = QuaminaBuilder::<&str>::new().build().unwrap();
q.set_memory_budget(0).unwrap();
for (i, (pat, mem)) in record.iter().enumerate() {
let (_, current) = q.get_memory_budget();
let low_budget = std::cmp::min(*mem / 2, current.saturating_sub(1)).max(1);
q.set_memory_budget(low_budget).unwrap();
let attempt = q.add_pattern("x", pat);
if attempt.is_ok() {
continue;
}
q.set_memory_budget(mem.saturating_mul(2).max(1024 * 1024))
.unwrap();
q.add_pattern("x", pat)
.unwrap_or_else(|e| panic!("pattern {i} rejected under generous budget: {e}"));
}
}
#[test]
fn test_set_memory_budget_boundary_equal_to_current() {
let mut q = QuaminaBuilder::<&str>::new().build().unwrap();
q.add_pattern("p", r#"{"x": [{"prefix": "abc"}]}"#).unwrap();
let (_, current) = q.get_memory_budget();
assert!(
current > 0,
"need non-zero current usage for the boundary check"
);
q.set_memory_budget(current)
.expect("budget == current must be accepted (strict `<` guard)");
assert_eq!(q.get_memory_budget().0, current);
assert!(
q.add_pattern("q", r#"{"x": [{"prefix": "xyz"}]}"#).is_err(),
"pattern that grows arena beyond budget must be rejected"
);
}
#[test]
fn test_memory_budget_zero_disables_check() {
let mut q = QuaminaBuilder::<&str>::new()
.with_arena_byte_budget(1)
.build()
.unwrap();
assert!(q.add_pattern("p", r#"{"x": [{"prefix": "abc"}]}"#).is_err());
q.set_memory_budget(0).unwrap();
q.add_pattern("p", r#"{"x": [{"prefix": "abc"}]}"#).unwrap();
}
#[test]
fn test_memory_budget_failure_leaves_no_state() {
let mut q = QuaminaBuilder::<&str>::new()
.with_arena_byte_budget(1)
.build()
.unwrap();
let before = q.pattern_count();
let _ = q.add_pattern("rejected", r#"{"x": [{"prefix": "abc"}]}"#);
assert_eq!(
q.pattern_count(),
before,
"rejected pattern must not be recorded in pattern_defs"
);
}
#[test]
fn test_memory_usage_dedups_shared_subgraphs() {
let mut q = QuaminaBuilder::<&str>::new().build().unwrap();
q.add_pattern("a", r#"{"x": ["v"]}"#).unwrap();
let (_, used_one) = q.get_memory_budget();
q.add_pattern("b", r#"{"x": ["v"]}"#).unwrap();
let (_, used_two) = q.get_memory_budget();
assert_eq!(
used_one, used_two,
"identical patterns should not inflate accounted memory"
);
}
#[test]
fn test_clone_preserves_live_budget() {
let mut q = QuaminaBuilder::<&str>::new().build().unwrap();
q.add_pattern("p", r#"{"x": ["v"]}"#).unwrap();
q.set_memory_budget(123_456).unwrap();
let cloned = q.clone();
assert_eq!(cloned.get_memory_budget().0, 123_456);
}