#![cfg(feature = "matching-regex")]
use proptest::prelude::*;
use vyre_libs::scan::{AnchoredWindowValidator, build_regex_dfa_pipeline};
use vyre_foundation::match_result::ByteRange;
fn dfa_for(pattern: &str) -> vyre_libs::scan::regex_dfa::RegexDfaPipeline {
build_regex_dfa_pipeline(&[pattern], 4096, 16_384)
.unwrap_or_else(|e| panic!("pattern {pattern:?} must compile to an anchored DFA: {e:?}"))
}
fn triples(matches: &[ByteRange]) -> Vec<(u32, u32, u32)> {
let mut v: Vec<(u32, u32, u32)> = matches.iter().map(|m| (m.tag, m.start, m.end)).collect();
v.sort_unstable();
v
}
#[test]
fn literal_repeat_collapses_to_single_longest_match() {
let pipeline = dfa_for("a{2,4}");
let validator = AnchoredWindowValidator::new(&pipeline.dfa);
let haystack = b"aaaa";
let raw = validator.validate_candidates(haystack, &[0]);
assert_eq!(
triples(&raw),
vec![(0, 0, 2), (0, 0, 3), (0, 0, 4)],
"raw fan-out must surface every admissible {{2,4}} length at origin 0"
);
let ll = validator.validate_candidates_leftmost_longest(haystack, &[0]);
assert_eq!(
triples(&ll),
vec![(0, 0, 4)],
"leftmost-longest must collapse the run to its single maximal match"
);
}
#[test]
fn class_repeat_takes_maximal_body_and_stops_at_terminator() {
let pipeline = dfa_for("k[0-9]{2,4}");
assert_eq!(
pipeline.dfa.max_pattern_len, 5,
"window must size to the MAX repetition (k + 4 digits)"
);
let validator = AnchoredWindowValidator::new(&pipeline.dfa);
assert_eq!(
triples(&validator.validate_candidates_leftmost_longest(b"k12x", &[0])),
vec![(0, 0, 3)],
"a 2-digit body terminated by a non-digit is the whole (minimal-length) token"
);
assert_eq!(
triples(&validator.validate_candidates_leftmost_longest(b"k1234", &[0])),
vec![(0, 0, 5)],
"a 4-digit body is consumed whole"
);
assert_eq!(
triples(&validator.validate_candidates_leftmost_longest(b"k123456", &[0])),
vec![(0, 0, 5)],
"maximal munch caps the body at m == 4 digits even when more digits follow"
);
assert!(
validator
.validate_candidates_leftmost_longest(b"k1x", &[0])
.is_empty(),
"a 1-digit body is below the {{2,4}} minimum and must not match"
);
}
#[test]
fn fixed_repeat_is_unchanged_by_leftmost_longest() {
let pipeline = dfa_for("ghp_[A-Za-z0-9]{4}");
let validator = AnchoredWindowValidator::new(&pipeline.dfa);
let haystack = b"ghp_aB3d";
let raw = triples(&validator.validate_candidates(haystack, &[0]));
let ll = triples(&validator.validate_candidates_leftmost_longest(haystack, &[0]));
assert_eq!(
raw,
vec![(0, 0, 8)],
"fixed token accepts once at its full length"
);
assert_eq!(
ll, raw,
"leftmost-longest must equal the fan-out for fixed patterns"
);
}
#[test]
fn two_variable_tokens_each_collapse_at_their_own_origin() {
let pipeline = dfa_for("v[0-9]{2,4}");
let validator = AnchoredWindowValidator::new(&pipeline.dfa);
let haystack = b"v123 xx v4567";
assert_eq!(
triples(&validator.validate_candidates_leftmost_longest(haystack, &[0, 8])),
vec![(0, 0, 4), (0, 8, 13)],
"each variable token collapses to one maximal match at its own origin"
);
}
#[test]
fn open_ended_repeat_window_uses_the_bounded_replay_policy() {
let plus = dfa_for("k[0-9]+");
assert_eq!(
plus.dfa.max_pattern_len,
vyre_libs::scan::regex_compile::DEFAULT_OPEN_ENDED_REPLAY_LIMIT_BYTES,
"open-ended `+` must use the finite default replay budget"
);
let lower_bounded = dfa_for("k[0-9]{3,}");
assert_eq!(
lower_bounded.dfa.max_pattern_len,
vyre_libs::scan::regex_compile::DEFAULT_OPEN_ENDED_REPLAY_LIMIT_BYTES,
"open-ended `{{3,}}` must use the same finite default replay budget"
);
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(800))]
#[test]
fn planted_bounded_token_yields_single_maximal_match(
k in 2usize..=6,
pre in 0usize..6,
body_seed in "[a-z]{6}",
) {
let pattern = "q_[a-z]{2,6}";
let pipeline = dfa_for(pattern);
let validator = AnchoredWindowValidator::new(&pipeline.dfa);
let body: String = body_seed.chars().take(k).collect();
let mut haystack = String::new();
haystack.push_str(&" ".repeat(pre)); let origin = haystack.len() as u32;
haystack.push_str("q_");
haystack.push_str(&body);
haystack.push('9');
let expected_end = origin + 2 + k as u32; prop_assert_eq!(
triples(&validator.validate_candidates_leftmost_longest(haystack.as_bytes(), &[origin])),
vec![(0, origin, expected_end)],
"planted token {:?} at origin {} must yield one maximal match ending at {}",
haystack, origin, expected_end
);
}
#[test]
fn planted_below_minimum_body_yields_no_match(
pre in 0usize..6,
c in "[a-z]",
) {
let pipeline = dfa_for("q_[a-z]{2,6}");
let validator = AnchoredWindowValidator::new(&pipeline.dfa);
let mut haystack = String::new();
haystack.push_str(&" ".repeat(pre));
let origin = haystack.len() as u32;
haystack.push_str("q_");
haystack.push_str(&c);
haystack.push('9');
prop_assert!(
validator
.validate_candidates_leftmost_longest(haystack.as_bytes(), &[origin])
.is_empty(),
"a single-byte body is below the {{2,6}} minimum; token {:?} must not match",
haystack
);
}
}