aboutsummaryrefslogtreecommitdiff
path: root/tests/compile/redos.test.py
blob: 97618bb9eace2f07039d01cdac80b91e6d0e05ae (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
"""Tests for the build-time ReDoS detector."""

import pytest

from edify import RegexBuilder
from edify.compile.redos import ReDoSWarning


def _classic_redos_builder():
    return RegexBuilder().one_or_more().group().one_or_more().digit().end()


def test_classic_nested_unbounded_quantifier_triggers_the_warning():
    with pytest.warns(ReDoSWarning, match="nested unbounded quantifier"):
        _classic_redos_builder().to_regex()


def test_zero_or_more_wrapping_one_or_more_triggers_the_warning():
    builder = RegexBuilder().zero_or_more().group().one_or_more().digit().end()
    with pytest.warns(ReDoSWarning):
        builder.to_regex()


def test_at_least_wrapping_zero_or_more_triggers_the_warning():
    builder = RegexBuilder().at_least(2).group().zero_or_more().word().end()
    with pytest.warns(ReDoSWarning):
        builder.to_regex()


def test_lazy_variants_also_trigger_the_warning():
    builder = RegexBuilder().one_or_more_lazy().group().zero_or_more_lazy().digit().end()
    with pytest.warns(ReDoSWarning):
        builder.to_regex()


def test_warning_message_names_both_quantifiers():
    with pytest.warns(ReDoSWarning) as record:
        _classic_redos_builder().to_regex()
    message_text = str(record[0].message)
    assert "one_or_more()" in message_text
    assert "engine='regex'" in message_text


def test_bounded_quantifier_does_not_trigger_the_warning(recwarn: pytest.WarningsRecorder):
    builder = RegexBuilder().exactly(3).group().exactly(2).digit().end()
    builder.to_regex()
    is_redos_flags = [isinstance(warning.message, ReDoSWarning) for warning in recwarn]
    assert not any(is_redos_flags)


def test_grouped_quantifier_over_a_composite_group_does_not_trigger(
    recwarn: pytest.WarningsRecorder,
):
    builder = RegexBuilder().one_or_more().group().digit().letter().end()
    builder.to_regex()
    is_redos_flags = [isinstance(warning.message, ReDoSWarning) for warning in recwarn]
    assert not any(is_redos_flags)


def test_grouped_quantifier_over_a_single_non_quantifier_child_does_not_trigger(
    recwarn: pytest.WarningsRecorder,
):
    builder = RegexBuilder().one_or_more().group().digit().end()
    builder.to_regex()
    is_redos_flags = [isinstance(warning.message, ReDoSWarning) for warning in recwarn]
    assert not any(is_redos_flags)


def test_sequential_unbounded_quantifiers_do_not_trigger_the_warning(
    recwarn: pytest.WarningsRecorder,
):
    builder = RegexBuilder().one_or_more().digit().one_or_more().letter()
    builder.to_regex()
    is_redos_flags = [isinstance(warning.message, ReDoSWarning) for warning in recwarn]
    assert not any(is_redos_flags)


def test_warning_only_fires_once_per_construct_within_a_single_terminal_call():
    with pytest.warns(ReDoSWarning) as record:
        _classic_redos_builder().to_regex()
    redos_records = [w for w in record if isinstance(w.message, ReDoSWarning)]
    assert len(redos_records) == 1


def test_terminal_still_returns_the_compiled_regex_after_warning():
    with pytest.warns(ReDoSWarning):
        compiled = _classic_redos_builder().to_regex()
    assert compiled.source == "(?:\\d+)+"