diff options
Diffstat (limited to 'test/test_matcherrror.py')
-rw-r--r-- | test/test_matcherrror.py | 203 |
1 files changed, 203 insertions, 0 deletions
diff --git a/test/test_matcherrror.py b/test/test_matcherrror.py new file mode 100644 index 0000000..041b7ce --- /dev/null +++ b/test/test_matcherrror.py @@ -0,0 +1,203 @@ +"""Tests for MatchError.""" + +import operator +from typing import Any, Callable + +import pytest + +from ansiblelint.errors import MatchError +from ansiblelint.file_utils import Lintable +from ansiblelint.rules.no_changed_when import CommandHasChangesCheckRule +from ansiblelint.rules.partial_become import BecomeUserWithoutBecomeRule + + +class DummyTestObject: + """A dummy object for equality tests.""" + + def __repr__(self) -> str: + """Return a dummy object representation for parametrize.""" + return f"{self.__class__.__name__}()" + + def __eq__(self, other: object) -> bool: + """Report the equality check failure with any object.""" + return False + + def __ne__(self, other: object) -> bool: + """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: object) -> bool: + """Return sentinel as result of equality check w/ anything.""" + return "EQ_SENTINEL" # type: ignore + + def __ne__(self, other: object) -> bool: + """Return sentinel as result of inequality check w/ anything.""" + return "NE_SENTINEL" # type: ignore + + def __lt__(self, other: object) -> bool: + """Return sentinel as result of less than check w/ anything.""" + return "LT_SENTINEL" # type: ignore + + def __gt__(self, other: object) -> bool: + """Return sentinel as result of greater than chk w/ anything.""" + return "GT_SENTINEL" # type: ignore + + +@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: MatchError, right_match_error: MatchError +) -> None: + """Check that MatchError instances with similar attrs are equivalent.""" + assert left_match_error == right_match_error + + +def test_matcherror_invalid() -> None: + """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): + raise 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=Lintable("b", content="")), + MatchError("a", filename=Lintable("a", content="")), + ), + # rule id partial-become > rule id no-changed-when + ( + MatchError(rule=BecomeUserWithoutBecomeRule()), + MatchError(rule=CommandHasChangesCheckRule()), + ), + # details are taken into account + (MatchError("a", details="foo"), MatchError("a", details="bar")), + # columns are taken into account + (MatchError("a", column=3), MatchError("a", column=1)), + (MatchError("a", column=3), MatchError("a")), + ), +) +class TestMatchErrorCompare: + """Test the comparison of MatchError instances.""" + + @staticmethod + def test_match_error_less_than( + left_match_error: MatchError, right_match_error: MatchError + ) -> None: + """Check 'less than' protocol implementation in MatchError.""" + assert right_match_error < left_match_error + + @staticmethod + def test_match_error_greater_than( + left_match_error: MatchError, right_match_error: MatchError + ) -> None: + """Check 'greater than' protocol implementation in MatchError.""" + assert left_match_error > right_match_error + + @staticmethod + def test_match_error_not_equal( + left_match_error: MatchError, right_match_error: MatchError + ) -> None: + """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: Any, operation: Callable[..., bool], operator_char: str +) -> None: + """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: object, + operation: Callable[..., bool], + expected_value: bool, +) -> None: + """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: Callable[..., bool], expected_value: str +) -> None: + """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 precise and we don't + # NOTE: want extra operator protocol methods to influence the test. + assert operation(MatchError("foo"), dummy_obj) is expected_value # type: ignore |