summaryrefslogtreecommitdiffstats
path: root/test/TestMatchError.py
blob: f17d8656a79ca98d2ca94b9da24d9760e07e28fd (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
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
"""Tests for MatchError."""

import operator

import pytest

from ansiblelint.errors import MatchError
from ansiblelint.rules import AnsibleLintRule
from ansiblelint.rules.AlwaysRunRule import AlwaysRunRule
from ansiblelint.rules.BecomeUserWithoutBecomeRule import BecomeUserWithoutBecomeRule


class DummyTestObject:
    """A dummy object for equality tests."""

    def __repr__(self):
        """Return a dummy object representation for parmetrize."""
        return '{self.__class__.__name__}()'.format(self=self)

    def __eq__(self, other):
        """Report the equality check failure with any object."""
        return False

    def __ne__(self, other):
        """Report the confirmation of inequality with any object."""
        return True


class DummySentinelTestObject:
    """A dummy object for equality protocol tests with sentinel."""

    def __eq__(self, other):
        """Return sentinel as result of equality check w/ anything."""
        return 'EQ_SENTINEL'

    def __ne__(self, other):
        """Return sentinel as result of inequality check w/ anything."""
        return 'NE_SENTINEL'

    def __lt__(self, other):
        """Return sentinel as result of less than check w/ anything."""
        return 'LT_SENTINEL'

    def __gt__(self, other):
        """Return sentinel as result of greater than chk w/ anything."""
        return 'GT_SENTINEL'


@pytest.mark.parametrize(
    ('left_match_error', 'right_match_error'),
    (
        (MatchError("foo"), MatchError("foo")),
        (MatchError("a", details="foo"), MatchError("a", details="foo")),
    ),
)
def test_matcherror_compare(left_match_error, right_match_error):
    """Check that MatchError instances with similar attrs are equivalent."""
    assert left_match_error == right_match_error


class AnsibleLintRuleWithStringId(AnsibleLintRule):
    id = "ANSIBLE200"


def test_matcherror_invalid():
    """Ensure that MatchError requires message or rule."""
    expected_err = r"^MatchError\(\) missing a required argument: one of 'message' or 'rule'$"
    with pytest.raises(TypeError, match=expected_err):
        MatchError()


@pytest.mark.parametrize(
    ('left_match_error', 'right_match_error'), (
        # sorting by message
        (MatchError("z"), MatchError("a")),
        # filenames takes priority in sorting
        (MatchError("a", filename="b"), MatchError("a", filename="a")),
        # rule id 501 > rule id 101
        (MatchError(rule=BecomeUserWithoutBecomeRule), MatchError(rule=AlwaysRunRule)),
        # rule id "200" > rule id 101
        (MatchError(rule=AnsibleLintRuleWithStringId), MatchError(rule=AlwaysRunRule)),
        # details are taken into account
        (MatchError("a", details="foo"), MatchError("a", details="bar")),
    ))
class TestMatchErrorCompare:

    def test_match_error_less_than(self, left_match_error, right_match_error):
        """Check 'less than' protocol implementation in MatchError."""
        assert right_match_error < left_match_error

    def test_match_error_greater_than(self, left_match_error, right_match_error):
        """Check 'greater than' protocol implementation in MatchError."""
        assert left_match_error > right_match_error

    def test_match_error_not_equal(self, left_match_error, right_match_error):
        """Check 'not equals' protocol implementation in MatchError."""
        assert left_match_error != right_match_error


@pytest.mark.parametrize(
    'other',
    (
        None,
        "foo",
        42,
        Exception("foo"),
    ),
    ids=repr,
)
@pytest.mark.parametrize(
    ('operation', 'operator_char'),
    (
        pytest.param(operator.le, '<=', id='<='),
        pytest.param(operator.gt, '>', id='>'),
    ),
)
def test_matcherror_compare_no_other_fallback(other, operation, operator_char):
    """Check that MatchError comparison with other types causes TypeError."""
    expected_error = (
        r'^('
        r'unsupported operand type\(s\) for {operator!s}:|'
        r"'{operator!s}' not supported between instances of"
        r") 'MatchError' and '{other_type!s}'$".
        format(other_type=type(other).__name__, operator=operator_char)
    )
    with pytest.raises(TypeError, match=expected_error):
        operation(MatchError("foo"), other)


@pytest.mark.parametrize(
    'other',
    (
        None,
        'foo',
        42,
        Exception('foo'),
        DummyTestObject(),
    ),
    ids=repr,
)
@pytest.mark.parametrize(
    ('operation', 'expected_value'),
    (
        (operator.eq, False),
        (operator.ne, True),
    ),
    ids=('==', '!=')
)
def test_matcherror_compare_with_other_fallback(
        other,
        operation,
        expected_value,
):
    """Check that MatchError comparison runs other types fallbacks."""
    assert operation(MatchError("foo"), other) is expected_value


@pytest.mark.parametrize(
    ('operation', 'expected_value'),
    (
        (operator.eq, 'EQ_SENTINEL'),
        (operator.ne, 'NE_SENTINEL'),
        # NOTE: these are swapped because when we do `x < y`, and `x.__lt__(y)`
        # NOTE: returns `NotImplemented`, Python will reverse the check into
        # NOTE: `y > x`, and so `y.__gt__(x) is called.
        # Ref: https://docs.python.org/3/reference/datamodel.html#object.__lt__
        (operator.lt, 'GT_SENTINEL'),
        (operator.gt, 'LT_SENTINEL'),
    ),
    ids=('==', '!=', '<', '>'),
)
def test_matcherror_compare_with_dummy_sentinel(operation, expected_value):
    """Check that MatchError comparison runs other types fallbacks."""
    dummy_obj = DummySentinelTestObject()
    # NOTE: This assertion abuses the CPython property to cache short string
    # NOTE: objects because the identity check is more presice and we don't
    # NOTE: want extra operator protocol methods to influence the test.
    assert operation(MatchError("foo"), dummy_obj) is expected_value