mirror of
https://github.com/home-assistant/core.git
synced 2026-08-24 10:13:52 -05:00
Migrate diagnostics quality scale check from hassfest to pylint (#170717)
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
"""Checker for missing diagnostics functions.
|
||||
|
||||
**Quality-scale-gated** (Gold): only fires for integrations whose
|
||||
``quality_scale.yaml`` marks ``diagnostics`` as ``done``.
|
||||
|
||||
The integration must have a ``diagnostics.py`` module that implements at
|
||||
least one of ``async_get_config_entry_diagnostics`` or
|
||||
``async_get_device_diagnostics``.
|
||||
|
||||
https://developers.home-assistant.io/docs/core/integration-quality-scale/rules/diagnostics/
|
||||
"""
|
||||
|
||||
from astroid import nodes
|
||||
from pylint.checkers import BaseChecker
|
||||
from pylint.lint import PyLinter
|
||||
|
||||
from pylint_home_assistant.const import Module, QualityScaleRule
|
||||
from pylint_home_assistant.helpers.module_info import get_module_platform
|
||||
from pylint_home_assistant.helpers.quality_scale import quality_scale_rule_is_done
|
||||
|
||||
_DIAGNOSTICS_FUNCTIONS: frozenset[str] = frozenset(
|
||||
{
|
||||
"async_get_config_entry_diagnostics",
|
||||
"async_get_device_diagnostics",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class DiagnosticsChecker(BaseChecker):
|
||||
"""Checker for diagnostics functions in diagnostics modules."""
|
||||
|
||||
name = "home_assistant_diagnostics"
|
||||
priority = -1
|
||||
msgs = {
|
||||
"W7412": (
|
||||
"Integration diagnostics module should implement "
|
||||
"`async_get_config_entry_diagnostics` or "
|
||||
"`async_get_device_diagnostics` "
|
||||
"(https://developers.home-assistant.io/docs/core/"
|
||||
"integration-quality-scale/rules/diagnostics)",
|
||||
"home-assistant-missing-diagnostics",
|
||||
"Used when an integration's diagnostics.py does not implement "
|
||||
"at least one of async_get_config_entry_diagnostics or "
|
||||
"async_get_device_diagnostics.",
|
||||
),
|
||||
}
|
||||
options = ()
|
||||
|
||||
def visit_module(self, node: nodes.Module) -> None:
|
||||
"""Check that diagnostics modules define a diagnostics function."""
|
||||
platform = get_module_platform(node.name)
|
||||
if platform != Module.DIAGNOSTICS:
|
||||
return
|
||||
|
||||
if not quality_scale_rule_is_done(node, QualityScaleRule.DIAGNOSTICS):
|
||||
return
|
||||
|
||||
for child in node.nodes_of_class(nodes.AsyncFunctionDef):
|
||||
if child.name in _DIAGNOSTICS_FUNCTIONS:
|
||||
return
|
||||
|
||||
self.add_message("home-assistant-missing-diagnostics", node=node)
|
||||
|
||||
|
||||
def register(linter: PyLinter) -> None:
|
||||
"""Register the checker."""
|
||||
linter.register_checker(DiagnosticsChecker(linter))
|
||||
@@ -15,7 +15,6 @@ from .quality_scale_validation import (
|
||||
action_setup,
|
||||
config_entry_unloading,
|
||||
config_flow,
|
||||
diagnostics,
|
||||
discovery,
|
||||
reauthentication_flow,
|
||||
reconfiguration_flow,
|
||||
@@ -74,7 +73,7 @@ ALL_RULES = [
|
||||
Rule("test-coverage", ScaledQualityScaleTiers.SILVER),
|
||||
# GOLD: [
|
||||
Rule("devices", ScaledQualityScaleTiers.GOLD),
|
||||
Rule("diagnostics", ScaledQualityScaleTiers.GOLD, diagnostics),
|
||||
Rule("diagnostics", ScaledQualityScaleTiers.GOLD),
|
||||
Rule("discovery", ScaledQualityScaleTiers.GOLD, discovery),
|
||||
Rule("discovery-update-info", ScaledQualityScaleTiers.GOLD),
|
||||
Rule("docs-data-update", ScaledQualityScaleTiers.GOLD),
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
"""Enforce that the integration implements diagnostics.
|
||||
|
||||
https://developers.home-assistant.io/docs/core/integration-quality-scale/rules/diagnostics/
|
||||
"""
|
||||
|
||||
import ast
|
||||
|
||||
from script.hassfest import ast_parse_module
|
||||
from script.hassfest.model import Config, Integration
|
||||
|
||||
DIAGNOSTICS_FUNCTIONS = {
|
||||
"async_get_config_entry_diagnostics",
|
||||
"async_get_device_diagnostics",
|
||||
}
|
||||
|
||||
|
||||
def _has_diagnostics_function(module: ast.Module) -> bool:
|
||||
"""Test if the module defines at least one of diagnostic functions."""
|
||||
return any(
|
||||
type(item) is ast.AsyncFunctionDef and item.name in DIAGNOSTICS_FUNCTIONS
|
||||
for item in ast.walk(module)
|
||||
)
|
||||
|
||||
|
||||
def validate(
|
||||
config: Config, integration: Integration, *, rules_done: set[str]
|
||||
) -> list[str] | None:
|
||||
"""Validate that the integration implements diagnostics."""
|
||||
|
||||
diagnostics_file = integration.path / "diagnostics.py"
|
||||
if not diagnostics_file.exists():
|
||||
return [
|
||||
"Integration does implement diagnostics platform "
|
||||
"(is missing diagnostics.py)",
|
||||
]
|
||||
|
||||
diagnostics = ast_parse_module(diagnostics_file)
|
||||
|
||||
if not _has_diagnostics_function(diagnostics):
|
||||
return [
|
||||
f"Integration is missing one of {DIAGNOSTICS_FUNCTIONS} "
|
||||
f"in {diagnostics_file}"
|
||||
]
|
||||
|
||||
return None
|
||||
@@ -0,0 +1,172 @@
|
||||
"""Tests for the diagnostics quality scale checker."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import astroid
|
||||
from pylint.testutils import MessageTest, UnittestLinter
|
||||
from pylint.utils.ast_walker import ASTWalker
|
||||
from pylint_home_assistant.checkers.quality_scale.diagnostics import DiagnosticsChecker
|
||||
from pylint_home_assistant.helpers.quality_scale import clear_quality_scale_cache
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
from tests.pylint import assert_adds_messages, assert_no_messages
|
||||
|
||||
|
||||
@pytest.fixture(name="diagnostics_checker")
|
||||
def diagnostics_checker_fixture(linter: UnittestLinter) -> DiagnosticsChecker:
|
||||
"""Fixture to provide a diagnostics checker."""
|
||||
clear_quality_scale_cache()
|
||||
return DiagnosticsChecker(linter)
|
||||
|
||||
|
||||
def _create_quality_scale(integration_dir: Path, rules: dict | None = None) -> None:
|
||||
"""Create a quality_scale.yaml in the integration directory."""
|
||||
if rules is not None:
|
||||
(integration_dir / "quality_scale.yaml").write_text(yaml.dump({"rules": rules}))
|
||||
|
||||
|
||||
def _make_integration(tmp_path: Path) -> Path:
|
||||
"""Create a fake integration directory under components/."""
|
||||
integration_dir = tmp_path / "homeassistant" / "components" / "test_int"
|
||||
integration_dir.mkdir(parents=True)
|
||||
return integration_dir
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"code",
|
||||
[
|
||||
pytest.param(
|
||||
"""
|
||||
async def async_get_config_entry_diagnostics(hass, entry):
|
||||
return {"key": "value"}
|
||||
""",
|
||||
id="config_entry_diagnostics",
|
||||
),
|
||||
pytest.param(
|
||||
"""
|
||||
async def async_get_device_diagnostics(hass, entry, device):
|
||||
return {"key": "value"}
|
||||
""",
|
||||
id="device_diagnostics",
|
||||
),
|
||||
pytest.param(
|
||||
"""
|
||||
async def async_get_config_entry_diagnostics(hass, entry):
|
||||
return {"key": "value"}
|
||||
|
||||
async def async_get_device_diagnostics(hass, entry, device):
|
||||
return {"key": "value"}
|
||||
""",
|
||||
id="both_diagnostics",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_diagnostics_present(
|
||||
linter: UnittestLinter,
|
||||
diagnostics_checker: DiagnosticsChecker,
|
||||
tmp_path: Path,
|
||||
code: str,
|
||||
) -> None:
|
||||
"""No warning when diagnostics function is defined and rule is done."""
|
||||
integration_dir = _make_integration(tmp_path)
|
||||
_create_quality_scale(integration_dir, {"diagnostics": "done"})
|
||||
|
||||
root_node = astroid.parse(code, "homeassistant.components.test_int.diagnostics")
|
||||
root_node.file = str(integration_dir / "diagnostics.py")
|
||||
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(diagnostics_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
def test_diagnostics_missing_fires(
|
||||
linter: UnittestLinter,
|
||||
diagnostics_checker: DiagnosticsChecker,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""Warning when no diagnostics function is defined and rule is done."""
|
||||
integration_dir = _make_integration(tmp_path)
|
||||
_create_quality_scale(integration_dir, {"diagnostics": "done"})
|
||||
|
||||
root_node = astroid.parse(
|
||||
"""
|
||||
async def async_setup(hass, config):
|
||||
pass
|
||||
""",
|
||||
"homeassistant.components.test_int.diagnostics",
|
||||
)
|
||||
root_node.file = str(integration_dir / "diagnostics.py")
|
||||
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(diagnostics_checker)
|
||||
|
||||
with assert_adds_messages(
|
||||
linter,
|
||||
MessageTest(
|
||||
msg_id="home-assistant-missing-diagnostics",
|
||||
node=root_node,
|
||||
line=0,
|
||||
col_offset=0,
|
||||
),
|
||||
):
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("module_name", "rules"),
|
||||
[
|
||||
pytest.param(
|
||||
"homeassistant.components.test_int.diagnostics",
|
||||
None,
|
||||
id="no_quality_scale_file",
|
||||
),
|
||||
pytest.param(
|
||||
"homeassistant.components.test_int.diagnostics",
|
||||
{"diagnostics": "todo"},
|
||||
id="rule_todo",
|
||||
),
|
||||
pytest.param(
|
||||
"homeassistant.components.test_int.diagnostics",
|
||||
{"diagnostics": {"status": "exempt", "comment": "reason"}},
|
||||
id="rule_exempt",
|
||||
),
|
||||
pytest.param(
|
||||
"homeassistant.components.test_int.sensor",
|
||||
{"diagnostics": "done"},
|
||||
id="not_diagnostics_module",
|
||||
),
|
||||
pytest.param(
|
||||
"not_homeassistant.something.diagnostics",
|
||||
{"diagnostics": "done"},
|
||||
id="not_an_integration",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_diagnostics_not_fired(
|
||||
linter: UnittestLinter,
|
||||
diagnostics_checker: DiagnosticsChecker,
|
||||
tmp_path: Path,
|
||||
module_name: str,
|
||||
rules: dict | None,
|
||||
) -> None:
|
||||
"""No warning when rule is not done or module is not diagnostics."""
|
||||
integration_dir = _make_integration(tmp_path)
|
||||
_create_quality_scale(integration_dir, rules)
|
||||
|
||||
root_node = astroid.parse(
|
||||
"""
|
||||
async def async_setup(hass, config):
|
||||
pass
|
||||
""",
|
||||
module_name,
|
||||
)
|
||||
root_node.file = str(integration_dir / "diagnostics.py")
|
||||
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(diagnostics_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walker.walk(root_node)
|
||||
Reference in New Issue
Block a user