From ee865d2f0ff4da4012c68e4393aa46b24dd86156 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 9 Jan 2025 21:21:47 +0100 Subject: [PATCH] Add exception-translations rule to quality_scale pytest validation (#131914) * Add exception-translations rule to quality_scale pytest validation * Adjust * Return empty dict if file is missing * Fix * Improve typing * Address comments * Update tests/components/conftest.py * Update tests/components/conftest.py * Update tests/components/conftest.py --------- Co-authored-by: Robert Resch --- tests/common.py | 31 +++++++++++++++++++++++++++++-- tests/components/conftest.py | 30 ++++++++++++++++++++++++++++-- 2 files changed, 57 insertions(+), 4 deletions(-) diff --git a/tests/common.py b/tests/common.py index ac6f10b8c44..9386fdee729 100644 --- a/tests/common.py +++ b/tests/common.py @@ -15,7 +15,7 @@ from collections.abc import ( ) from contextlib import asynccontextmanager, contextmanager, suppress from datetime import UTC, datetime, timedelta -from enum import Enum +from enum import Enum, StrEnum import functools as ft from functools import lru_cache from io import StringIO @@ -108,7 +108,7 @@ from homeassistant.util.json import ( from homeassistant.util.signal_type import SignalType import homeassistant.util.ulid as ulid_util from homeassistant.util.unit_system import METRIC_SYSTEM -import homeassistant.util.yaml.loader as yaml_loader +from homeassistant.util.yaml import load_yaml_dict, loader as yaml_loader from .testing_config.custom_components.test_constant_deprecation import ( import_deprecated_constant, @@ -122,6 +122,14 @@ CLIENT_ID = "https://example.com/app" CLIENT_REDIRECT_URI = "https://example.com/app/callback" +class QualityScaleStatus(StrEnum): + """Source of core configuration.""" + + DONE = "done" + EXEMPT = "exempt" + TODO = "todo" + + async def async_get_device_automations( hass: HomeAssistant, automation_type: device_automation.DeviceAutomationType, @@ -1832,3 +1840,22 @@ def reset_translation_cache(hass: HomeAssistant, components: list[str]) -> None: for loaded_components in loaded_categories.values(): for component_to_unload in components: loaded_components.pop(component_to_unload, None) + + +@lru_cache +def get_quality_scale(integration: str) -> dict[str, QualityScaleStatus]: + """Load quality scale for integration.""" + quality_scale_file = pathlib.Path( + f"homeassistant/components/{integration}/quality_scale.yaml" + ) + if not quality_scale_file.exists(): + return {} + raw = load_yaml_dict(quality_scale_file) + return { + rule: ( + QualityScaleStatus(details) + if isinstance(details, str) + else QualityScaleStatus(details["status"]) + ) + for rule, details in raw["rules"].items() + } diff --git a/tests/components/conftest.py b/tests/components/conftest.py index 81f7b2044d6..362a1bff4ee 100644 --- a/tests/components/conftest.py +++ b/tests/components/conftest.py @@ -7,6 +7,7 @@ from collections.abc import AsyncGenerator, Callable, Generator from functools import lru_cache from importlib.util import find_spec from pathlib import Path +import re import string from typing import TYPE_CHECKING, Any from unittest.mock import AsyncMock, MagicMock, patch @@ -42,6 +43,8 @@ from homeassistant.helpers import issue_registry as ir from homeassistant.helpers.translation import async_get_translations from homeassistant.util import yaml +from tests.common import QualityScaleStatus, get_quality_scale + if TYPE_CHECKING: from homeassistant.components.hassio import AddonManager @@ -51,6 +54,9 @@ if TYPE_CHECKING: from .sensor.common import MockSensor from .switch.common import MockSwitch +# Regex for accessing the integration name from the test path +RE_REQUEST_DOMAIN = re.compile(r".*tests\/components\/([^/]+)\/.*") + @pytest.fixture(scope="session", autouse=find_spec("zeroconf") is not None) def patch_zeroconf_multiple_catcher() -> Generator[None]: @@ -804,12 +810,29 @@ async def _check_create_issue_translations( ) +def _get_request_quality_scale( + request: pytest.FixtureRequest, rule: str +) -> QualityScaleStatus: + if not (match := RE_REQUEST_DOMAIN.match(str(request.path))): + return QualityScaleStatus.TODO + integration = match.groups(1)[0] + return get_quality_scale(integration).get(rule, QualityScaleStatus.TODO) + + async def _check_exception_translation( hass: HomeAssistant, exception: HomeAssistantError, translation_errors: dict[str, str], + request: pytest.FixtureRequest, ) -> None: if exception.translation_key is None: + if ( + _get_request_quality_scale(request, "exception-translations") + is QualityScaleStatus.DONE + ): + translation_errors["quality_scale"] = ( + f"Found untranslated {type(exception).__name__} exception: {exception}" + ) return await _validate_translation( hass, @@ -823,13 +846,14 @@ async def _check_exception_translation( @pytest.fixture(autouse=True) async def check_translations( - ignore_translations: str | list[str], + ignore_translations: str | list[str], request: pytest.FixtureRequest ) -> AsyncGenerator[None]: """Check that translation requirements are met. Current checks: - data entry flow results (ConfigFlow/OptionsFlow/RepairFlow) - issue registry entries + - action (service) exceptions """ if not isinstance(ignore_translations, list): ignore_translations = [ignore_translations] @@ -887,7 +911,9 @@ async def check_translations( ) except HomeAssistantError as err: translation_coros.add( - _check_exception_translation(self._hass, err, translation_errors) + _check_exception_translation( + self._hass, err, translation_errors, request + ) ) raise