use irgx::{Error, Regex, RegexBuilder};
#[test]
fn a_bad_pattern_is_an_error_not_a_panic() {
for pattern in ["(unclosed", "[z-a]", "a{9,1}", "*", "(?P<", r"\"] {
let why = Regex::new(pattern).expect_err(pattern);
assert!(
matches!(why, Error::Syntax { .. }),
"{pattern}: expected a syntax error, got {why:?}"
);
let said = why.to_string();
assert!(said.contains(pattern), "{pattern}: message was {said:?}");
assert!(said.contains("cannot compile"), "{said:?}");
assert!(said.len() > 30, "a bare status is not a reason: {said:?}");
assert!(why.status().is_some());
assert!(!why.is_out_of_memory());
let boxed: Box<dyn std::error::Error> = Box::new(why);
assert!(boxed.source().is_none());
}
}
#[test]
fn constructs_outside_the_linear_grammar_are_refused() {
for pattern in ["foo(?=bar)", "(?<=x)y", "(?!x)", "x(?i)y", "(?x) a b"] {
let why = Regex::new(pattern).expect_err(pattern);
assert!(
matches!(why, Error::NeedsPcre { .. }),
"{pattern}: expected a declinature, got {why:?}"
);
RegexBuilder::new(pattern)
.pcre(true)
.build()
.unwrap_or_else(|why| panic!("{pattern} should compile under pcre: {why}"));
}
}
#[test]
fn pcre_reports_its_own_refusals() {
let why = RegexBuilder::new(r"(?<=a+)b")
.pcre(true)
.build()
.expect_err("a variable-length lookbehind is not valid PCRE2");
assert!(matches!(why, Error::Syntax { .. }), "{why:?}");
assert!(why.to_string().contains("(?<=a+)b"));
}
#[test]
fn a_declined_pattern_compiles_and_matches_under_pcre() {
let rescuable = [
("(?=x)", "x"),
("(?<=x)y", "xy"),
(r"(a)\1", "aa"),
("(?>ab)", "ab"),
];
for (pattern, text) in rescuable {
let why = Regex::new(pattern).expect_err(pattern);
assert!(
matches!(why, Error::NeedsPcre { .. }),
"{pattern}: expected a declinature, got {why:?}"
);
let said = why.to_string();
assert!(said.contains(pattern), "{pattern}: message was {said:?}");
assert!(
said.contains("pcre"),
"{pattern}: no repair named: {said:?}"
);
let re = RegexBuilder::new(pattern)
.pcre(true)
.build()
.unwrap_or_else(|why| panic!("{pattern} should compile under pcre: {why}"));
assert!(
re.is_match(text),
"{pattern} compiled under pcre but did not match {text:?}"
);
}
}
#[test]
fn the_retry_idiom_is_two_lines() {
fn compile(pattern: &str) -> Result<Regex, Error> {
match Regex::new(pattern) {
Err(Error::NeedsPcre { .. }) => RegexBuilder::new(pattern).pcre(true).build(),
other => other,
}
}
assert!(compile(r"\d+").unwrap().is_match("42"));
assert_eq!(
compile(r"(?<=\$)\d+")
.unwrap()
.find("cost $42")
.unwrap()
.as_str(),
"42"
);
assert!(matches!(
compile("(unclosed").unwrap_err(),
Error::Syntax { .. }
));
}
#[test]
fn a_malformed_pattern_carries_an_offset_pcre_cannot_rescue() {
for pattern in ["(unclosed", "a{2,1}", "[z-a]", "*x", "[abc"] {
let why = Regex::new(pattern).expect_err(pattern);
let Error::Syntax {
pattern: reported,
at,
status,
detail,
} = &why
else {
panic!("{pattern}: expected a syntax error, got {why:?}");
};
assert_eq!(reported, pattern);
assert!(
*at <= pattern.len(),
"{pattern}: offset {at} is outside a pattern of {} bytes",
pattern.len()
);
let _ = &pattern[..*at];
assert!(status.code() < 0);
assert!(
detail.is_some(),
"{pattern}: the engine names this fault, and the message should carry it"
);
let said = why.to_string();
assert!(said.contains(&format!("byte {at}")), "{said:?}");
assert!(said.contains(pattern), "{said:?}");
let under_pcre = RegexBuilder::new(pattern)
.pcre(true)
.build()
.expect_err("pcre does not rescue a malformed pattern");
assert!(
matches!(under_pcre, Error::Syntax { .. }),
"{pattern} under pcre: {under_pcre:?}"
);
}
}
#[test]
fn the_offsets_are_where_the_problem_is() {
let expected = [
("(unclosed", 9),
("a{2,1}", 5),
("[z-a]", 4),
("*x", 1),
("[abc", 4),
];
let got: Vec<_> = expected
.iter()
.map(|(pattern, _)| match Regex::new(pattern) {
Err(Error::Syntax { at, .. }) => (*pattern, at),
other => panic!("{pattern}: {other:?}"),
})
.collect();
assert_eq!(got, expected.to_vec());
}
#[test]
fn the_offset_indexes_the_pattern_and_not_some_other_string() {
for (pattern, want) in [("café(unclosed", 14), ("日本[z-a]", 10)] {
let why = Regex::new(pattern).expect_err(pattern);
let Error::Syntax { at, .. } = why else {
panic!("{pattern}: expected a syntax error, got {why:?}");
};
assert_eq!(at, want, "{pattern}");
assert!(at <= pattern.len(), "{pattern}: {at} is past the end");
assert!(
at > pattern.chars().count(),
"{pattern}: byte {at} should be past the character count, or the offset is being \
read in the wrong unit"
);
assert!(pattern.is_char_boundary(at));
let _ = &pattern[..at];
}
}
#[test]
fn a_refusal_without_a_position_reports_none() {
let declined = Regex::new("(?=x)").expect_err("outside the linear grammar");
assert!(matches!(declined, Error::NeedsPcre { .. }), "{declined:?}");
assert!(
!declined.to_string().contains("byte"),
"a declinature has no offset to name: {declined}"
);
let at_the_start = Regex::new("*x").expect_err("a leading quantifier");
assert!(
matches!(at_the_start, Error::Syntax { at: 1, .. }),
"{at_the_start:?}"
);
}
#[test]
fn a_declinature_yields_no_handle_and_keeps_nothing() {
for _ in 0..10_000 {
let outcome = Regex::new("(?=x)");
assert!(
matches!(outcome, Err(Error::NeedsPcre { .. })),
"a declinature must not produce a Regex"
);
}
assert!(Regex::new(r"\d+").unwrap().is_match("42"));
}
#[test]
fn a_declinature_does_not_borrow_the_previous_failures_detail() {
let earlier = Regex::new("(unclosed").expect_err("malformed");
let Error::Syntax { at, detail, .. } = &earlier else {
panic!("expected a syntax error, got {earlier:?}");
};
let (at, named) = (*at, detail.clone().expect("the engine names this fault"));
let declined = Regex::new("(?=x)").expect_err("outside the linear grammar");
assert!(matches!(declined, Error::NeedsPcre { .. }), "{declined:?}");
let said = declined.to_string();
assert!(
!said.contains(&named),
"the declinature inherited the previous fault name: {said:?}"
);
assert!(
!said.contains(&format!("byte {at}")),
"the declinature inherited the previous offset: {said:?}"
);
assert!(!said.contains("unclosed"), "{said:?}");
}
#[test]
fn variants_are_distinguishable() {
let pattern = Regex::new("(").unwrap_err();
assert!(!pattern.is_out_of_memory());
let oom = Error::OutOfMemory { detail: None };
assert!(oom.is_out_of_memory());
assert_eq!(oom.status(), Some(irgx::Status::OUT_OF_MEMORY));
assert_eq!(oom.status().unwrap().code(), -2);
assert!(oom.to_string().contains("out of memory"));
assert!(!oom.status().unwrap().message().is_empty());
let boundary = Error::NotCharBoundary { offset: 1 };
assert_eq!(
boundary.status(),
None,
"no status crossed the seam for this"
);
assert!(!boundary.is_out_of_memory());
assert_ne!(pattern, boundary);
let declined = Regex::new("(?=x)").unwrap_err();
let malformed = Regex::new("(?=x").unwrap_err();
assert!(matches!(declined, Error::NeedsPcre { .. }), "{declined:?}");
assert!(matches!(malformed, Error::Syntax { .. }), "{malformed:?}");
assert_ne!(declined, malformed);
assert_ne!(declined, boundary);
assert_ne!(declined, oom);
assert_ne!(malformed, boundary);
assert_ne!(declined.status(), malformed.status());
assert_eq!(declined.status(), Some(irgx::Status::DECLINED));
assert_eq!(declined.status().unwrap().code(), -1);
assert!(!declined.is_out_of_memory());
assert!(declined.to_string().contains("pcre"));
assert!(!malformed.to_string().contains("pcre"));
}
#[test]
fn status_carries_the_engines_own_sentence() {
let refused = Regex::new("(").unwrap_err().status().expect("a status");
assert!(refused.code() < 0);
assert!(
!refused.message().is_empty(),
"the library has a sentence for every status it returns"
);
assert!(format!("{refused}").contains(&format!("status {}", refused.code())));
let oom = irgx::Status::OUT_OF_MEMORY;
assert_eq!(oom.code(), -2);
assert!(oom.message().to_lowercase().contains("memory"));
assert!(format!("{oom}").contains("status -2"));
assert!(format!("{oom:?}").contains("Status(-2"));
}
#[test]
fn a_fixed_pattern_cannot_be_invalid() {
for pattern in ["(unclosed", "[z-a]", "*", r"\", "a{9,1}"] {
let re = RegexBuilder::new(pattern)
.fixed(true)
.build()
.unwrap_or_else(|why| panic!("{pattern} as a literal: {why}"));
let text = format!("x{pattern}y");
assert_eq!(re.find(&text).map(|m| m.as_str()), Some(pattern));
}
}
#[test]
fn no_match_and_failure_are_different_answers() {
let re = Regex::new("a").unwrap();
assert!(!re.try_is_match("").unwrap());
assert_eq!(re.try_find("").unwrap(), None);
assert_eq!(re.try_find_iter("").unwrap().count(), 0);
assert!(re.try_captures("").unwrap().is_none());
assert_eq!(re.try_captures_iter("").unwrap().count(), 0);
assert_eq!(re.split("").collect::<Vec<_>>(), [""]);
assert_eq!(re.replace_all("", "x"), "");
}
#[test]
fn unknown_names_do_not_resolve() {
let re = Regex::new(r"(?P<real>a)(b)").unwrap();
assert_eq!(re.group_index("real"), Some(1));
assert_eq!(re.group_index("nope"), None);
assert_eq!(re.group_index(""), None);
assert_eq!(re.group_index("REAL"), None, "names are case sensitive");
let caps = re.captures("ab").unwrap();
assert_eq!(caps.name("nope"), None);
assert_eq!(caps.get(9), None);
}
#[test]
#[should_panic(expected = "no group named")]
fn indexing_an_unknown_name_says_so() {
let re = Regex::new("(a)").unwrap();
let _ = &re.captures("a").unwrap()["nope"];
}
#[test]
fn abi_version_is_checked() {
assert!(Regex::new("a").is_ok());
assert_eq!(irgx::ABI_VERSION, 2);
let stale = irgx::ABI_VERSION - 1;
let mismatch = Error::Abi {
expected: irgx::ABI_VERSION,
found: stale,
};
let said = mismatch.to_string();
assert!(
said.contains(&format!("ABI {}", irgx::ABI_VERSION)),
"{said:?}"
);
assert!(said.contains(&format!("ABI {stale}")), "{said:?}");
assert!(said.contains("IRGX_LIB_DIR"));
}