use std::collections::BTreeMap;
use irgx::{Regex, RegexBuilder};
use serde::Deserialize;
#[derive(Deserialize)]
struct Corpus {
engine_version: String,
cases: Vec<Case>,
}
#[derive(Deserialize)]
struct Case {
name: String,
pattern: String,
#[serde(default)]
flags: BTreeMap<String, bool>,
text: String,
spans: Vec<[i64; 2]>,
groups: Vec<Vec<[i64; 2]>>,
is_match: bool,
}
impl Case {
fn label(&self) -> String {
format!(
"case {} pattern {:?} flags {:?} text {:?}",
self.name,
self.pattern,
self.flags.keys().collect::<Vec<_>>(),
self.text
)
}
fn compile(&self) -> Regex {
let mut builder = RegexBuilder::new(&self.pattern);
for (flag, &on) in &self.flags {
match flag.as_str() {
"fixed" => builder.fixed(on),
"ignore_case" => builder.ignore_case(on),
"word" => builder.word(on),
"smart_case" => builder.smart_case(on),
"unicode" => builder.unicode(on),
"pcre" => builder.pcre(on),
other => panic!("{}: unknown flag {other} in the corpus", self.label()),
};
}
builder
.build()
.unwrap_or_else(|why| panic!("{}: {why}", self.label()))
}
}
fn corpus() -> Corpus {
let raw = include_str!("../testdata/python_oracle.json");
serde_json::from_str(raw).expect("testdata/python_oracle.json should parse")
}
#[test]
fn corpus_matches_the_linked_engine() {
let found = irgx::engine_version();
assert_eq!(
corpus().engine_version,
found,
"testdata/python_oracle.json was generated against a different engine build than the \
one linked here ({found}). Regenerate it with scripts/python_oracle.py."
);
}
fn accounted(found: &[[i64; 2]], recorded: &[[i64; 2]], label: &str) {
let mut showing = found.iter();
for span in recorded {
if showing.clone().next() == Some(span) {
showing.next();
} else {
assert_eq!(
span[0], span[1],
"{label}: dropped {span:?}, which is not an empty match\n\
shown {found:?}\n\
recorded {recorded:?}"
);
}
}
assert!(
showing.next().is_none(),
"{label}: shows a span the engine did not report\n\
shown {found:?}\n\
recorded {recorded:?}"
);
}
#[test]
fn spans_agree_with_the_python_binding() {
let corpus = corpus();
assert!(corpus.cases.len() > 50, "the corpus should be substantial");
for case in &corpus.cases {
let re = case.compile();
let found: Vec<[i64; 2]> = re
.try_find_iter(&case.text)
.unwrap_or_else(|why| panic!("{}: {why}", case.label()))
.map(|m| [m.start() as i64, m.end() as i64])
.collect();
accounted(&found, &case.spans, &case.label());
}
}
#[test]
fn the_corpus_contains_a_case_where_the_two_conventions_differ() {
let differs = corpus().cases.iter().any(|case| {
let shown = case.compile().try_find_iter(&case.text).unwrap().count();
shown != case.spans.len()
});
assert!(
differs,
"no case exercises the empty-match convention gap; add a nullable pattern"
);
}
#[test]
fn group_spans_agree_with_the_python_binding() {
for case in &corpus().cases {
let re = case.compile();
let Some(groups) = re.groups() else {
panic!("{}: the capture arm refused this pattern", case.label());
};
let found: Vec<Vec<[i64; 2]>> = re
.try_captures_iter(&case.text)
.unwrap_or_else(|why| panic!("{}: {why}", case.label()))
.map(|caps| {
(0..=groups)
.map(|at| match caps.get(at) {
None => [-1, -1],
Some(m) => [m.start() as i64, m.end() as i64],
})
.collect()
})
.collect();
let whole = |row: &Vec<[i64; 2]>| row[0];
accounted(
&found.iter().map(whole).collect::<Vec<_>>(),
&case.groups.iter().map(whole).collect::<Vec<_>>(),
&case.label(),
);
for row in &found {
let same = case
.groups
.iter()
.find(|recorded| recorded[0] == row[0])
.unwrap_or_else(|| panic!("{}: no recorded row at {:?}", case.label(), row[0]));
assert_eq!(row, same, "{}: group detail differs", case.label());
}
}
}
#[test]
fn is_match_agrees_with_find_all() {
for case in &corpus().cases {
let re = case.compile();
let said = re
.try_is_match(&case.text)
.unwrap_or_else(|why| panic!("{}: {why}", case.label()));
assert_eq!(
said,
case.is_match,
"{}: is_match disagrees with the Python binding",
case.label()
);
assert_eq!(
said,
!case.spans.is_empty(),
"{}: find_all reports {} match(es) and is_match says {said}. find_all is the \
header's authority on the sequence, so the two arms have diverged.",
case.label(),
case.spans.len()
);
}
}
#[test]
fn find_agrees_with_the_first_span_of_find_all() {
for case in &corpus().cases {
let re = case.compile();
let want = case.spans.first().map(|span| (span[0], span[1]));
for (verb, got) in [
("find", re.try_find(&case.text)),
("find_at(0)", re.try_find_at(&case.text, 0)),
] {
let got = got.unwrap_or_else(|why| panic!("{}: {verb}: {why}", case.label()));
let got = got.map(|m| (m.start() as i64, m.end() as i64));
assert_eq!(
got,
want,
"{}: {verb} reports {got:?} where find_all's first span is {want:?}",
case.label()
);
}
}
}
#[test]
fn corpus_covers_the_hard_shapes() {
let corpus = corpus();
let names: Vec<&str> = corpus.cases.iter().map(|case| case.name.as_str()).collect();
for required in [
"star_nullable", "empty_pattern", "word_boundary", "unicode_literal", "ascii_class", "groups_optional", "pcre_backref", "word", ] {
assert!(
names.contains(&required),
"the corpus should cover {required}"
);
}
for flag in [
"fixed",
"ignore_case",
"word",
"smart_case",
"unicode",
"pcre",
] {
assert!(
corpus
.cases
.iter()
.any(|case| case.flags.contains_key(flag)),
"the corpus should exercise the {flag} flag"
);
}
}