Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
3 changes: 3 additions & 0 deletions doc/whatsnew/fragments/9598.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
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.

Closes #9598
Comment thread
ShehabSherif0 marked this conversation as resolved.
Outdated
48 changes: 48 additions & 0 deletions pylint/testutils/checker_test_case.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,54 @@ def assertNoMessages(self) -> Iterator[None]:
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
Comment thread
ShehabSherif0 marked this conversation as resolved.
Outdated
none of the *specific* messages passed as arguments are emitted, while
other messages may still be present.
"""
yield
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)
Comment thread
ShehabSherif0 marked this conversation as resolved.
Outdated

@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
96 changes: 96 additions & 0 deletions tests/testutils/test_checker_test_case.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# 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):
Comment thread
Pierre-Sassoulas marked this conversation as resolved.
Outdated
with self.assertDoesNotAddMessages(_MSG_A, _MSG_B):
self.linter.add_message("W9902", line=2)