From 17779c5f0c7b4d01a5746eb9f265d33773979e9f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 4 Oct 2023 13:40:33 +0200 Subject: [PATCH] Add loader.async_suggest_report_issue and loader.async_get_issue_tracker (#101336) * Add loader.async_suggest_report_issue and loader.async_get_issue_tracker * Update tests * Add tests * Address review comments * Address review comments --- homeassistant/helpers/entity.py | 38 ++------ homeassistant/loader.py | 52 +++++++++++ tests/components/sensor/test_init.py | 2 +- tests/helpers/test_entity.py | 7 +- tests/test_loader.py | 129 +++++++++++++++++++++++++++ 5 files changed, 191 insertions(+), 37 deletions(-) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 9b16b0c24fd..542841f7f7c 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -4,7 +4,6 @@ from __future__ import annotations from abc import ABC import asyncio from collections.abc import Coroutine, Iterable, Mapping, MutableMapping -from contextlib import suppress from dataclasses import dataclass from datetime import timedelta from enum import Enum, auto @@ -50,11 +49,7 @@ from homeassistant.exceptions import ( InvalidStateError, NoEntitySpecifiedError, ) -from homeassistant.loader import ( - IntegrationNotLoaded, - async_get_loaded_integration, - bind_hass, -) +from homeassistant.loader import async_suggest_report_issue, bind_hass from homeassistant.util import ensure_unique_string, slugify from . import device_registry as dr, entity_registry as er @@ -1257,35 +1252,12 @@ class Entity(ABC): def _suggest_report_issue(self) -> str: """Suggest to report an issue.""" - report_issue = "" - - integration = None # The check for self.platform guards against integrations not using an # EntityComponent and can be removed in HA Core 2024.1 - if self.platform: - with suppress(IntegrationNotLoaded): - integration = async_get_loaded_integration( - self.hass, self.platform.platform_name - ) - - if "custom_components" in type(self).__module__: - if integration and integration.issue_tracker: - report_issue = f"create a bug report at {integration.issue_tracker}" - else: - report_issue = "report it to the custom integration author" - else: - report_issue = ( - "create a bug report at " - "https://github.com/home-assistant/core/issues?q=is%3Aopen+is%3Aissue" - ) - # The check for self.platform guards against integrations not using an - # EntityComponent and can be removed in HA Core 2024.1 - if self.platform: - report_issue += ( - f"+label%3A%22integration%3A+{self.platform.platform_name}%22" - ) - - return report_issue + platform_name = self.platform.platform_name if self.platform else None + return async_suggest_report_issue( + self.hass, integration_domain=platform_name, module=type(self).__module__ + ) @dataclass(slots=True) diff --git a/homeassistant/loader.py b/homeassistant/loader.py index a3ddbf4cbca..6107150cebb 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -1187,3 +1187,55 @@ def _lookup_path(hass: HomeAssistant) -> list[str]: def is_component_module_loaded(hass: HomeAssistant, module: str) -> bool: """Test if a component module is loaded.""" return module in hass.data[DATA_COMPONENTS] + + +@callback +def async_get_issue_tracker( + hass: HomeAssistant | None, + *, + integration_domain: str | None = None, + module: str | None = None, +) -> str | None: + """Return a URL for an integration's issue tracker.""" + issue_tracker = ( + "https://github.com/home-assistant/core/issues?q=is%3Aopen+is%3Aissue" + ) + if not integration_domain and not module: + # If we know nothing about the entity, suggest opening an issue on HA core + return issue_tracker + + if hass and integration_domain: + with suppress(IntegrationNotLoaded): + integration = async_get_loaded_integration(hass, integration_domain) + if not integration.is_built_in: + return integration.issue_tracker + + if module and "custom_components" in module: + return None + + if integration_domain: + issue_tracker += f"+label%3A%22integration%3A+{integration_domain}%22" + return issue_tracker + + +@callback +def async_suggest_report_issue( + hass: HomeAssistant | None, + *, + integration_domain: str | None = None, + module: str | None = None, +) -> str: + """Generate a blurb asking the user to file a bug report.""" + issue_tracker = async_get_issue_tracker( + hass, integration_domain=integration_domain, module=module + ) + + if not issue_tracker: + if not integration_domain: + return "report it to the custom integration author" + return ( + f"report it to the author of the '{integration_domain}' " + "custom integration" + ) + + return f"create a bug report at {issue_tracker}" diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py index 01dfb9b3649..395f6d41a14 100644 --- a/tests/components/sensor/test_init.py +++ b/tests/components/sensor/test_init.py @@ -166,7 +166,7 @@ async def test_deprecated_last_reset( f"with state_class {state_class} has set last_reset. Setting last_reset for " "entities with state_class other than 'total' is not supported. Please update " "your configuration if state_class is manually configured, otherwise report it " - "to the custom integration author" + "to the author of the 'test' custom integration" ) in caplog.text state = hass.states.get("sensor.test") diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index 61ee38a66a7..68a09310540 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -776,9 +776,10 @@ async def test_warn_slow_write_state_custom_component( mock_entity.async_write_ha_state() assert ( - "Updating state for comp_test.test_entity " - "(.CustomComponentEntity'>) " - "took 10.000 seconds. Please report it to the custom integration author" + "Updating state for comp_test.test_entity (.CustomComponentEntity'>)" + " took 10.000 seconds. Please report it to the author of the 'hue' custom " + "integration" ) in caplog.text diff --git a/tests/test_loader.py b/tests/test_loader.py index 4a03a7379b0..3c95111db3a 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -744,3 +744,132 @@ async def test_loggers(hass: HomeAssistant) -> None: }, ) assert integration.loggers == ["name1", "name2"] + + +CORE_ISSUE_TRACKER = ( + "https://github.com/home-assistant/core/issues?q=is%3Aopen+is%3Aissue" +) +CORE_ISSUE_TRACKER_BUILT_IN = ( + CORE_ISSUE_TRACKER + "+label%3A%22integration%3A+bla_built_in%22" +) +CORE_ISSUE_TRACKER_CUSTOM = ( + CORE_ISSUE_TRACKER + "+label%3A%22integration%3A+bla_custom%22" +) +CORE_ISSUE_TRACKER_CUSTOM_NO_TRACKER = ( + CORE_ISSUE_TRACKER + "+label%3A%22integration%3A+bla_custom_no_tracker%22" +) +CORE_ISSUE_TRACKER_HUE = CORE_ISSUE_TRACKER + "+label%3A%22integration%3A+hue%22" +CUSTOM_ISSUE_TRACKER = "https://blablabla.com" + + +@pytest.mark.parametrize( + ("domain", "module", "issue_tracker"), + [ + # If no information is available, open issue on core + (None, None, CORE_ISSUE_TRACKER), + ("hue", "homeassistant.components.hue.sensor", CORE_ISSUE_TRACKER_HUE), + ("hue", None, CORE_ISSUE_TRACKER_HUE), + ("bla_built_in", None, CORE_ISSUE_TRACKER_BUILT_IN), + # Integration domain is not currently deduced from module + (None, "homeassistant.components.hue.sensor", CORE_ISSUE_TRACKER), + ("hue", "homeassistant.components.mqtt.sensor", CORE_ISSUE_TRACKER_HUE), + # Custom integration with known issue tracker + ("bla_custom", "custom_components.bla_custom.sensor", CUSTOM_ISSUE_TRACKER), + ("bla_custom", None, CUSTOM_ISSUE_TRACKER), + # Custom integration without known issue tracker + (None, "custom_components.bla.sensor", None), + ("bla_custom_no_tracker", "custom_components.bla_custom.sensor", None), + ("bla_custom_no_tracker", None, None), + ("hue", "custom_components.bla.sensor", None), + # Integration domain has priority over module + ("bla_custom_no_tracker", "homeassistant.components.bla_custom.sensor", None), + ], +) +async def test_async_get_issue_tracker( + hass, domain: str | None, module: str | None, issue_tracker: str | None +) -> None: + """Test async_get_issue_tracker.""" + mock_integration(hass, MockModule("bla_built_in")) + mock_integration( + hass, + MockModule( + "bla_custom", partial_manifest={"issue_tracker": CUSTOM_ISSUE_TRACKER} + ), + built_in=False, + ) + mock_integration(hass, MockModule("bla_custom_no_tracker"), built_in=False) + assert ( + loader.async_get_issue_tracker(hass, integration_domain=domain, module=module) + == issue_tracker + ) + + +@pytest.mark.parametrize( + ("domain", "module", "issue_tracker"), + [ + # If no information is available, open issue on core + (None, None, CORE_ISSUE_TRACKER), + ("hue", "homeassistant.components.hue.sensor", CORE_ISSUE_TRACKER_HUE), + ("hue", None, CORE_ISSUE_TRACKER_HUE), + ("bla_built_in", None, CORE_ISSUE_TRACKER_BUILT_IN), + # Integration domain is not currently deduced from module + (None, "homeassistant.components.hue.sensor", CORE_ISSUE_TRACKER), + ("hue", "homeassistant.components.mqtt.sensor", CORE_ISSUE_TRACKER_HUE), + # Custom integration with known issue tracker - can't find it without hass + ("bla_custom", "custom_components.bla_custom.sensor", None), + # Assumed to be a core integration without hass and without module + ("bla_custom", None, CORE_ISSUE_TRACKER_CUSTOM), + ], +) +async def test_async_get_issue_tracker_no_hass( + hass, domain: str | None, module: str | None, issue_tracker: str +) -> None: + """Test async_get_issue_tracker.""" + mock_integration(hass, MockModule("bla_built_in")) + mock_integration( + hass, + MockModule( + "bla_custom", partial_manifest={"issue_tracker": CUSTOM_ISSUE_TRACKER} + ), + built_in=False, + ) + assert ( + loader.async_get_issue_tracker(None, integration_domain=domain, module=module) + == issue_tracker + ) + + +REPORT_CUSTOM = ( + "report it to the author of the 'bla_custom_no_tracker' custom integration" +) +REPORT_CUSTOM_UNKNOWN = "report it to the custom integration author" + + +@pytest.mark.parametrize( + ("domain", "module", "report_issue"), + [ + (None, None, f"create a bug report at {CORE_ISSUE_TRACKER}"), + ("bla_custom", None, f"create a bug report at {CUSTOM_ISSUE_TRACKER}"), + ("bla_custom_no_tracker", None, REPORT_CUSTOM), + (None, "custom_components.hue.sensor", REPORT_CUSTOM_UNKNOWN), + ], +) +async def test_async_suggest_report_issue( + hass, domain: str | None, module: str | None, report_issue: str +) -> None: + """Test async_suggest_report_issue.""" + mock_integration(hass, MockModule("bla_built_in")) + mock_integration( + hass, + MockModule( + "bla_custom", partial_manifest={"issue_tracker": CUSTOM_ISSUE_TRACKER} + ), + built_in=False, + ) + mock_integration(hass, MockModule("bla_custom_no_tracker"), built_in=False) + assert ( + loader.async_suggest_report_issue( + hass, integration_domain=domain, module=module + ) + == report_issue + )