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]
#[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);
for &byte in &q {
assert!(
byte < 128,
"Q-number 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
);
for (j, &byte) in q.iter().enumerate() {
assert!(
byte < 128,
"Q-number byte {} at pos {} >= 128 for value at index {}",
byte,
j,
i
);
}
assert!(
std::str::from_utf8(&q).is_ok(),
"Q-number should be valid UTF-8 for value at index {}",
i
);
assert!(
q.len() <= 10,
"Q-number length {} exceeds max 10 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_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_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]
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_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]
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_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"));
}