use std::borrow::Cow;
use std::collections::HashSet;
use regexsolver::error::EngineError;
use regexsolver::fast_automaton::{CharacterOrder, FastAutomaton, GenerationOptions, PathOrder};
use regexsolver::regex::RegularExpression;
use regexsolver::regex_charclass::CharacterClass;
use regexsolver::{CharRange, Term};
fn term(pattern: &str) -> Term {
Term::from_pattern(pattern).unwrap()
}
#[test]
fn unicode_tables_agree_with_the_parser() {
assert_eq!("16.0.0", regexsolver::regex_charclass::UCD_VERSION);
for class in [
"\\p{Greek}",
"\\p{Latin}",
"\\p{Cyrillic}",
"\\p{Han}",
"\\p{Letter}",
"\\p{Alphabetic}",
"\\w",
"\\d",
"\\s",
] {
assert_eq!(
class,
RegularExpression::new(class).unwrap().to_string(),
"`{class}` came back as a range list rather than its name; the `ucd-*` \
feature and `regex-syntax` disagree about the Unicode version"
);
}
assert_eq!(
"\\p{Uppercase_Letter}",
RegularExpression::new("\\p{Lu}").unwrap().to_string()
);
assert_eq!(
"\\d",
RegularExpression::new("\\p{Decimal_Number}")
.unwrap()
.to_string()
);
}
#[test]
fn default_is_the_empty_language() {
assert_eq!(Term::new_empty(), Term::default());
assert!(Term::default().is_empty().unwrap());
}
#[test]
fn parses_through_from_str_and_from_pattern_alike() {
let parsed: Term = "(ab|xy){2}".parse().unwrap();
assert_eq!(Term::from_pattern("(ab|xy){2}").unwrap(), parsed);
let error = "a(".parse::<Term>().unwrap_err();
assert!(
matches!(error, EngineError::RegexSyntaxError(_)),
"{error:?}"
);
}
#[test]
fn converts_from_both_representations() {
let regex = RegularExpression::new("abc").unwrap();
let automaton = regex.to_automaton().unwrap();
let from_regex: Term = regex.clone().into();
let from_automaton: Term = automaton.clone().into();
assert_eq!(Term::from_regex(regex), from_regex);
assert_eq!(Term::from_automaton(automaton), from_automaton);
assert!(from_regex.equivalent(&from_automaton).unwrap());
}
#[test]
fn set_operations_accept_every_argument_shape() {
let a = term("abc");
let b = term("abcd");
let expected = term("abc|abcd");
assert!(a.union([&b]).unwrap().equivalent(&expected).unwrap());
assert!(
a.union(std::slice::from_ref(&b))
.unwrap()
.equivalent(&expected)
.unwrap()
);
assert!(
a.union(vec![b.clone()])
.unwrap()
.equivalent(&expected)
.unwrap()
);
assert!(
a.union(std::iter::once(&b))
.unwrap()
.equivalent(&expected)
.unwrap()
);
assert!(
a.union(Vec::<&Term>::new())
.unwrap()
.equivalent(&a)
.unwrap()
);
}
#[test]
fn repeat_accepts_every_range_form() {
let a = term("ab");
assert_eq!("(ab){2}", a.repeat(2..3).unwrap().to_pattern().unwrap());
assert_eq!("(ab){2}", a.repeat(2..=2).unwrap().to_pattern().unwrap());
assert_eq!("(ab){2,3}", a.repeat(2..=3).unwrap().to_pattern().unwrap());
assert_eq!("(ab){2,}", a.repeat(2..).unwrap().to_pattern().unwrap());
assert_eq!("(ab)*", a.repeat(..).unwrap().to_pattern().unwrap());
#[allow(clippy::reversed_empty_ranges)]
let reversed = 3..2;
assert!(a.repeat(reversed).unwrap().is_empty().unwrap());
#[allow(clippy::reversed_empty_ranges)]
let empty = 0..0;
assert!(a.repeat(empty).unwrap().is_empty().unwrap());
}
#[test]
fn conversions_borrow_when_they_can() {
let regex_backed = term("abc");
assert!(matches!(regex_backed.to_regex().unwrap(), Cow::Borrowed(_)));
assert!(matches!(
regex_backed.to_automaton().unwrap(),
Cow::Owned(_)
));
let automaton_backed = Term::from_automaton(
RegularExpression::new("abc")
.unwrap()
.to_automaton()
.unwrap(),
);
assert!(matches!(
automaton_backed.to_automaton().unwrap(),
Cow::Borrowed(_)
));
}
#[test]
fn both_representations_give_the_same_answers() {
let regex_backed = term("(ab|xy){2}");
let automaton_backed = Term::from_automaton(regex_backed.to_automaton().unwrap().into_owned());
assert_eq!(
regex_backed.length(),
automaton_backed.length(),
"length disagrees"
);
assert_eq!(
regex_backed.cardinality().unwrap(),
automaton_backed.cardinality().unwrap(),
"cardinality disagrees"
);
assert_eq!(
regex_backed.is_empty().unwrap(),
automaton_backed.is_empty().unwrap()
);
assert_eq!(
regex_backed.is_total().unwrap(),
automaton_backed.is_total().unwrap()
);
assert_eq!(
regex_backed.is_finite().unwrap(),
automaton_backed.is_finite().unwrap()
);
assert_eq!(
regex_backed.matches("abxy").unwrap(),
automaton_backed.matches("abxy").unwrap()
);
assert!(regex_backed.equivalent(&automaton_backed).unwrap());
}
#[test]
fn eager_and_lazy_generation_agree() {
let term = term("[a-c][0-9]");
for options in [
GenerationOptions::from(PathOrder::Sweep),
GenerationOptions::from(PathOrder::Interleave),
GenerationOptions::from((PathOrder::Shuffled, CharacterOrder::Shuffled)),
] {
let eager = term.generate_strings(10, 0, options.clone()).unwrap();
let lazy = term
.iter_strings(options.clone())
.take(10)
.collect::<Result<Vec<_>, _>>()
.unwrap();
assert_eq!(eager, lazy);
let paged = term.generate_strings(4, 6, options).unwrap();
assert_eq!(&eager[6..], paged.as_slice());
}
}
#[test]
fn every_generation_order_enumerates_the_same_language() {
let term = term("[a-c][0-9]");
let total = 30;
let reference: HashSet<String> = term
.generate_strings(total, 0, PathOrder::Sweep)
.unwrap()
.into_iter()
.collect();
assert_eq!(total, reference.len());
for path in [PathOrder::Sweep, PathOrder::Interleave, PathOrder::Shuffled] {
for characters in [CharacterOrder::Ascending, CharacterOrder::Shuffled] {
let generated: HashSet<String> = term
.generate_strings(total, 0, (path, characters))
.unwrap()
.into_iter()
.collect();
assert_eq!(reference, generated, "{path:?} / {characters:?} differs");
}
}
}
#[test]
fn shuffled_generation_is_reproducible_by_seed() {
let term = term("[a-z]{4}");
let shuffled = GenerationOptions::from((PathOrder::Shuffled, CharacterOrder::Shuffled));
let first = term
.generate_strings(20, 0, shuffled.clone().with_seed(42))
.unwrap();
let again = term
.generate_strings(20, 0, shuffled.clone().with_seed(42))
.unwrap();
let other_seed = term
.generate_strings(20, 0, shuffled.with_seed(43))
.unwrap();
assert_eq!(first, again);
assert_ne!(first, other_seed);
}
#[test]
fn generation_bounds_constrain_what_is_generated() {
let term = term(".*abc.*");
let options = GenerationOptions::new()
.with_charset(CharRange::new_from_range_char('a'..='z'))
.with_min_length(4)
.with_max_length(6);
for string in term.generate_strings(50, 0, options).unwrap() {
assert!(string.contains("abc"), "{string:?} is not in the language");
assert!((4..=6).contains(&string.chars().count()), "{string:?}");
assert!(
string.chars().all(|c| c.is_ascii_lowercase()),
"{string:?} leaves the charset"
);
}
}
#[test]
fn a_max_length_makes_an_infinite_language_finite() {
let strings = term("[ab]*")
.iter_strings(GenerationOptions::new().with_max_length(3))
.collect::<Result<Vec<_>, _>>()
.unwrap();
assert_eq!(1 + 2 + 4 + 8, strings.len());
assert!(strings.iter().all(|s| s.chars().count() <= 3));
}
#[test]
fn set_identities_hold() {
let a = term("(ab|xy){2}");
let b = term(".*xy");
let complement = a.complement().unwrap();
assert!(a.intersection([&complement]).unwrap().is_empty().unwrap());
assert!(a.union([&complement]).unwrap().is_total().unwrap());
assert!(complement.complement().unwrap().equivalent(&a).unwrap());
assert!(
a.difference(&b)
.unwrap()
.equivalent(&a.intersection([&b.complement().unwrap()]).unwrap())
.unwrap()
);
let union = a.union([&b]).unwrap();
let intersection = a.intersection([&b]).unwrap();
assert!(a.subset(&union).unwrap());
assert!(b.subset(&union).unwrap());
assert!(intersection.subset(&a).unwrap());
assert!(intersection.subset(&b).unwrap());
}
#[test]
fn patterns_round_trip_through_their_language() {
for pattern in [
"(ab|xy){2}",
".*abc.*def.*",
"[a-c][0-9]{2,4}",
"a(bcfe|bcdg|mkv)*",
"[]",
"",
] {
let original = term(pattern);
let printed = original.to_pattern().unwrap();
let reparsed = term(&printed);
assert!(
original.equivalent(&reparsed).unwrap(),
"`{pattern}` printed as `{printed}`, which is a different language"
);
}
}
#[test]
fn the_empty_language_is_not_the_empty_string() {
let empty = Term::new_empty();
let empty_string = Term::new_empty_string();
assert!(empty.is_empty().unwrap());
assert!(!empty.is_empty_string().unwrap());
assert!(!empty_string.is_empty().unwrap());
assert!(empty_string.is_empty_string().unwrap());
assert!(!empty.equivalent(&empty_string).unwrap());
assert_eq!("[]", empty.to_pattern().unwrap());
assert_eq!("", empty_string.to_pattern().unwrap());
assert!(!empty.matches("").unwrap());
assert!(empty_string.matches("").unwrap());
}
#[test]
fn matching_is_anchored_and_dot_matches_a_line_feed() {
let abc = term("abc");
assert!(abc.matches("abc").unwrap());
assert!(!abc.matches("xabc").unwrap());
assert!(!abc.matches("abcx").unwrap());
assert!(term(".").matches("\n").unwrap());
}
#[test]
fn unsupported_constructs_are_refused() {
for pattern in ["(a)\\1", "a(?=b)", "a(?<=b)", "(?i)abc", "a\\bc", "a^b"] {
assert!(
Term::from_pattern(pattern).is_err(),
"`{pattern}` was accepted"
);
}
assert!(term("^abc$").equivalent(&term("abc")).unwrap());
}
#[test]
fn a_hand_built_automaton_is_a_term() {
use regexsolver::regex_charclass::char::Char;
let mut automaton = FastAutomaton::new_empty();
let state = automaton.new_state();
automaton.accept(state);
automaton
.add_transition_from_range(
0,
state,
&CharRange::new_from_range(Char::new('a')..=Char::new('c')),
)
.unwrap();
let built = Term::from_automaton(automaton);
assert!(built.equivalent(&term("[a-c]")).unwrap());
assert_eq!("[a-c]", built.to_pattern().unwrap());
}
#[test]
fn errors_are_comparable_cloneable_and_non_exhaustive() {
let malformed =
RegularExpression::Repetition(Box::new(RegularExpression::new("ab").unwrap()), 3, Some(1));
let error = Term::from_regex(malformed).to_automaton().unwrap_err();
assert_eq!(EngineError::InvalidRepetitionBounds(3, 1), error);
assert_eq!(error, error.clone());
assert!(!error.to_string().is_empty());
let described = match &error {
EngineError::InvalidRepetitionBounds(..) => "bounds",
_ => "something else",
};
assert_eq!("bounds", described);
}