diff options
| author | natsuoto <[email protected]> | 2026-07-15 14:58:16 +0530 |
|---|---|---|
| committer | natsuoto <[email protected]> | 2026-07-15 14:58:16 +0530 |
| commit | cf84fbf5af3b3f321582d74ed34d53b69d8c0002 (patch) | |
| tree | 5f10e1334bdf8bb9519f514c202814ccb8cdd3ef /tests | |
| parent | 5a771b525a59d94c1d339d302eae118e0e46ab1e (diff) | |
| download | edify-cf84fbf5af3b3f321582d74ed34d53b69d8c0002.tar.xz edify-cf84fbf5af3b3f321582d74ed34d53b69d8c0002.zip | |
feat!(compile): normalize char-class escaping to minimal correct form; add equivalence corpus
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/builder/builder.test.py | 8 | ||||
| -rw-r--r-- | tests/compile/escape.test.py | 115 |
2 files changed, 119 insertions, 4 deletions
diff --git a/tests/builder/builder.test.py b/tests/builder/builder.test.py index 82aa1cf..5f96b5c 100644 --- a/tests/builder/builder.test.py +++ b/tests/builder/builder.test.py @@ -404,14 +404,14 @@ def test_end_of_input(): def test_any_of_chars(): expr = RegexBuilder().any_of_chars("aeiou.-") - regex_equality("[aeiou\\.\\-]", expr) - regex_compilation("[aeiou\\.\\-]", expr) + regex_equality("[aeiou.-]", expr) + regex_compilation("[aeiou.-]", expr) def test_anything_but_chars(): expr = RegexBuilder().anything_but_chars("aeiou.-") - regex_equality("[^aeiou\\.\\-]", expr) - regex_compilation("[^aeiou\\.\\-]", expr) + regex_equality("[^aeiou.-]", expr) + regex_compilation("[^aeiou.-]", expr) def test_anything_but_string(): diff --git a/tests/compile/escape.test.py b/tests/compile/escape.test.py new file mode 100644 index 0000000..e9ab657 --- /dev/null +++ b/tests/compile/escape.test.py @@ -0,0 +1,115 @@ +"""Tests for the character-class escape normalization. + +The minimal-correct form escapes only ``\\``, ``]``, first-position ``^``, +and interior ``-`` — everything else passes through as a literal. Match +behavior against a representative corpus must stay identical to the +previous over-escaped form. +""" + +import re + +import pytest + +from edify import RegexBuilder + +_METACHARS_UNRELATED_TO_CLASS = "#?!@$%^&*" +_MIXED_CORPUS = [ + "", + "a", + "z", + "#", + "?", + "!", + "@", + "$", + "%", + "^", + "&", + "*", + "-", + ".", + "abc", + "a#b", + "1-2", + " ", + "foo.bar", + "hello world", + "a^b", + "]", + "[a]", + "\\", +] + + +def test_any_of_chars_emits_minimal_form_for_class_safe_metachars(): + emitted = RegexBuilder().any_of_chars(_METACHARS_UNRELATED_TO_CLASS).to_regex_string() + assert emitted == "[#?!@$%^&*]" + + +def test_any_of_chars_escapes_backslash_and_closing_bracket_only(): + emitted = RegexBuilder().any_of_chars("a\\b]c").to_regex_string() + assert emitted == "[a\\\\b\\]c]" + + +def test_any_of_chars_escapes_first_position_caret_only(): + first_caret = RegexBuilder().any_of_chars("^abc").to_regex_string() + later_caret = RegexBuilder().any_of_chars("a^bc").to_regex_string() + assert first_caret == "[\\^abc]" + assert later_caret == "[a^bc]" + + +def test_any_of_chars_escapes_interior_dash_only(): + edge_dashes = RegexBuilder().any_of_chars("-a-").to_regex_string() + interior_dash = RegexBuilder().any_of_chars("a-b").to_regex_string() + assert edge_dashes == "[-a-]" + assert interior_dash == "[a\\-b]" + + +def test_any_of_chars_leaves_dot_and_asterisk_unescaped_inside_a_class(): + emitted = RegexBuilder().any_of_chars(".*").to_regex_string() + assert emitted == "[.*]" + + +def test_anything_but_chars_leading_position_of_body_is_not_caret(): + emitted = RegexBuilder().anything_but_chars("^abc").to_regex_string() + assert emitted == "[^\\^abc]" + + +def test_anything_but_chars_normalizes_the_same_way_as_any_of_chars(): + emitted = RegexBuilder().anything_but_chars("#?!@$%^&*").to_regex_string() + assert emitted == "[^#?!@$%^&*]" + + +def test_string_terminal_still_uses_full_escape_outside_the_class(): + emitted = RegexBuilder().string("a.b").to_regex_string() + assert emitted == "a\\.b" + + +def test_char_terminal_still_uses_full_escape_outside_the_class(): + emitted = RegexBuilder().char(".").to_regex_string() + assert emitted == "\\." + + [email protected]("candidate", _MIXED_CORPUS) +def test_normalized_and_over_escaped_char_classes_match_the_same_inputs(candidate): + normalized_pattern = ( + RegexBuilder().any_of_chars(_METACHARS_UNRELATED_TO_CLASS).to_regex_string() + ) + over_escaped_pattern = f"[{re.escape(_METACHARS_UNRELATED_TO_CLASS)}]" + normalized_hits = re.findall(normalized_pattern, candidate) + over_escaped_hits = re.findall(over_escaped_pattern, candidate) + assert normalized_hits == over_escaped_hits + + [email protected]("candidate", _MIXED_CORPUS) +def test_normalized_and_over_escaped_negated_classes_match_the_same_inputs(candidate): + normalized_pattern = RegexBuilder().anything_but_chars("aeiou.-").to_regex_string() + over_escaped_pattern = "[^aeiou\\.\\-]" + normalized_hits = re.findall(normalized_pattern, candidate) + over_escaped_hits = re.findall(over_escaped_pattern, candidate) + assert normalized_hits == over_escaped_hits + + +def test_any_of_chars_empty_class_still_renders_empty(): + emitted = RegexBuilder().any_of_chars("").to_regex_string() + assert emitted == "[]" |
