checker_test_case.py 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103
  1. # Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
  2. # For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE
  3. # Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt
  4. from __future__ import annotations
  5. import contextlib
  6. import warnings
  7. from collections.abc import Generator, Iterator
  8. from typing import Any
  9. from astroid import nodes
  10. from pylint.constants import IS_PYPY, PY38_PLUS, PY39_PLUS
  11. from pylint.testutils.global_test_linter import linter
  12. from pylint.testutils.output_line import MessageTest
  13. from pylint.testutils.unittest_linter import UnittestLinter
  14. from pylint.utils import ASTWalker
  15. class CheckerTestCase:
  16. """A base testcase class for unit testing individual checker classes."""
  17. # TODO: Figure out way to type this as type[BaseChecker] while also
  18. # setting self.checker correctly.
  19. CHECKER_CLASS: Any
  20. CONFIG: dict[str, Any] = {}
  21. def setup_method(self) -> None:
  22. self.linter = UnittestLinter()
  23. self.checker = self.CHECKER_CLASS(self.linter)
  24. for key, value in self.CONFIG.items():
  25. setattr(self.checker.linter.config, key, value)
  26. self.checker.open()
  27. @contextlib.contextmanager
  28. def assertNoMessages(self) -> Iterator[None]:
  29. """Assert that no messages are added by the given method."""
  30. with self.assertAddsMessages():
  31. yield
  32. @contextlib.contextmanager
  33. def assertAddsMessages(
  34. self, *messages: MessageTest, ignore_position: bool = False
  35. ) -> Generator[None, None, None]:
  36. """Assert that exactly the given method adds the given messages.
  37. The list of messages must exactly match *all* the messages added by the
  38. method. Additionally, we check to see whether the args in each message can
  39. actually be substituted into the message string.
  40. Using the keyword argument `ignore_position`, all checks for position
  41. arguments (line, col_offset, ...) will be skipped. This can be used to
  42. just test messages for the correct node.
  43. """
  44. yield
  45. got = self.linter.release_messages()
  46. no_msg = "No message."
  47. expected = "\n".join(repr(m) for m in messages) or no_msg
  48. got_str = "\n".join(repr(m) for m in got) or no_msg
  49. msg = (
  50. "Expected messages did not match actual.\n"
  51. f"\nExpected:\n{expected}\n\nGot:\n{got_str}\n"
  52. )
  53. assert len(messages) == len(got), msg
  54. for expected_msg, gotten_msg in zip(messages, got):
  55. assert expected_msg.msg_id == gotten_msg.msg_id, msg
  56. assert expected_msg.node == gotten_msg.node, msg
  57. assert expected_msg.args == gotten_msg.args, msg
  58. assert expected_msg.confidence == gotten_msg.confidence, msg
  59. if ignore_position:
  60. # Do not check for line, col_offset etc...
  61. continue
  62. assert expected_msg.line == gotten_msg.line, msg
  63. assert expected_msg.col_offset == gotten_msg.col_offset, msg
  64. if PY38_PLUS and not IS_PYPY or PY39_PLUS:
  65. # TODO: 3.0: Remove deprecated missing arguments and remove the warning
  66. if not expected_msg.end_line == gotten_msg.end_line:
  67. warnings.warn( # pragma: no cover
  68. f"The end_line attribute of {gotten_msg} does not match "
  69. f"the expected value in {expected_msg}. In pylint 3.0 correct end_line "
  70. "attributes will be required for MessageTest.",
  71. DeprecationWarning,
  72. stacklevel=2,
  73. )
  74. if not expected_msg.end_col_offset == gotten_msg.end_col_offset:
  75. warnings.warn( # pragma: no cover
  76. f"The end_col_offset attribute of {gotten_msg} does not match "
  77. f"the expected value in {expected_msg}. In pylint 3.0 correct end_col_offset "
  78. "attributes will be required for MessageTest.",
  79. DeprecationWarning,
  80. stacklevel=2,
  81. )
  82. def walk(self, node: nodes.NodeNG) -> None:
  83. """Recursive walk on the given node."""
  84. walker = ASTWalker(linter)
  85. walker.add_checker(self.checker)
  86. walker.walk(node)