use irgx::{Regex, RegexBuilder};
fn ours(pattern: &str, text: &str) -> Vec<(usize, usize)> {
Regex::new(pattern)
.unwrap()
.find_iter(text)
.map(|found| (found.start(), found.end()))
.collect()
}
fn theirs(pattern: &str, text: &str) -> Vec<(usize, usize)> {
regex::Regex::new(pattern)
.unwrap()
.find_iter(text)
.map(|found| (found.start(), found.end()))
.collect()
}
fn agree(pattern: &str, text: &str) {
assert_eq!(
ours(pattern, text),
theirs(pattern, text),
"pattern {pattern:?} over text {text:?}"
);
}
const NULLABLE: &[&str] = &[
"a*", "b*", "x*", "", "a?", "l*", "(a)*", "a*b*", "[^x]*", "(?:ab)*", "a{0,2}", "é*",
];
const TEXTS: &[&str] = &[
"", "a", "b", "abc", "abcb", "aaa", "bbb", "aXaXa", "bab", "héllo", "ééé", "ab\ncd", "\n",
"a\n", "\na",
];
#[test]
fn nullable_patterns_yield_the_same_sequence_as_the_regex_crate() {
for pattern in NULLABLE {
for text in TEXTS {
agree(pattern, text);
}
}
}
#[test]
fn an_empty_match_abutting_the_previous_one_is_skipped_by_both() {
agree("a*", "abc");
assert_eq!(ours("a*", "abc"), [(0, 1), (2, 2), (3, 3)]);
}
#[test]
fn an_empty_match_inside_a_character_is_unreachable_for_both() {
agree("l*", "héllo");
let spans = ours("l*", "héllo");
assert!(!spans.contains(&(2, 2)), "byte 2 splits the é: {spans:?}");
assert_eq!(spans, [(0, 0), (1, 1), (3, 5), (6, 6)]);
}
#[test]
fn the_empty_match_at_the_end_of_the_text_is_reported_by_both() {
agree("x*", "abc");
assert_eq!(ours("x*", "abc"), [(0, 0), (1, 1), (2, 2), (3, 3)]);
agree("x*", "");
assert_eq!(ours("x*", ""), [(0, 0)]);
}
#[test]
fn non_nullable_patterns_need_no_thinning_and_still_agree() {
for (pattern, text) in [
("a", "banana"),
("a+", "aabaa"),
("[abc]", "xaybzc"),
("ab|ba", "abba"),
("(a)(b)", "abab"),
("é", "ééé"),
(".", "héllo"),
("\\w+", "one two"),
] {
agree(pattern, text);
}
}
#[test]
fn find_at_agrees_with_the_regex_crate_including_at_the_edges() {
for pattern in [
"^b", r"\bbc", r"\Bc", "b$", r"b\b", "a", "x*", "", "a?", "l*", "bc", r"\w+", "é", ".",
] {
for text in [
"", "a", "abc", "aBaBa", "héllo", "ééé", "ab\ncd", "\n", "a\n",
] {
let (mine, crates) = (
Regex::new(pattern).unwrap(),
regex::Regex::new(pattern).unwrap(),
);
for start in (0..=text.len()).filter(|at| text.is_char_boundary(*at)) {
assert_eq!(
mine.find_at(text, start)
.map(|found| (found.start(), found.end())),
crates
.find_at(text, start)
.map(|found| (found.start(), found.end())),
"find_at({pattern:?}, {text:?}, {start})"
);
assert_eq!(
mine.is_match_at(text, start),
crates.is_match_at(text, start),
"is_match_at({pattern:?}, {text:?}, {start})"
);
}
}
}
}
#[test]
fn a_late_start_does_not_move_the_haystacks_edges() {
let caret = Regex::new("^b").unwrap();
assert!(caret.find_at("abc", 1).is_none());
assert!(caret.find("bc").is_some());
let boundary = Regex::new(r"\bbc").unwrap();
assert!(boundary.find_at("abc", 1).is_none());
assert!(boundary.find("bc").is_some());
assert_eq!(
Regex::new("c$")
.unwrap()
.find_at("abc", 1)
.map(|m| m.start()),
Some(2)
);
}
#[test]
fn a_window_confines_the_match_and_slicing_oracles_the_assertion_free_half() {
for pattern in ["a", "x*", "", "a?", "bc", r"\w+", "b|abc", "a+b", "[^x]"] {
for text in ["", "a", "abc", "aBaBa", "héllo", "ab\ncd", "a\n"] {
let (mine, crates) = (
Regex::new(pattern).unwrap(),
regex::Regex::new(pattern).unwrap(),
);
let bounds: Vec<usize> = (0..=text.len())
.filter(|at| text.is_char_boundary(*at))
.collect();
for &start in &bounds {
for &end in bounds.iter().filter(|end| **end >= start) {
assert_eq!(
mine.is_match_within(text, start, end),
crates.is_match(&text[start..end]),
"is_match_within({pattern:?}, {text:?}, {start}, {end})"
);
}
}
}
}
}
#[test]
fn an_assertion_reads_the_whole_text_no_matter_where_the_window_is() {
for (pattern, text, start, end) in [
("b$", "abc", 0, 2),
(r"b\z", "abc", 0, 2),
("^b", "abc", 1, 3),
(r"\bb\b", "abc", 1, 2),
] {
let (mine, crates) = (
Regex::new(pattern).unwrap(),
regex::Regex::new(pattern).unwrap(),
);
assert!(
!mine.is_match_within(text, start, end),
"{pattern:?} should not match within {text:?}[{start}..{end}]"
);
assert!(
crates.is_match(&text[start..end]),
"the slice is the contrast, so {pattern:?} must match {:?}",
&text[start..end]
);
}
}
#[test]
fn a_window_admits_a_shorter_match_the_unwindowed_verb_would_never_report() {
let word = Regex::new(r"\w+").unwrap();
assert_eq!(word.find("abcd").map(|at| at.range()), Some(0..4));
assert!(word.is_match_within("abcd", 0, 2));
assert!(word.is_match_within("abcd", 0, 1));
assert!(!word.is_match_within("abcd", 0, 0));
}
#[test]
fn the_inert_ceiling_is_the_unwindowed_verb_and_widening_only_adds() {
for pattern in [
"^b", r"\bbc", r"\Bc", "b$", r"b\b", r"c\z", "a", "x*", "", r"\w+", ".",
] {
for text in ["", "a", "abc", "aBaBa", "héllo", "ab\ncd", "a\n"] {
let re = Regex::new(pattern).unwrap();
let bounds: Vec<usize> = (0..=text.len())
.filter(|at| text.is_char_boundary(*at))
.collect();
for &start in &bounds {
assert_eq!(
re.is_match_within(text, start, text.len()),
re.is_match_at(text, start),
"an inert ceiling changed the answer: {pattern:?}, {text:?}, {start}"
);
for pair in bounds
.iter()
.filter(|end| **end >= start)
.collect::<Vec<_>>()
.windows(2)
{
let (&narrow, &wide) = (pair[0], pair[1]);
assert!(
!re.is_match_within(text, start, narrow)
|| re.is_match_within(text, start, wide),
"widening lost a match: {pattern:?}, {text:?}, \
[{start},{narrow}] matched but [{start},{wide}] did not"
);
}
}
}
}
}
#[test]
fn a_backwards_window_is_refused_by_name() {
let re = Regex::new("a").unwrap();
assert!(matches!(
re.try_is_match_within("abc", 2, 1),
Err(irgx::Error::BadWindow { start: 2, end: 1 })
));
assert!(matches!(
re.try_is_match_within("héllo", 0, 2),
Err(irgx::Error::NotCharBoundary { offset: 2 })
));
let refused = re.try_is_match_within("abc", 2, 1).unwrap_err();
assert!(refused.status().is_none());
}
#[test]
fn the_linear_engine_windows() {
for pattern in ["a", "^b", "b$", r"\w+", r"c\z", ""] {
assert!(
Regex::new(pattern).unwrap().windows(),
"{pattern:?} should window"
);
}
}
#[test]
fn a_start_that_is_not_a_character_boundary_is_an_error() {
let re = Regex::new("l*").unwrap();
assert!(matches!(
re.try_find_at("héllo", 2),
Err(irgx::Error::NotCharBoundary { offset: 2 })
));
assert!(matches!(
re.try_is_match_at("héllo", 2),
Err(irgx::Error::NotCharBoundary { offset: 2 })
));
assert!(re.try_find_at("abc", 99).is_err());
assert_eq!(
re.find_at("abc", 3).map(|m| (m.start(), m.end())),
regex::Regex::new("l*")
.unwrap()
.find_at("abc", 3)
.map(|m| (m.start(), m.end()))
);
}
#[test]
fn a_leading_inline_flag_says_what_the_builder_says() {
let texts = ["ab\ncd", "AB ab", "a\nb", "", "a\n"];
for (inline, body, fold, lines, dot) in [
("(?i)AB", "AB", true, false, false),
("(?m)^c", "^c", false, true, false),
("(?s)b.c", "b.c", false, false, true),
("(?ms)^c.", "^c.", false, true, true),
] {
let folded = Regex::new(inline).unwrap();
let built = RegexBuilder::new(body)
.ignore_case(fold)
.multi_line(lines)
.dot_matches_new_line(dot)
.build()
.unwrap();
let theirs = regex::Regex::new(inline).unwrap();
for text in texts {
let spans = |m: irgx::Match| (m.start(), m.end());
let ours: Vec<_> = folded.find_iter(text).map(spans).collect();
let via_builder: Vec<_> = built.find_iter(text).map(spans).collect();
let want: Vec<_> = theirs
.find_iter(text)
.map(|m| (m.start(), m.end()))
.collect();
assert_eq!(ours, via_builder, "{inline:?} over {text:?}");
assert_eq!(ours, want, "{inline:?} over {text:?} vs regex");
}
}
let sensitive = RegexBuilder::new("(?-i)ab")
.ignore_case(true)
.build()
.unwrap();
assert_eq!(sensitive.find_iter("ab AB").count(), 1);
let data = RegexBuilder::new("(?i)ab").fixed(true).build().unwrap();
assert_eq!(
data.find("(?i)ab AB").map(|m| (m.start(), m.end())),
Some((0, 6))
);
}