aboutsummaryrefslogtreecommitdiff
path: root/tests/compile/escape.test.py
blob: aeaff93b85012a2a389754a7a5e064286571e42d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
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 == "\\."


@pytest.mark.parametrize("candidate", _MIXED_CORPUS)
def test_normalized_and_over_escaped_char_classes_match_the_same_inputs(candidate: str):
    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


@pytest.mark.parametrize("candidate", _MIXED_CORPUS)
def test_normalized_and_over_escaped_negated_classes_match_the_same_inputs(candidate: str):
    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 == "[]"