Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions doc/whatsnew/fragments/9598.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Add ``assertDoesNotAddMessages`` to ``CheckerTestCase`` to assert that
specific messages are not emitted, while allowing other messages to be
present. This complements ``assertNoMessages`` which asserts that no
messages at all are emitted.

Refs #9598
57 changes: 57 additions & 0 deletions pylint/testutils/checker_test_case.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,63 @@
with self.assertAddsMessages():
yield

@contextlib.contextmanager
def assertDoesNotAddMessages(
self, *messages: MessageTest, ignore_position: bool = False
) -> Generator[None]:
Comment thread
ShehabSherif0 marked this conversation as resolved.
Outdated
"""Assert that the given messages are not added by the given method.

This is different from ``assertNoMessages`` which asserts that no
messages at all are added. ``assertDoesNotAddMessages`` checks that
none of the *specific* messages passed as arguments are emitted, while
other messages may still be present.
"""
if not messages:
raise TypeError(
"assertDoesNotAddMessages requires at least one MessageTest argument"
)
try:
yield
except Exception:
self.linter.release_messages()
raise
else:
got = self.linter.release_messages()
for unwanted in messages:
for gotten_msg in got:
if not self._messages_match(unwanted, gotten_msg, ignore_position):
continue
got_str = "\n".join(repr(m) for m in got)
msg = (
"Expected the following message to not be raised:\n"
f"\n {unwanted!r}\n\n"
f"but it was found among the actual messages:\n\n{got_str}\n"
)
raise AssertionError(msg)

Check notice on line 72 in pylint/testutils/checker_test_case.py

View workflow job for this annotation

GitHub Actions / pylint

R1720

Unnecessary "else" after "raise", remove the "else" and de-indent the code inside it

@staticmethod
def _messages_match(
expected: MessageTest, actual: MessageTest, ignore_position: bool
) -> bool:
if expected.msg_id != actual.msg_id:
return False
if expected.node != actual.node:
return False
if expected.args != actual.args:
return False
if expected.confidence != actual.confidence:
return False
if not ignore_position:
if expected.line != actual.line:
return False
if expected.col_offset != actual.col_offset:
return False
if expected.end_line != actual.end_line:
return False
if expected.end_col_offset != actual.end_col_offset:
return False
return True
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't we only care about the msgid ?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right. The method now accepts plain message ID strings instead of full MessageTest objects, matching the intended use pattern from the issue. The check is just: if that message ID appears anywhere in the emitted messages, fail. Position, args, node and confidence are all irrelevant to the "did this message fire at all" question. The _messages_match helper and ignore_position parameter have been removed.


@contextlib.contextmanager
def assertAddsMessages(
self, *messages: MessageTest, ignore_position: bool = False
Expand Down
150 changes: 150 additions & 0 deletions tests/testutils/test_checker_test_case.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE
# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt

"""Tests for CheckerTestCase assertion methods."""

from __future__ import annotations

import pytest

from pylint.checkers.base_checker import BaseChecker
from pylint.interfaces import UNDEFINED
from pylint.testutils import CheckerTestCase, MessageTest


class _DummyChecker(BaseChecker):
"""A minimal checker used only for testing the test infrastructure."""

name = "dummy-for-testcase"
msgs = {
"W9901": ("Dummy message A", "dummy-msg-a", "Dummy message A."),
"W9902": ("Dummy message B", "dummy-msg-b", "Dummy message B."),
}


_MSG_A = MessageTest("W9901", line=1, node=None, args=None, confidence=UNDEFINED)
_MSG_B = MessageTest("W9902", line=2, node=None, args=None, confidence=UNDEFINED)


class TestCheckerTestCase(CheckerTestCase):
CHECKER_CLASS = _DummyChecker

# -- assertAddsMessages scenarios (1-3) -----------------------------------
Comment thread
Pierre-Sassoulas marked this conversation as resolved.
Outdated

def test_assert_adds_messages_success(self) -> None:
"""Scenario 1: expected raised / actual raised."""
with self.assertAddsMessages(_MSG_A):
self.linter.add_message("W9901", line=1)

def test_assert_adds_messages_failure_not_raised(self) -> None:
"""Scenario 2: expected raised / actual not raised."""
with pytest.raises(AssertionError):
Comment thread
Pierre-Sassoulas marked this conversation as resolved.
Outdated
with self.assertAddsMessages(_MSG_A):
pass # nothing emitted

def test_assert_adds_messages_failure_wrong_message(self) -> None:
"""Scenario 3: expected raised / actual not raised but another one raised."""
with pytest.raises(AssertionError):
with self.assertAddsMessages(_MSG_A):
self.linter.add_message("W9902", line=2)

# -- assertDoesNotAddMessages scenarios (4-6) -----------------------------
Comment thread
Pierre-Sassoulas marked this conversation as resolved.
Outdated

def test_assert_does_not_add_messages_failure(self) -> None:
"""Scenario 4: expected not raised / actual raised."""
with pytest.raises(AssertionError):
Comment thread
Pierre-Sassoulas marked this conversation as resolved.
Outdated
with self.assertDoesNotAddMessages(_MSG_A):
self.linter.add_message("W9901", line=1)

def test_assert_does_not_add_messages_success(self) -> None:
"""Scenario 5: expected not raised / actual not raised."""
with self.assertDoesNotAddMessages(_MSG_A):
pass # nothing emitted

def test_assert_does_not_add_messages_success_other_raised(self) -> None:
"""Scenario 6: expected not raised / actual not raised but another one raised."""
with self.assertDoesNotAddMessages(_MSG_A):
self.linter.add_message("W9902", line=2)

# -- additional edge cases ------------------------------------------------
Comment thread
Pierre-Sassoulas marked this conversation as resolved.
Outdated

def test_assert_does_not_add_messages_ignore_position(self) -> None:
"""Position mismatch means no match when ignore_position=False."""
# Same msg_id but different line: should pass (not a match)
msg_different_line = MessageTest(
"W9901", line=99, node=None, args=None, confidence=UNDEFINED
)
with self.assertDoesNotAddMessages(msg_different_line):
self.linter.add_message("W9901", line=1)

def test_assert_does_not_add_messages_ignore_position_true(self) -> None:
"""With ignore_position=True, position differences are ignored."""
msg_different_line = MessageTest(
"W9901", line=99, node=None, args=None, confidence=UNDEFINED
)
with pytest.raises(AssertionError):
with self.assertDoesNotAddMessages(
msg_different_line, ignore_position=True
):
self.linter.add_message("W9901", line=1)

def test_assert_does_not_add_messages_multiple_unwanted(self) -> None:
"""Fails when any of several unwanted messages is found."""
with pytest.raises(AssertionError):
with self.assertDoesNotAddMessages(_MSG_A, _MSG_B):
self.linter.add_message("W9902", line=2)

def test_assert_does_not_add_messages_no_args_raises(self) -> None:
"""Calling with no arguments must raise TypeError."""
with pytest.raises(TypeError, match="requires at least one"):
with self.assertDoesNotAddMessages():
pass

def test_assert_does_not_add_messages_exception_in_body_drains_messages(
self,
) -> None:
"""An exception in the with-block must not leak messages to later tests."""
with pytest.raises(RuntimeError):
with self.assertDoesNotAddMessages(_MSG_A):
self.linter.add_message("W9901", line=1)
raise RuntimeError("something went wrong")
# Messages must have been drained; a subsequent assertNoMessages should pass.
with self.assertNoMessages():
pass

# -- _messages_match branch coverage --------------------------------------

def test_messages_match_node_mismatch(self) -> None:
expected = MessageTest("W9901", line=1, node="sentinel", args=None)
actual = MessageTest("W9901", line=1, node=None, args=None)
Comment thread
ShehabSherif0 marked this conversation as resolved.
Outdated
assert not self._messages_match(expected, actual, ignore_position=False)

def test_messages_match_args_mismatch(self) -> None:
expected = MessageTest("W9901", line=1, node=None, args=("x",))
actual = MessageTest("W9901", line=1, node=None, args=None)
assert not self._messages_match(expected, actual, ignore_position=False)

def test_messages_match_confidence_mismatch(self) -> None:
from pylint.interfaces import HIGH

Check notice on line 129 in tests/testutils/test_checker_test_case.py

View workflow job for this annotation

GitHub Actions / pylint

C0415

Import outside toplevel (pylint.interfaces.HIGH)

expected = MessageTest("W9901", line=1, node=None, args=None, confidence=HIGH)
actual = MessageTest(
"W9901", line=1, node=None, args=None, confidence=UNDEFINED
)
assert not self._messages_match(expected, actual, ignore_position=False)

def test_messages_match_col_offset_mismatch(self) -> None:
expected = MessageTest("W9901", line=1, node=None, args=None, col_offset=5)
actual = MessageTest("W9901", line=1, node=None, args=None, col_offset=10)
assert not self._messages_match(expected, actual, ignore_position=False)

def test_messages_match_end_line_mismatch(self) -> None:
expected = MessageTest("W9901", line=1, node=None, args=None, end_line=5)
actual = MessageTest("W9901", line=1, node=None, args=None, end_line=10)
assert not self._messages_match(expected, actual, ignore_position=False)

def test_messages_match_end_col_offset_mismatch(self) -> None:
expected = MessageTest("W9901", line=1, node=None, args=None, end_col_offset=5)
actual = MessageTest("W9901", line=1, node=None, args=None, end_col_offset=10)
assert not self._messages_match(expected, actual, ignore_position=False)
Loading