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 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()
}

View File

@ -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