From 4ca3d7a1f8e9e773846a64d34ed30b4fce4d465e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 14 Nov 2024 07:53:13 +0000 Subject: [PATCH 1/8] Add translation checks for issue registry --- tests/components/conftest.py | 56 ++++++++++++++++++++++++++++++++---- 1 file changed, 51 insertions(+), 5 deletions(-) diff --git a/tests/components/conftest.py b/tests/components/conftest.py index 363d39a2e63..c3224fb5317 100644 --- a/tests/components/conftest.py +++ b/tests/components/conftest.py @@ -2,7 +2,8 @@ from __future__ import annotations -from collections.abc import Callable, Generator +import asyncio +from collections.abc import AsyncGenerator, Callable, Generator from importlib.util import find_spec from pathlib import Path import string @@ -32,6 +33,7 @@ from homeassistant.data_entry_flow import ( FlowManager, FlowResultType, ) +from homeassistant.helpers import issue_registry as ir from homeassistant.helpers.translation import async_get_translations if TYPE_CHECKING: @@ -670,8 +672,28 @@ async def _check_config_flow_result_translations( ) +async def _check_create_issue_translations( + issue_registry: ir.IssueRegistry, + result: ir.IssueEntry, + ignore_translations: dict[str, str], +) -> None: + if result.translation_key is None: + return + for header in ("title", "description"): + await _validate_translation( + issue_registry.hass, + ignore_translations, + "issues", + result.domain, + f"{result.translation_key}.{header}", + result.translation_placeholders, + ) + + @pytest.fixture(autouse=True) -def check_translations(ignore_translations: str | list[str]) -> Generator[None]: +async def check_translations( + ignore_translations: str | list[str], +) -> AsyncGenerator[None]: """Check that translation requirements are met. Current checks: @@ -682,8 +704,11 @@ def check_translations(ignore_translations: str | list[str]) -> Generator[None]: _ignore_translations = {k: "unused" for k in ignore_translations} + translation_tasks = set[asyncio.Task[None]]() + # Keep reference to original functions _original_flow_manager_async_handle_step = FlowManager._async_handle_step + _original_issue_registry_async_create_issue = ir.IssueRegistry.async_get_or_create # Prepare override functions async def _flow_manager_async_handle_step( @@ -695,13 +720,34 @@ def check_translations(ignore_translations: str | list[str]) -> Generator[None]: ) return result + def _issue_registry_async_create_issue( + self: ir.IssueRegistry, domain: str, issue_id: str, *args, **kwargs + ) -> None: + result = _original_issue_registry_async_create_issue( + self, domain, issue_id, *args, **kwargs + ) + self.hass.async_create_task_internal( + _check_create_issue_translations(self, result, _ignore_translations), + "Check create_issue translations", + eager_start=True, + ) + return result + # Use override functions - with patch( - "homeassistant.data_entry_flow.FlowManager._async_handle_step", - _flow_manager_async_handle_step, + with ( + patch( + "homeassistant.data_entry_flow.FlowManager._async_handle_step", + _flow_manager_async_handle_step, + ), + patch( + "homeassistant.helpers.issue_registry.IssueRegistry.async_get_or_create", + _issue_registry_async_create_issue, + ), ): yield + await asyncio.gather(*translation_tasks) + # Run final checks unused_ignore = [k for k, v in _ignore_translations.items() if v == "unused"] if unused_ignore: From cecf5d02aed1c437e20fd84eed65fb4549bbd69c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 14 Nov 2024 08:00:17 +0000 Subject: [PATCH 2/8] description is only required on non-fixable flows --- tests/components/conftest.py | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/tests/components/conftest.py b/tests/components/conftest.py index c3224fb5317..af594af5795 100644 --- a/tests/components/conftest.py +++ b/tests/components/conftest.py @@ -679,15 +679,24 @@ async def _check_create_issue_translations( ) -> None: if result.translation_key is None: return - for header in ("title", "description"): - await _validate_translation( - issue_registry.hass, - ignore_translations, - "issues", - result.domain, - f"{result.translation_key}.{header}", - result.translation_placeholders, - ) + await _validate_translation( + issue_registry.hass, + ignore_translations, + "issues", + result.domain, + f"{result.translation_key}.title", + result.translation_placeholders, + ) + if result.is_fixable: + return + await _validate_translation( + issue_registry.hass, + ignore_translations, + "issues", + result.domain, + f"{result.translation_key}.description", + result.translation_placeholders, + ) @pytest.fixture(autouse=True) From 1539f19a10b4c99dc5250899c2e6f3760a3db71e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 14 Nov 2024 08:02:19 +0000 Subject: [PATCH 3/8] Fix reference to task --- tests/components/conftest.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/components/conftest.py b/tests/components/conftest.py index af594af5795..b9e5dc35ead 100644 --- a/tests/components/conftest.py +++ b/tests/components/conftest.py @@ -735,10 +735,12 @@ async def check_translations( result = _original_issue_registry_async_create_issue( self, domain, issue_id, *args, **kwargs ) - self.hass.async_create_task_internal( - _check_create_issue_translations(self, result, _ignore_translations), - "Check create_issue translations", - eager_start=True, + translation_tasks.add( + self.hass.async_create_task_internal( + _check_create_issue_translations(self, result, _ignore_translations), + "Check create_issue translations", + eager_start=True, + ) ) return result From 26685cd75cbb04cb99a8ed0be0fd0a0816a51357 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 14 Nov 2024 08:07:03 +0000 Subject: [PATCH 4/8] Adjust comment --- tests/components/conftest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/components/conftest.py b/tests/components/conftest.py index b9e5dc35ead..da5790ed2db 100644 --- a/tests/components/conftest.py +++ b/tests/components/conftest.py @@ -707,6 +707,7 @@ async def check_translations( Current checks: - data entry flow results (ConfigFlow/OptionsFlow) + - issue registry entries """ if not isinstance(ignore_translations, list): ignore_translations = [ignore_translations] From b79fd62a14eccc93df4a3abb9ef82e7a1f30f86a Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 14 Nov 2024 08:52:31 +0000 Subject: [PATCH 5/8] Ignore test translations --- tests/components/repairs/test_init.py | 14 +++++ .../components/repairs/test_websocket_api.py | 56 ++++++++++++++++++- tests/components/sensor/test_recorder.py | 11 ++++ tests/components/workday/test_repairs.py | 6 ++ tests/components/zwave_js/test_repairs.py | 5 ++ 5 files changed, 89 insertions(+), 3 deletions(-) diff --git a/tests/components/repairs/test_init.py b/tests/components/repairs/test_init.py index edb6e509841..7d0ac101411 100644 --- a/tests/components/repairs/test_init.py +++ b/tests/components/repairs/test_init.py @@ -21,6 +21,16 @@ from tests.common import mock_platform from tests.typing import WebSocketGenerator +@pytest.mark.parametrize( + "ignore_translations", + [ + [ + "component.test.issues.even_worse.title", + "component.test.issues.even_worse.description", + "component.test.issues.abc_123.title", + ] + ], +) @pytest.mark.freeze_time("2022-07-19 07:53:05") async def test_create_update_issue( hass: HomeAssistant, hass_ws_client: WebSocketGenerator @@ -329,6 +339,10 @@ async def test_ignore_issue( } +@pytest.mark.parametrize( + "ignore_translations", + ["component.fake_integration.issues.abc_123.title"], +) @pytest.mark.freeze_time("2022-07-19 07:53:05") async def test_delete_issue( hass: HomeAssistant, diff --git a/tests/components/repairs/test_websocket_api.py b/tests/components/repairs/test_websocket_api.py index bb3d50f9eb5..81a41300366 100644 --- a/tests/components/repairs/test_websocket_api.py +++ b/tests/components/repairs/test_websocket_api.py @@ -151,6 +151,10 @@ async def mock_repairs_integration(hass: HomeAssistant) -> None: ) +@pytest.mark.parametrize( + "ignore_translations", + ["component.fake_integration.issues.abc_123.title"], +) async def test_dismiss_issue( hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: @@ -234,6 +238,10 @@ async def test_dismiss_issue( } +@pytest.mark.parametrize( + "ignore_translations", + ["component.fake_integration.issues.abc_123.title"], +) async def test_fix_non_existing_issue( hass: HomeAssistant, hass_client: ClientSessionGenerator, @@ -281,10 +289,20 @@ async def test_fix_non_existing_issue( @pytest.mark.parametrize( - ("domain", "step", "description_placeholders"), + ("domain", "step", "description_placeholders", "ignore_translations"), [ - ("fake_integration", "custom_step", None), - ("fake_integration_default_handler", "confirm", {"abc": "123"}), + ( + "fake_integration", + "custom_step", + None, + ["component.fake_integration.issues.abc_123.title"], + ), + ( + "fake_integration_default_handler", + "confirm", + {"abc": "123"}, + ["component.fake_integration_default_handler.issues.abc_123.title"], + ), ], ) async def test_fix_issue( @@ -380,6 +398,10 @@ async def test_fix_issue_unauth( assert resp.status == HTTPStatus.UNAUTHORIZED +@pytest.mark.parametrize( + "ignore_translations", + ["component.fake_integration.issues.abc_123.title"], +) async def test_get_progress_unauth( hass: HomeAssistant, hass_client: ClientSessionGenerator, @@ -411,6 +433,10 @@ async def test_get_progress_unauth( assert resp.status == HTTPStatus.UNAUTHORIZED +@pytest.mark.parametrize( + "ignore_translations", + ["component.fake_integration.issues.abc_123.title"], +) async def test_step_unauth( hass: HomeAssistant, hass_client: ClientSessionGenerator, @@ -442,6 +468,16 @@ async def test_step_unauth( assert resp.status == HTTPStatus.UNAUTHORIZED +@pytest.mark.parametrize( + "ignore_translations", + [ + [ + "component.test.issues.even_worse.title", + "component.test.issues.even_worse.description", + "component.test.issues.abc_123.title", + ] + ], +) @pytest.mark.freeze_time("2022-07-19 07:53:05") async def test_list_issues( hass: HomeAssistant, @@ -533,6 +569,10 @@ async def test_list_issues( } +@pytest.mark.parametrize( + "ignore_translations", + ["component.fake_integration.issues.abc_123.title"], +) async def test_fix_issue_aborted( hass: HomeAssistant, hass_client: ClientSessionGenerator, @@ -594,6 +634,16 @@ async def test_fix_issue_aborted( assert msg["result"]["issues"][0] == first_issue +@pytest.mark.parametrize( + "ignore_translations", + [ + [ + "component.test.issues.abc_123.title", + "component.test.issues.even_worse.title", + "component.test.issues.even_worse.description", + ] + ], +) @pytest.mark.freeze_time("2022-07-19 07:53:05") async def test_get_issue_data( hass: HomeAssistant, hass_ws_client: WebSocketGenerator diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index 0e8c2a5e188..aec6ec84f1b 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -5432,6 +5432,17 @@ async def test_exclude_attributes(hass: HomeAssistant) -> None: assert ATTR_FRIENDLY_NAME in states[0].attributes +@pytest.mark.parametrize( + "ignore_translations", + [ + [ + "component.test.issues..title", + "component.test.issues..description", + "component.sensor.issues..title", + "component.sensor.issues..description", + ] + ], +) async def test_clean_up_repairs( hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: diff --git a/tests/components/workday/test_repairs.py b/tests/components/workday/test_repairs.py index e25d4e0ca45..adbae5676e6 100644 --- a/tests/components/workday/test_repairs.py +++ b/tests/components/workday/test_repairs.py @@ -2,6 +2,8 @@ from __future__ import annotations +import pytest + from homeassistant.components.workday.const import CONF_REMOVE_HOLIDAYS, DOMAIN from homeassistant.const import CONF_COUNTRY from homeassistant.core import HomeAssistant @@ -427,6 +429,10 @@ async def test_bad_date_holiday( assert issue +@pytest.mark.parametrize( + "ignore_translations", + ["component.workday.issues.issue_1.title"], +) async def test_other_fixable_issues( hass: HomeAssistant, hass_client: ClientSessionGenerator, diff --git a/tests/components/zwave_js/test_repairs.py b/tests/components/zwave_js/test_repairs.py index 2f10b70b48a..d237a6e410a 100644 --- a/tests/components/zwave_js/test_repairs.py +++ b/tests/components/zwave_js/test_repairs.py @@ -3,6 +3,7 @@ from copy import deepcopy from unittest.mock import patch +import pytest from zwave_js_server.event import Event from zwave_js_server.model.node import Node @@ -179,6 +180,10 @@ async def test_device_config_file_changed_ignore_step( assert msg["result"]["issues"][0].get("dismissed_version") is not None +@pytest.mark.parametrize( + "ignore_translations", + ["component.zwave_js.issues.invalid_issue.title"], +) async def test_invalid_issue( hass: HomeAssistant, hass_client: ClientSessionGenerator, From f4d1eafb2006cc78327fd199e4a4f99529e3cd5e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 14 Nov 2024 15:06:24 +0000 Subject: [PATCH 6/8] Ignore repairs --- tests/components/repairs/test_init.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/components/repairs/test_init.py b/tests/components/repairs/test_init.py index 7d0ac101411..e78563503f1 100644 --- a/tests/components/repairs/test_init.py +++ b/tests/components/repairs/test_init.py @@ -170,6 +170,14 @@ async def test_create_issue_invalid_version( assert msg["result"] == {"issues": []} +@pytest.mark.parametrize( + "ignore_translations", + [ + [ + "component.test.issues.abc_123.title", + ] + ], +) @pytest.mark.freeze_time("2022-07-19 07:53:05") async def test_ignore_issue( hass: HomeAssistant, hass_ws_client: WebSocketGenerator @@ -497,6 +505,10 @@ async def test_non_compliant_platform( assert list(hass.data[DOMAIN]["platforms"].keys()) == ["fake_integration"] +@pytest.mark.parametrize( + "ignore_translations", + ["component.fake_integration.issues.abc_123.title"], +) @pytest.mark.freeze_time("2022-07-21 08:22:00") async def test_sync_methods( hass: HomeAssistant, From cd1272008507c7cb82155a8d7509c95067290774 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Thu, 14 Nov 2024 16:31:33 +0100 Subject: [PATCH 7/8] Add Python version to issue ID (#130611) --- homeassistant/bootstrap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index dcfb6685627..1034223051c 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -515,7 +515,7 @@ async def async_from_config_dict( issue_registry.async_create_issue( hass, core.DOMAIN, - "python_version", + f"python_version_{required_python_version}", is_fixable=False, severity=issue_registry.IssueSeverity.WARNING, breaks_in_ha_version=REQUIRED_NEXT_PYTHON_HA_RELEASE, From 1ce8bfdaa438949da707d94ff7b12ff7b20ce0cc Mon Sep 17 00:00:00 2001 From: Josef Zweck <24647999+zweckj@users.noreply.github.com> Date: Thu, 14 Nov 2024 16:34:17 +0100 Subject: [PATCH 8/8] Use test helpers for acaia buttons (#130626) --- .../acaia/snapshots/test_button.ambr | 60 +++++++++---------- tests/components/acaia/test_button.py | 33 ++++++---- 2 files changed, 50 insertions(+), 43 deletions(-) diff --git a/tests/components/acaia/snapshots/test_button.ambr b/tests/components/acaia/snapshots/test_button.ambr index 7e2624923af..cd91ca1a17a 100644 --- a/tests/components/acaia/snapshots/test_button.ambr +++ b/tests/components/acaia/snapshots/test_button.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_buttons[entry_button_reset_timer] +# name: test_buttons[button.lunar_ddeeff_reset_timer-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -32,7 +32,20 @@ 'unit_of_measurement': None, }) # --- -# name: test_buttons[entry_button_start_stop_timer] +# name: test_buttons[button.lunar_ddeeff_reset_timer-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'LUNAR-DDEEFF Reset timer', + }), + 'context': , + 'entity_id': 'button.lunar_ddeeff_reset_timer', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_buttons[button.lunar_ddeeff_start_stop_timer-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -65,7 +78,20 @@ 'unit_of_measurement': None, }) # --- -# name: test_buttons[entry_button_tare] +# name: test_buttons[button.lunar_ddeeff_start_stop_timer-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'LUNAR-DDEEFF Start/stop timer', + }), + 'context': , + 'entity_id': 'button.lunar_ddeeff_start_stop_timer', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_buttons[button.lunar_ddeeff_tare-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -98,33 +124,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_buttons[state_button_reset_timer] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'LUNAR-DDEEFF Reset timer', - }), - 'context': , - 'entity_id': 'button.lunar_ddeeff_reset_timer', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- -# name: test_buttons[state_button_start_stop_timer] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'LUNAR-DDEEFF Start/stop timer', - }), - 'context': , - 'entity_id': 'button.lunar_ddeeff_start_stop_timer', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- -# name: test_buttons[state_button_tare] +# name: test_buttons[button.lunar_ddeeff_tare-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'LUNAR-DDEEFF Tare', diff --git a/tests/components/acaia/test_button.py b/tests/components/acaia/test_button.py index 62eb8b61b8a..f68f85e253d 100644 --- a/tests/components/acaia/test_button.py +++ b/tests/components/acaia/test_button.py @@ -1,21 +1,24 @@ """Tests for the acaia buttons.""" from datetime import timedelta -from unittest.mock import MagicMock +from unittest.mock import MagicMock, patch from freezegun.api import FrozenDateTimeFactory -import pytest from syrupy import SnapshotAssertion from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS -from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, STATE_UNKNOWN +from homeassistant.const import ( + ATTR_ENTITY_ID, + STATE_UNAVAILABLE, + STATE_UNKNOWN, + Platform, +) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from tests.common import async_fire_time_changed - -pytestmark = pytest.mark.usefixtures("init_integration") +from . import setup_integration +from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform BUTTONS = ( "tare", @@ -28,24 +31,25 @@ async def test_buttons( hass: HomeAssistant, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion, + mock_scale: MagicMock, + mock_config_entry: MockConfigEntry, ) -> None: """Test the acaia buttons.""" - for button in BUTTONS: - state = hass.states.get(f"button.lunar_ddeeff_{button}") - assert state - assert state == snapshot(name=f"state_button_{button}") - entry = entity_registry.async_get(state.entity_id) - assert entry - assert entry == snapshot(name=f"entry_button_{button}") + with patch("homeassistant.components.acaia.PLATFORMS", [Platform.BUTTON]): + await setup_integration(hass, mock_config_entry) + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) async def test_button_presses( hass: HomeAssistant, mock_scale: MagicMock, + mock_config_entry: MockConfigEntry, ) -> None: """Test the acaia button presses.""" + await setup_integration(hass, mock_config_entry) + for button in BUTTONS: await hass.services.async_call( BUTTON_DOMAIN, @@ -63,10 +67,13 @@ async def test_button_presses( async def test_buttons_unavailable_on_disconnected_scale( hass: HomeAssistant, mock_scale: MagicMock, + mock_config_entry: MockConfigEntry, freezer: FrozenDateTimeFactory, ) -> None: """Test the acaia buttons are unavailable when the scale is disconnected.""" + await setup_integration(hass, mock_config_entry) + for button in BUTTONS: state = hass.states.get(f"button.lunar_ddeeff_{button}") assert state