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 <robert@resch.dev>
This commit is contained in:
epenet 2025-01-09 21:21:47 +01:00 committed by GitHub
parent dd57c75e64
commit ee865d2f0f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 57 additions and 4 deletions

View File

@ -15,7 +15,7 @@ from collections.abc import (
) )
from contextlib import asynccontextmanager, contextmanager, suppress from contextlib import asynccontextmanager, contextmanager, suppress
from datetime import UTC, datetime, timedelta from datetime import UTC, datetime, timedelta
from enum import Enum from enum import Enum, StrEnum
import functools as ft import functools as ft
from functools import lru_cache from functools import lru_cache
from io import StringIO from io import StringIO
@ -108,7 +108,7 @@ from homeassistant.util.json import (
from homeassistant.util.signal_type import SignalType from homeassistant.util.signal_type import SignalType
import homeassistant.util.ulid as ulid_util import homeassistant.util.ulid as ulid_util
from homeassistant.util.unit_system import METRIC_SYSTEM 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 ( from .testing_config.custom_components.test_constant_deprecation import (
import_deprecated_constant, import_deprecated_constant,
@ -122,6 +122,14 @@ CLIENT_ID = "https://example.com/app"
CLIENT_REDIRECT_URI = "https://example.com/app/callback" 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( async def async_get_device_automations(
hass: HomeAssistant, hass: HomeAssistant,
automation_type: device_automation.DeviceAutomationType, 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 loaded_components in loaded_categories.values():
for component_to_unload in components: for component_to_unload in components:
loaded_components.pop(component_to_unload, None) 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()
}

View File

@ -7,6 +7,7 @@ from collections.abc import AsyncGenerator, Callable, Generator
from functools import lru_cache from functools import lru_cache
from importlib.util import find_spec from importlib.util import find_spec
from pathlib import Path from pathlib import Path
import re
import string import string
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any
from unittest.mock import AsyncMock, MagicMock, patch 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.helpers.translation import async_get_translations
from homeassistant.util import yaml from homeassistant.util import yaml
from tests.common import QualityScaleStatus, get_quality_scale
if TYPE_CHECKING: if TYPE_CHECKING:
from homeassistant.components.hassio import AddonManager from homeassistant.components.hassio import AddonManager
@ -51,6 +54,9 @@ if TYPE_CHECKING:
from .sensor.common import MockSensor from .sensor.common import MockSensor
from .switch.common import MockSwitch 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) @pytest.fixture(scope="session", autouse=find_spec("zeroconf") is not None)
def patch_zeroconf_multiple_catcher() -> Generator[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( async def _check_exception_translation(
hass: HomeAssistant, hass: HomeAssistant,
exception: HomeAssistantError, exception: HomeAssistantError,
translation_errors: dict[str, str], translation_errors: dict[str, str],
request: pytest.FixtureRequest,
) -> None: ) -> None:
if exception.translation_key is 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 return
await _validate_translation( await _validate_translation(
hass, hass,
@ -823,13 +846,14 @@ async def _check_exception_translation(
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
async def check_translations( async def check_translations(
ignore_translations: str | list[str], ignore_translations: str | list[str], request: pytest.FixtureRequest
) -> AsyncGenerator[None]: ) -> AsyncGenerator[None]:
"""Check that translation requirements are met. """Check that translation requirements are met.
Current checks: Current checks:
- data entry flow results (ConfigFlow/OptionsFlow/RepairFlow) - data entry flow results (ConfigFlow/OptionsFlow/RepairFlow)
- issue registry entries - issue registry entries
- action (service) exceptions
""" """
if not isinstance(ignore_translations, list): if not isinstance(ignore_translations, list):
ignore_translations = [ignore_translations] ignore_translations = [ignore_translations]
@ -887,7 +911,9 @@ async def check_translations(
) )
except HomeAssistantError as err: except HomeAssistantError as err:
translation_coros.add( translation_coros.add(
_check_exception_translation(self._hass, err, translation_errors) _check_exception_translation(
self._hass, err, translation_errors, request
)
) )
raise raise