Add tests for pylint plugins (#65436)

Co-authored-by: epenet <epenet@users.noreply.github.com>
This commit is contained in:
epenet 2022-02-03 10:01:02 +01:00 committed by GitHub
parent b587201654
commit c8504bd21d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 192 additions and 10 deletions

View File

@ -1 +1,31 @@
"""Tests for pylint."""
import contextlib
from pylint.testutils.unittest_linter import UnittestLinter
@contextlib.contextmanager
def assert_no_messages(linter: UnittestLinter):
"""Assert that no messages are added by the given method."""
with assert_adds_messages(linter):
yield
@contextlib.contextmanager
def assert_adds_messages(linter: UnittestLinter, *messages):
"""Assert that exactly the given method adds the given messages.
The list of messages must exactly match *all* the messages added by the
method. Additionally, we check to see whether the args in each message can
actually be substituted into the message string.
"""
yield
got = linter.release_messages()
no_msg = "No message."
expected = "\n".join(repr(m) for m in messages) or no_msg
got_str = "\n".join(repr(m) for m in got) or no_msg
msg = (
"Expected messages did not match actual.\n"
f"\nExpected:\n{expected}\n\nGot:\n{got_str}\n"
)
assert got == list(messages), msg

30
tests/pylint/conftest.py Normal file
View File

@ -0,0 +1,30 @@
"""Configuration for pylint tests."""
from importlib.machinery import SourceFileLoader
from types import ModuleType
from pylint.checkers import BaseChecker
from pylint.testutils.unittest_linter import UnittestLinter
import pytest
@pytest.fixture(name="hass_enforce_type_hints")
def hass_enforce_type_hints_fixture() -> ModuleType:
"""Fixture to provide a requests mocker."""
loader = SourceFileLoader(
"hass_enforce_type_hints", "pylint/plugins/hass_enforce_type_hints.py"
)
return loader.load_module(None)
@pytest.fixture(name="linter")
def linter_fixture() -> UnittestLinter:
"""Fixture to provide a requests mocker."""
return UnittestLinter()
@pytest.fixture(name="type_hint_checker")
def type_hint_checker_fixture(hass_enforce_type_hints, linter) -> BaseChecker:
"""Fixture to provide a requests mocker."""
type_hint_checker = hass_enforce_type_hints.HassTypeHintChecker(linter)
type_hint_checker.module = "homeassistant.components.pylint_test"
return type_hint_checker

View File

@ -1,16 +1,17 @@
"""Tests for pylint hass_enforce_type_hints plugin."""
# pylint:disable=protected-access
from importlib.machinery import SourceFileLoader
import re
from types import ModuleType
from unittest.mock import patch
import astroid
from pylint.checkers import BaseChecker
import pylint.testutils
from pylint.testutils.unittest_linter import UnittestLinter
import pytest
loader = SourceFileLoader(
"hass_enforce_type_hints", "pylint/plugins/hass_enforce_type_hints.py"
)
hass_enforce_type_hints = loader.load_module(None)
_TYPE_HINT_MATCHERS: dict[str, re.Pattern] = hass_enforce_type_hints._TYPE_HINT_MATCHERS
from . import assert_adds_messages, assert_no_messages
@pytest.mark.parametrize(
@ -20,9 +21,17 @@ _TYPE_HINT_MATCHERS: dict[str, re.Pattern] = hass_enforce_type_hints._TYPE_HINT_
("Callable[..., Awaitable[None]]", "Callable", "...", "Awaitable[None]"),
],
)
def test_regex_x_of_y_comma_z(string, expected_x, expected_y, expected_z):
def test_regex_x_of_y_comma_z(
hass_enforce_type_hints: ModuleType,
string: str,
expected_x: str,
expected_y: str,
expected_z: str,
) -> None:
"""Test x_of_y_comma_z regexes."""
assert (match := _TYPE_HINT_MATCHERS["x_of_y_comma_z"].match(string))
matchers: dict[str, re.Pattern] = hass_enforce_type_hints._TYPE_HINT_MATCHERS
assert (match := matchers["x_of_y_comma_z"].match(string))
assert match.group(0) == string
assert match.group(1) == expected_x
assert match.group(2) == expected_y
@ -33,9 +42,122 @@ def test_regex_x_of_y_comma_z(string, expected_x, expected_y, expected_z):
("string", "expected_a", "expected_b"),
[("DiscoveryInfoType | None", "DiscoveryInfoType", "None")],
)
def test_regex_a_or_b(string, expected_a, expected_b):
def test_regex_a_or_b(
hass_enforce_type_hints: ModuleType, string: str, expected_a: str, expected_b: str
) -> None:
"""Test a_or_b regexes."""
assert (match := _TYPE_HINT_MATCHERS["a_or_b"].match(string))
matchers: dict[str, re.Pattern] = hass_enforce_type_hints._TYPE_HINT_MATCHERS
assert (match := matchers["a_or_b"].match(string))
assert match.group(0) == string
assert match.group(1) == expected_a
assert match.group(2) == expected_b
@pytest.mark.parametrize(
"code",
[
"""
async def setup( #@
arg1, arg2
):
pass
"""
],
)
def test_ignore_not_annotations(
hass_enforce_type_hints: ModuleType, type_hint_checker: BaseChecker, code: str
) -> None:
"""Ensure that _is_valid_type is not run if there are no annotations."""
func_node = astroid.extract_node(code)
with patch.object(
hass_enforce_type_hints, "_is_valid_type", return_value=True
) as is_valid_type:
type_hint_checker.visit_asyncfunctiondef(func_node)
is_valid_type.assert_not_called()
@pytest.mark.parametrize(
"code",
[
"""
async def setup( #@
arg1: ArgHint, arg2
):
pass
""",
"""
async def setup( #@
arg1, arg2
) -> ReturnHint:
pass
""",
"""
async def setup( #@
arg1: ArgHint, arg2: ArgHint
) -> ReturnHint:
pass
""",
],
)
def test_dont_ignore_partial_annotations(
hass_enforce_type_hints: ModuleType, type_hint_checker: BaseChecker, code: str
) -> None:
"""Ensure that _is_valid_type is run if there is at least one annotation."""
func_node = astroid.extract_node(code)
with patch.object(
hass_enforce_type_hints, "_is_valid_type", return_value=True
) as is_valid_type:
type_hint_checker.visit_asyncfunctiondef(func_node)
is_valid_type.assert_called()
def test_invalid_discovery_info(
linter: UnittestLinter, type_hint_checker: BaseChecker
) -> None:
"""Ensure invalid hints are rejected for discovery_info."""
type_hint_checker.module = "homeassistant.components.pylint_test.device_tracker"
func_node, discovery_info_node = astroid.extract_node(
"""
async def async_setup_scanner( #@
hass: HomeAssistant,
config: ConfigType,
async_see: Callable[..., Awaitable[None]],
discovery_info: dict[str, Any] | None = None, #@
) -> bool:
pass
"""
)
with assert_adds_messages(
linter,
pylint.testutils.MessageTest(
msg_id="hass-argument-type",
node=discovery_info_node,
args=(4, "DiscoveryInfoType | None"),
),
):
type_hint_checker.visit_asyncfunctiondef(func_node)
def test_valid_discovery_info(
linter: UnittestLinter, type_hint_checker: BaseChecker
) -> None:
"""Ensure valid hints are accepted for discovery_info."""
type_hint_checker.module = "homeassistant.components.pylint_test.device_tracker"
func_node = astroid.extract_node(
"""
async def async_setup_scanner( #@
hass: HomeAssistant,
config: ConfigType,
async_see: Callable[..., Awaitable[None]],
discovery_info: DiscoveryInfoType | None = None,
) -> bool:
pass
"""
)
with assert_no_messages(linter):
type_hint_checker.visit_asyncfunctiondef(func_node)