home-assistant-core/tests/test_loader.py

1962 lines
70 KiB
Python

"""Test to verify that we can load components."""
import asyncio
import os
import sys
import threading
from typing import Any
from unittest.mock import MagicMock, Mock, patch
from awesomeversion import AwesomeVersion
import pytest
from homeassistant import loader
from homeassistant.components import http, hue
from homeassistant.components.hue import light as hue_light
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import frame
from .common import MockModule, async_get_persistent_notifications, mock_integration
async def test_circular_component_dependencies(hass: HomeAssistant) -> None:
"""Test if we can detect circular dependencies of components."""
mock_integration(hass, MockModule("mod1"))
mock_integration(hass, MockModule("mod2", ["mod1"]))
mock_integration(hass, MockModule("mod3", ["mod1"]))
mod_4 = mock_integration(hass, MockModule("mod4", ["mod2", "mod3"]))
deps = await loader._async_component_dependencies(hass, mod_4)
assert deps == {"mod1", "mod2", "mod3", "mod4"}
# Create a circular dependency
mock_integration(hass, MockModule("mod1", ["mod4"]))
with pytest.raises(loader.CircularDependency):
await loader._async_component_dependencies(hass, mod_4)
# Create a different circular dependency
mock_integration(hass, MockModule("mod1", ["mod3"]))
with pytest.raises(loader.CircularDependency):
await loader._async_component_dependencies(hass, mod_4)
# Create a circular after_dependency
mock_integration(
hass, MockModule("mod1", partial_manifest={"after_dependencies": ["mod4"]})
)
with pytest.raises(loader.CircularDependency):
await loader._async_component_dependencies(hass, mod_4)
# Create a different circular after_dependency
mock_integration(
hass, MockModule("mod1", partial_manifest={"after_dependencies": ["mod3"]})
)
with pytest.raises(loader.CircularDependency):
await loader._async_component_dependencies(hass, mod_4)
async def test_nonexistent_component_dependencies(hass: HomeAssistant) -> None:
"""Test if we can detect nonexistent dependencies of components."""
mod_1 = mock_integration(hass, MockModule("mod1", ["nonexistent"]))
with pytest.raises(loader.IntegrationNotFound):
await loader._async_component_dependencies(hass, mod_1)
def test_component_loader(hass: HomeAssistant) -> None:
"""Test loading components."""
components = loader.Components(hass)
assert components.http.CONFIG_SCHEMA is http.CONFIG_SCHEMA
assert hass.components.http.CONFIG_SCHEMA is http.CONFIG_SCHEMA
def test_component_loader_non_existing(hass: HomeAssistant) -> None:
"""Test loading components."""
components = loader.Components(hass)
with pytest.raises(ImportError):
_ = components.non_existing
async def test_component_wrapper(hass: HomeAssistant) -> None:
"""Test component wrapper."""
components = loader.Components(hass)
components.persistent_notification.async_create("message")
notifications = async_get_persistent_notifications(hass)
assert len(notifications)
async def test_helpers_wrapper(hass: HomeAssistant) -> None:
"""Test helpers wrapper."""
helpers = loader.Helpers(hass)
result = []
@callback
def discovery_callback(service, discovered):
"""Handle discovery callback."""
result.append(discovered)
helpers.discovery.async_listen("service_name", discovery_callback)
await helpers.discovery.async_discover("service_name", "hello", None, {})
await hass.async_block_till_done()
assert result == ["hello"]
async def test_custom_component_name(
hass: HomeAssistant, enable_custom_integrations: None
) -> None:
"""Test the name attribute of custom components."""
with pytest.raises(loader.IntegrationNotFound):
await loader.async_get_integration(hass, "test_standalone")
integration = await loader.async_get_integration(hass, "test_package")
int_comp = integration.get_component()
assert int_comp.__name__ == "custom_components.test_package"
assert int_comp.__package__ == "custom_components.test_package"
comp = hass.components.test_package
assert comp.__name__ == "custom_components.test_package"
assert comp.__package__ == "custom_components.test_package"
integration = await loader.async_get_integration(hass, "test")
platform = integration.get_platform("light")
assert integration.get_platform_cached("light") is platform
assert platform.__name__ == "custom_components.test.light"
assert platform.__package__ == "custom_components.test"
# Test custom components is mounted
from custom_components.test_package import TEST
assert TEST == 5
async def test_log_warning_custom_component(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
enable_custom_integrations: None,
) -> None:
"""Test that we log a warning when loading a custom component."""
await loader.async_get_integration(hass, "test_package")
assert "We found a custom integration test_package" in caplog.text
await loader.async_get_integration(hass, "test")
assert "We found a custom integration test " in caplog.text
async def test_custom_integration_version_not_valid(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
enable_custom_integrations: None,
) -> None:
"""Test that we log a warning when custom integrations have a invalid version."""
with pytest.raises(loader.IntegrationNotFound):
await loader.async_get_integration(hass, "test_no_version")
assert (
"The custom integration 'test_no_version' does not have a version key in the"
" manifest file and was blocked from loading."
) in caplog.text
with pytest.raises(loader.IntegrationNotFound):
await loader.async_get_integration(hass, "test2")
assert (
"The custom integration 'test_bad_version' does not have a valid version key"
" (bad) in the manifest file and was blocked from loading."
) in caplog.text
@pytest.mark.parametrize(
"blocked_versions",
[
loader.BlockedIntegration(None, "breaks Home Assistant"),
loader.BlockedIntegration(AwesomeVersion("2.0.0"), "breaks Home Assistant"),
],
)
async def test_custom_integration_version_blocked(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
enable_custom_integrations: None,
blocked_versions,
) -> None:
"""Test that we log a warning when custom integrations have a blocked version."""
with patch.dict(
loader.BLOCKED_CUSTOM_INTEGRATIONS, {"test_blocked_version": blocked_versions}
):
with pytest.raises(loader.IntegrationNotFound):
await loader.async_get_integration(hass, "test_blocked_version")
assert (
"Version 1.0.0 of custom integration 'test_blocked_version' breaks"
" Home Assistant and was blocked from loading, please report it to the"
" author of the 'test_blocked_version' custom integration"
) in caplog.text
@pytest.mark.parametrize(
"blocked_versions",
[
loader.BlockedIntegration(AwesomeVersion("0.9.9"), "breaks Home Assistant"),
loader.BlockedIntegration(AwesomeVersion("1.0.0"), "breaks Home Assistant"),
],
)
async def test_custom_integration_version_not_blocked(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
enable_custom_integrations: None,
blocked_versions,
) -> None:
"""Test that we log a warning when custom integrations have a blocked version."""
with patch.dict(
loader.BLOCKED_CUSTOM_INTEGRATIONS, {"test_blocked_version": blocked_versions}
):
await loader.async_get_integration(hass, "test_blocked_version")
assert (
"Version 1.0.0 of custom integration 'test_blocked_version'"
) not in caplog.text
async def test_get_integration(hass: HomeAssistant) -> None:
"""Test resolving integration."""
with pytest.raises(loader.IntegrationNotLoaded):
loader.async_get_loaded_integration(hass, "hue")
integration = await loader.async_get_integration(hass, "hue")
assert hue == integration.get_component()
assert hue_light == integration.get_platform("light")
integration = loader.async_get_loaded_integration(hass, "hue")
assert hue == integration.get_component()
assert hue_light == integration.get_platform("light")
async def test_async_get_component(hass: HomeAssistant) -> None:
"""Test resolving integration."""
with pytest.raises(loader.IntegrationNotLoaded):
loader.async_get_loaded_integration(hass, "hue")
integration = await loader.async_get_integration(hass, "hue")
assert await integration.async_get_component() == hue
assert integration.get_platform("light") == hue_light
integration = loader.async_get_loaded_integration(hass, "hue")
assert await integration.async_get_component() == hue
assert integration.get_platform("light") == hue_light
async def test_get_integration_exceptions(hass: HomeAssistant) -> None:
"""Test resolving integration."""
integration = await loader.async_get_integration(hass, "hue")
with (
pytest.raises(ImportError),
patch(
"homeassistant.loader.importlib.import_module",
side_effect=ValueError("Boom"),
),
):
assert hue == integration.get_component()
with (
pytest.raises(ImportError),
patch(
"homeassistant.loader.importlib.import_module",
side_effect=ValueError("Boom"),
),
):
assert hue_light == integration.get_platform("light")
async def test_get_platform_caches_failures_when_component_loaded(
hass: HomeAssistant,
) -> None:
"""Test get_platform caches failures only when the component is loaded.
Only ModuleNotFoundError is cached, ImportError is not cached.
"""
integration = await loader.async_get_integration(hass, "hue")
with (
pytest.raises(ModuleNotFoundError),
patch(
"homeassistant.loader.importlib.import_module",
side_effect=ModuleNotFoundError("Boom"),
),
):
assert integration.get_component() == hue
with (
pytest.raises(ModuleNotFoundError),
patch(
"homeassistant.loader.importlib.import_module",
side_effect=ModuleNotFoundError("Boom"),
),
):
assert integration.get_platform("light") == hue_light
# Hue is not loaded so we should still hit the import_module path
with (
pytest.raises(ModuleNotFoundError),
patch(
"homeassistant.loader.importlib.import_module",
side_effect=ModuleNotFoundError("Boom"),
),
):
assert integration.get_platform("light") == hue_light
assert integration.get_component() == hue
# Hue is loaded so we should cache the import_module failure now
with (
pytest.raises(ModuleNotFoundError),
patch(
"homeassistant.loader.importlib.import_module",
side_effect=ModuleNotFoundError("Boom"),
),
):
assert integration.get_platform("light") == hue_light
# Hue is loaded and the last call should have cached the import_module failure
with pytest.raises(ModuleNotFoundError):
assert integration.get_platform("light") == hue_light
async def test_get_platform_only_cached_module_not_found_when_component_loaded(
hass: HomeAssistant,
) -> None:
"""Test get_platform cache only cache module not found when the component is loaded."""
integration = await loader.async_get_integration(hass, "hue")
with (
pytest.raises(ImportError),
patch(
"homeassistant.loader.importlib.import_module",
side_effect=ImportError("Boom"),
),
):
assert integration.get_component() == hue
with (
pytest.raises(ImportError),
patch(
"homeassistant.loader.importlib.import_module",
side_effect=ImportError("Boom"),
),
):
assert integration.get_platform("light") == hue_light
# Hue is not loaded so we should still hit the import_module path
with (
pytest.raises(ImportError),
patch(
"homeassistant.loader.importlib.import_module",
side_effect=ImportError("Boom"),
),
):
assert integration.get_platform("light") == hue_light
assert integration.get_component() == hue
# Hue is loaded so we should cache the import_module failure now
with (
pytest.raises(ImportError),
patch(
"homeassistant.loader.importlib.import_module",
side_effect=ImportError("Boom"),
),
):
assert integration.get_platform("light") == hue_light
# ImportError is not cached because we only cache ModuleNotFoundError
assert integration.get_platform("light") == hue_light
async def test_async_get_platform_caches_failures_when_component_loaded(
hass: HomeAssistant,
) -> None:
"""Test async_get_platform caches failures only when the component is loaded.
Only ModuleNotFoundError is cached, ImportError is not cached.
"""
integration = await loader.async_get_integration(hass, "hue")
with (
pytest.raises(ModuleNotFoundError),
patch(
"homeassistant.loader.importlib.import_module",
side_effect=ModuleNotFoundError("Boom"),
),
):
assert integration.get_component() == hue
with (
pytest.raises(ModuleNotFoundError),
patch(
"homeassistant.loader.importlib.import_module",
side_effect=ModuleNotFoundError("Boom"),
),
):
assert await integration.async_get_platform("light") == hue_light
# Hue is not loaded so we should still hit the import_module path
with (
pytest.raises(ModuleNotFoundError),
patch(
"homeassistant.loader.importlib.import_module",
side_effect=ModuleNotFoundError("Boom"),
),
):
assert await integration.async_get_platform("light") == hue_light
assert integration.get_component() == hue
# Hue is loaded so we should cache the import_module failure now
with (
pytest.raises(ModuleNotFoundError),
patch(
"homeassistant.loader.importlib.import_module",
side_effect=ModuleNotFoundError("Boom"),
),
):
assert await integration.async_get_platform("light") == hue_light
# Hue is loaded and the last call should have cached the import_module failure
with pytest.raises(ModuleNotFoundError):
assert await integration.async_get_platform("light") == hue_light
# The cache should never be filled because the import error is remembered
assert integration.get_platform_cached("light") is None
async def test_async_get_platforms_caches_failures_when_component_loaded(
hass: HomeAssistant,
) -> None:
"""Test async_get_platforms cache failures only when the component is loaded.
Only ModuleNotFoundError is cached, ImportError is not cached.
"""
integration = await loader.async_get_integration(hass, "hue")
with (
pytest.raises(ModuleNotFoundError),
patch(
"homeassistant.loader.importlib.import_module",
side_effect=ModuleNotFoundError("Boom"),
),
):
assert integration.get_component() == hue
with (
pytest.raises(ModuleNotFoundError),
patch(
"homeassistant.loader.importlib.import_module",
side_effect=ModuleNotFoundError("Boom"),
),
):
assert await integration.async_get_platforms(["light"]) == {"light": hue_light}
# Hue is not loaded so we should still hit the import_module path
with (
pytest.raises(ModuleNotFoundError),
patch(
"homeassistant.loader.importlib.import_module",
side_effect=ModuleNotFoundError("Boom"),
),
):
assert await integration.async_get_platforms(["light"]) == {"light": hue_light}
assert integration.get_component() == hue
# Hue is loaded so we should cache the import_module failure now
with (
pytest.raises(ModuleNotFoundError),
patch(
"homeassistant.loader.importlib.import_module",
side_effect=ModuleNotFoundError("Boom"),
),
):
assert await integration.async_get_platforms(["light"]) == {"light": hue_light}
# Hue is loaded and the last call should have cached the import_module failure
with pytest.raises(ModuleNotFoundError):
assert await integration.async_get_platforms(["light"]) == {"light": hue_light}
# The cache should never be filled because the import error is remembered
assert integration.get_platform_cached("light") is None
async def test_get_integration_legacy(
hass: HomeAssistant, enable_custom_integrations: None
) -> None:
"""Test resolving integration."""
integration = await loader.async_get_integration(hass, "test_embedded")
assert integration.get_component().DOMAIN == "test_embedded"
assert integration.get_platform("switch") is not None
assert integration.get_platform_cached("switch") is not None
async def test_get_integration_custom_component(
hass: HomeAssistant, enable_custom_integrations: None
) -> None:
"""Test resolving integration."""
integration = await loader.async_get_integration(hass, "test_package")
assert integration.get_component().DOMAIN == "test_package"
assert integration.name == "Test Package"
def test_integration_properties(hass: HomeAssistant) -> None:
"""Test integration properties."""
integration = loader.Integration(
hass,
"homeassistant.components.hue",
None,
{
"name": "Philips Hue",
"domain": "hue",
"dependencies": ["test-dep"],
"requirements": ["test-req==1.0.0"],
"zeroconf": ["_hue._tcp.local."],
"homekit": {"models": ["BSB002"]},
"dhcp": [
{"hostname": "tesla_*", "macaddress": "4CFCAA*"},
{"hostname": "tesla_*", "macaddress": "044EAF*"},
{"hostname": "tesla_*", "macaddress": "98ED5C*"},
{"registered_devices": True},
],
"bluetooth": [{"manufacturer_id": 76, "manufacturer_data_start": [0x06]}],
"usb": [
{"vid": "10C4", "pid": "EA60"},
{"vid": "1CF1", "pid": "0030"},
{"vid": "1A86", "pid": "7523"},
{"vid": "10C4", "pid": "8A2A"},
],
"ssdp": [
{
"manufacturer": "Royal Philips Electronics",
"modelName": "Philips hue bridge 2012",
},
{
"manufacturer": "Royal Philips Electronics",
"modelName": "Philips hue bridge 2015",
},
{"manufacturer": "Signify", "modelName": "Philips hue bridge 2015"},
],
"mqtt": ["hue/discovery"],
"version": "1.0.0",
},
)
assert integration.name == "Philips Hue"
assert integration.domain == "hue"
assert integration.homekit == {"models": ["BSB002"]}
assert integration.zeroconf == ["_hue._tcp.local."]
assert integration.dhcp == [
{"hostname": "tesla_*", "macaddress": "4CFCAA*"},
{"hostname": "tesla_*", "macaddress": "044EAF*"},
{"hostname": "tesla_*", "macaddress": "98ED5C*"},
{"registered_devices": True},
]
assert integration.usb == [
{"vid": "10C4", "pid": "EA60"},
{"vid": "1CF1", "pid": "0030"},
{"vid": "1A86", "pid": "7523"},
{"vid": "10C4", "pid": "8A2A"},
]
assert integration.bluetooth == [
{"manufacturer_id": 76, "manufacturer_data_start": [0x06]}
]
assert integration.ssdp == [
{
"manufacturer": "Royal Philips Electronics",
"modelName": "Philips hue bridge 2012",
},
{
"manufacturer": "Royal Philips Electronics",
"modelName": "Philips hue bridge 2015",
},
{"manufacturer": "Signify", "modelName": "Philips hue bridge 2015"},
]
assert integration.mqtt == ["hue/discovery"]
assert integration.dependencies == ["test-dep"]
assert integration.requirements == ["test-req==1.0.0"]
assert integration.is_built_in is True
assert integration.version == "1.0.0"
integration = loader.Integration(
hass,
"custom_components.hue",
None,
{
"name": "Philips Hue",
"domain": "hue",
"dependencies": ["test-dep"],
"requirements": ["test-req==1.0.0"],
},
)
assert integration.is_built_in is False
assert integration.homekit is None
assert integration.zeroconf is None
assert integration.dhcp is None
assert integration.bluetooth is None
assert integration.usb is None
assert integration.ssdp is None
assert integration.mqtt is None
assert integration.version is None
integration = loader.Integration(
hass,
"custom_components.hue",
None,
{
"name": "Philips Hue",
"domain": "hue",
"dependencies": ["test-dep"],
"zeroconf": [{"type": "_hue._tcp.local.", "name": "hue*"}],
"requirements": ["test-req==1.0.0"],
},
)
assert integration.is_built_in is False
assert integration.homekit is None
assert integration.zeroconf == [{"type": "_hue._tcp.local.", "name": "hue*"}]
assert integration.dhcp is None
assert integration.usb is None
assert integration.bluetooth is None
assert integration.ssdp is None
async def test_integrations_only_once(hass: HomeAssistant) -> None:
"""Test that we load integrations only once."""
int_1 = hass.async_create_task(loader.async_get_integration(hass, "hue"))
int_2 = hass.async_create_task(loader.async_get_integration(hass, "hue"))
assert await int_1 is await int_2
def _get_test_integration(
hass: HomeAssistant, name: str, config_flow: bool, import_executor: bool = False
) -> loader.Integration:
"""Return a generated test integration."""
return loader.Integration(
hass,
f"homeassistant.components.{name}",
None,
{
"name": name,
"domain": name,
"config_flow": config_flow,
"dependencies": [],
"requirements": [],
"zeroconf": [f"_{name}._tcp.local."],
"homekit": {"models": [name]},
"ssdp": [{"manufacturer": name, "modelName": name}],
"mqtt": [f"{name}/discovery"],
"import_executor": import_executor,
},
)
def _get_test_integration_with_application_credentials(hass, name):
"""Return a generated test integration with application_credentials support."""
return loader.Integration(
hass,
f"homeassistant.components.{name}",
None,
{
"name": name,
"domain": name,
"config_flow": True,
"dependencies": ["application_credentials"],
"requirements": [],
"zeroconf": [f"_{name}._tcp.local."],
"homekit": {"models": [name]},
"ssdp": [{"manufacturer": name, "modelName": name}],
"mqtt": [f"{name}/discovery"],
},
)
def _get_test_integration_with_zeroconf_matcher(hass, name, config_flow):
"""Return a generated test integration with a zeroconf matcher."""
return loader.Integration(
hass,
f"homeassistant.components.{name}",
None,
{
"name": name,
"domain": name,
"config_flow": config_flow,
"dependencies": [],
"requirements": [],
"zeroconf": [{"type": f"_{name}._tcp.local.", "name": f"{name}*"}],
"homekit": {"models": [name]},
"ssdp": [{"manufacturer": name, "modelName": name}],
},
)
def _get_test_integration_with_legacy_zeroconf_matcher(hass, name, config_flow):
"""Return a generated test integration with a legacy zeroconf matcher."""
return loader.Integration(
hass,
f"homeassistant.components.{name}",
None,
{
"name": name,
"domain": name,
"config_flow": config_flow,
"dependencies": [],
"requirements": [],
"zeroconf": [
{
"type": f"_{name}._tcp.local.",
"macaddress": "AABBCC*",
"manufacturer": "legacy*",
"model": "legacy*",
"name": f"{name}*",
}
],
"homekit": {"models": [name]},
"ssdp": [{"manufacturer": name, "modelName": name}],
},
)
def _get_test_integration_with_dhcp_matcher(hass, name, config_flow):
"""Return a generated test integration with a dhcp matcher."""
return loader.Integration(
hass,
f"homeassistant.components.{name}",
None,
{
"name": name,
"domain": name,
"config_flow": config_flow,
"dependencies": [],
"requirements": [],
"zeroconf": [],
"dhcp": [
{"hostname": "tesla_*", "macaddress": "4CFCAA*"},
{"hostname": "tesla_*", "macaddress": "044EAF*"},
{"hostname": "tesla_*", "macaddress": "98ED5C*"},
],
"homekit": {"models": [name]},
"ssdp": [{"manufacturer": name, "modelName": name}],
},
)
def _get_test_integration_with_bluetooth_matcher(hass, name, config_flow):
"""Return a generated test integration with a bluetooth matcher."""
return loader.Integration(
hass,
f"homeassistant.components.{name}",
None,
{
"name": name,
"domain": name,
"config_flow": config_flow,
"bluetooth": [
{
"local_name": "Prodigio_*",
},
],
},
)
def _get_test_integration_with_usb_matcher(hass, name, config_flow):
"""Return a generated test integration with a usb matcher."""
return loader.Integration(
hass,
f"homeassistant.components.{name}",
None,
{
"name": name,
"domain": name,
"config_flow": config_flow,
"dependencies": [],
"requirements": [],
"usb": [
{
"vid": "10C4",
"pid": "EA60",
"known_devices": ["slae.sh cc2652rb stick"],
},
{"vid": "1CF1", "pid": "0030", "known_devices": ["Conbee II"]},
{
"vid": "1A86",
"pid": "7523",
"known_devices": ["Electrolama zig-a-zig-ah"],
},
{"vid": "10C4", "pid": "8A2A", "known_devices": ["Nortek HUSBZB-1"]},
],
},
)
async def test_get_custom_components(
hass: HomeAssistant, enable_custom_integrations: None
) -> None:
"""Verify that custom components are cached."""
test_1_integration = _get_test_integration(hass, "test_1", False)
test_2_integration = _get_test_integration(hass, "test_2", True)
name = "homeassistant.loader._async_get_custom_components"
with patch(name) as mock_get:
mock_get.return_value = {
"test_1": test_1_integration,
"test_2": test_2_integration,
}
integrations = await loader.async_get_custom_components(hass)
assert integrations == mock_get.return_value
integrations = await loader.async_get_custom_components(hass)
assert integrations == mock_get.return_value
mock_get.assert_called_once_with(hass)
async def test_get_config_flows(hass: HomeAssistant) -> None:
"""Verify that custom components with config_flow are available."""
test_1_integration = _get_test_integration(hass, "test_1", False)
test_2_integration = _get_test_integration(hass, "test_2", True)
with patch("homeassistant.loader.async_get_custom_components") as mock_get:
mock_get.return_value = {
"test_1": test_1_integration,
"test_2": test_2_integration,
}
flows = await loader.async_get_config_flows(hass)
assert "test_2" in flows
assert "test_1" not in flows
async def test_get_zeroconf(hass: HomeAssistant) -> None:
"""Verify that custom components with zeroconf are found."""
test_1_integration = _get_test_integration(hass, "test_1", True)
test_2_integration = _get_test_integration_with_zeroconf_matcher(
hass, "test_2", True
)
with patch("homeassistant.loader.async_get_custom_components") as mock_get:
mock_get.return_value = {
"test_1": test_1_integration,
"test_2": test_2_integration,
}
zeroconf = await loader.async_get_zeroconf(hass)
assert zeroconf["_test_1._tcp.local."] == [{"domain": "test_1"}]
assert zeroconf["_test_2._tcp.local."] == [
{"domain": "test_2", "name": "test_2*"}
]
async def test_get_application_credentials(hass: HomeAssistant) -> None:
"""Verify that custom components with application_credentials are found."""
test_1_integration = _get_test_integration(hass, "test_1", True)
test_2_integration = _get_test_integration_with_application_credentials(
hass, "test_2"
)
with patch("homeassistant.loader.async_get_custom_components") as mock_get:
mock_get.return_value = {
"test_1": test_1_integration,
"test_2": test_2_integration,
}
application_credentials = await loader.async_get_application_credentials(hass)
assert "test_2" in application_credentials
assert "test_1" not in application_credentials
async def test_get_zeroconf_back_compat(hass: HomeAssistant) -> None:
"""Verify that custom components with zeroconf are found and legacy matchers are converted."""
test_1_integration = _get_test_integration(hass, "test_1", True)
test_2_integration = _get_test_integration_with_legacy_zeroconf_matcher(
hass, "test_2", True
)
with patch("homeassistant.loader.async_get_custom_components") as mock_get:
mock_get.return_value = {
"test_1": test_1_integration,
"test_2": test_2_integration,
}
zeroconf = await loader.async_get_zeroconf(hass)
assert zeroconf["_test_1._tcp.local."] == [{"domain": "test_1"}]
assert zeroconf["_test_2._tcp.local."] == [
{
"domain": "test_2",
"name": "test_2*",
"properties": {
"macaddress": "aabbcc*",
"model": "legacy*",
"manufacturer": "legacy*",
},
}
]
async def test_get_bluetooth(hass: HomeAssistant) -> None:
"""Verify that custom components with bluetooth are found."""
test_1_integration = _get_test_integration_with_bluetooth_matcher(
hass, "test_1", True
)
test_2_integration = _get_test_integration_with_dhcp_matcher(hass, "test_2", True)
with patch("homeassistant.loader.async_get_custom_components") as mock_get:
mock_get.return_value = {
"test_1": test_1_integration,
"test_2": test_2_integration,
}
bluetooth = await loader.async_get_bluetooth(hass)
bluetooth_for_domain = [
entry for entry in bluetooth if entry["domain"] == "test_1"
]
assert bluetooth_for_domain == [
{"domain": "test_1", "local_name": "Prodigio_*"},
]
async def test_get_dhcp(hass: HomeAssistant) -> None:
"""Verify that custom components with dhcp are found."""
test_1_integration = _get_test_integration_with_dhcp_matcher(hass, "test_1", True)
with patch("homeassistant.loader.async_get_custom_components") as mock_get:
mock_get.return_value = {
"test_1": test_1_integration,
}
dhcp = await loader.async_get_dhcp(hass)
dhcp_for_domain = [entry for entry in dhcp if entry["domain"] == "test_1"]
assert dhcp_for_domain == [
{"domain": "test_1", "hostname": "tesla_*", "macaddress": "4CFCAA*"},
{"domain": "test_1", "hostname": "tesla_*", "macaddress": "044EAF*"},
{"domain": "test_1", "hostname": "tesla_*", "macaddress": "98ED5C*"},
]
async def test_get_usb(hass: HomeAssistant) -> None:
"""Verify that custom components with usb matchers are found."""
test_1_integration = _get_test_integration_with_usb_matcher(hass, "test_1", True)
with patch("homeassistant.loader.async_get_custom_components") as mock_get:
mock_get.return_value = {
"test_1": test_1_integration,
}
usb = await loader.async_get_usb(hass)
usb_for_domain = [entry for entry in usb if entry["domain"] == "test_1"]
assert usb_for_domain == [
{"domain": "test_1", "vid": "10C4", "pid": "EA60"},
{"domain": "test_1", "vid": "1CF1", "pid": "0030"},
{"domain": "test_1", "vid": "1A86", "pid": "7523"},
{"domain": "test_1", "vid": "10C4", "pid": "8A2A"},
]
async def test_get_homekit(hass: HomeAssistant) -> None:
"""Verify that custom components with homekit are found."""
test_1_integration = _get_test_integration(hass, "test_1", True)
test_2_integration = _get_test_integration(hass, "test_2", True)
with patch("homeassistant.loader.async_get_custom_components") as mock_get:
mock_get.return_value = {
"test_1": test_1_integration,
"test_2": test_2_integration,
}
homekit = await loader.async_get_homekit(hass)
assert homekit["test_1"] == loader.HomeKitDiscoveredIntegration("test_1", True)
assert homekit["test_2"] == loader.HomeKitDiscoveredIntegration("test_2", True)
async def test_get_ssdp(hass: HomeAssistant) -> None:
"""Verify that custom components with ssdp are found."""
test_1_integration = _get_test_integration(hass, "test_1", True)
test_2_integration = _get_test_integration(hass, "test_2", True)
with patch("homeassistant.loader.async_get_custom_components") as mock_get:
mock_get.return_value = {
"test_1": test_1_integration,
"test_2": test_2_integration,
}
ssdp = await loader.async_get_ssdp(hass)
assert ssdp["test_1"] == [{"manufacturer": "test_1", "modelName": "test_1"}]
assert ssdp["test_2"] == [{"manufacturer": "test_2", "modelName": "test_2"}]
async def test_get_mqtt(hass: HomeAssistant) -> None:
"""Verify that custom components with MQTT are found."""
test_1_integration = _get_test_integration(hass, "test_1", True)
test_2_integration = _get_test_integration(hass, "test_2", True)
with patch("homeassistant.loader.async_get_custom_components") as mock_get:
mock_get.return_value = {
"test_1": test_1_integration,
"test_2": test_2_integration,
}
mqtt = await loader.async_get_mqtt(hass)
assert mqtt["test_1"] == ["test_1/discovery"]
assert mqtt["test_2"] == ["test_2/discovery"]
async def test_import_platform_executor(
hass: HomeAssistant, enable_custom_integrations: None
) -> None:
"""Test import a platform in the executor."""
integration = await loader.async_get_integration(
hass, "test_package_loaded_executor"
)
config_flow_task_1 = asyncio.create_task(
integration.async_get_platform("config_flow")
)
config_flow_task_2 = asyncio.create_task(
integration.async_get_platform("config_flow")
)
config_flow_task_3 = asyncio.create_task(
integration.async_get_platform("config_flow")
)
config_flow_task1_result = await config_flow_task_1
config_flow_task2_result = await config_flow_task_2
config_flow_task3_result = await config_flow_task_3
assert (
config_flow_task1_result == config_flow_task2_result == config_flow_task3_result
)
assert await config_flow_task1_result._async_has_devices(hass) is True
async def test_get_custom_components_recovery_mode(hass: HomeAssistant) -> None:
"""Test that we get empty custom components in recovery mode."""
hass.config.recovery_mode = True
assert await loader.async_get_custom_components(hass) == {}
async def test_custom_integration_missing_version(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test trying to load a custom integration without a version twice does not deadlock."""
with pytest.raises(loader.IntegrationNotFound):
await loader.async_get_integration(hass, "test_no_version")
with pytest.raises(loader.IntegrationNotFound):
await loader.async_get_integration(hass, "test_no_version")
async def test_custom_integration_missing(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test trying to load a custom integration that is missing twice not deadlock."""
with patch("homeassistant.loader.async_get_custom_components") as mock_get:
mock_get.return_value = {}
with pytest.raises(loader.IntegrationNotFound):
await loader.async_get_integration(hass, "test1")
with pytest.raises(loader.IntegrationNotFound):
await loader.async_get_integration(hass, "test1")
async def test_validation(hass: HomeAssistant) -> None:
"""Test we raise if invalid domain passed in."""
with pytest.raises(ValueError):
await loader.async_get_integration(hass, "some.thing")
async def test_loggers(hass: HomeAssistant) -> None:
"""Test we can fetch the loggers from the integration."""
name = "dummy"
integration = loader.Integration(
hass,
f"homeassistant.components.{name}",
None,
{
"name": name,
"domain": name,
"config_flow": True,
"dependencies": [],
"requirements": [],
"loggers": ["name1", "name2"],
},
)
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
)
def test_import_executor_default(hass: HomeAssistant) -> None:
"""Test that import_executor defaults."""
custom_comp = mock_integration(hass, MockModule("any_random"), built_in=False)
assert custom_comp.import_executor is True
built_in_comp = mock_integration(hass, MockModule("other_random"), built_in=True)
assert built_in_comp.import_executor is True
async def test_config_folder_not_in_path(hass):
"""Test that config folder is not in path."""
# Verify that we are unable to import this file from top level
with pytest.raises(ImportError):
import check_config_not_in_path # noqa: F401
# Verify that we are able to load the file with absolute path
import tests.testing_config.check_config_not_in_path # noqa: F401
async def test_hass_components_use_reported(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture, mock_integration_frame: Mock
) -> None:
"""Test that use of hass.components is reported."""
mock_integration_frame.filename = (
"/home/paulus/homeassistant/custom_components/demo/light.py"
)
integration_frame = frame.IntegrationFrame(
custom_integration=True,
_frame=mock_integration_frame,
integration="test_integration_frame",
module="custom_components.test_integration_frame",
relative_filename="custom_components/test_integration_frame/__init__.py",
)
with (
patch(
"homeassistant.helpers.frame.get_integration_frame",
return_value=integration_frame,
),
patch(
"homeassistant.components.http.start_http_server_and_save_config",
return_value=None,
),
):
await hass.components.http.start_http_server_and_save_config(hass, [], None)
assert (
"Detected that custom integration 'test_integration_frame'"
" accesses hass.components.http. This is deprecated"
) in caplog.text
async def test_async_get_component_preloads_config_and_config_flow(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Verify async_get_component will try to preload the config and config_flow platform."""
executor_import_integration = _get_test_integration(
hass, "executor_import", True, import_executor=True
)
assert executor_import_integration.import_executor is True
assert "homeassistant.components.executor_import" not in sys.modules
assert "custom_components.executor_import" not in sys.modules
platform_exists_calls = []
def mock_platforms_exists(platforms: list[str]) -> bool:
platform_exists_calls.append(platforms)
return platforms
with (
patch("homeassistant.loader.importlib.import_module") as mock_import,
patch.object(
executor_import_integration, "platforms_exists", mock_platforms_exists
),
):
await executor_import_integration.async_get_component()
assert len(platform_exists_calls[0]) == len(loader.BASE_PRELOAD_PLATFORMS)
assert mock_import.call_count == 1 + len(loader.BASE_PRELOAD_PLATFORMS)
assert (
mock_import.call_args_list[0][0][0]
== "homeassistant.components.executor_import"
)
checked_platforms = {
mock_import.call_args_list[i][0][0]
for i in range(1, len(mock_import.call_args_list))
}
assert checked_platforms == {
"homeassistant.components.executor_import.config_flow",
*(
f"homeassistant.components.executor_import.{platform}"
for platform in loader.BASE_PRELOAD_PLATFORMS
),
}
async def test_async_get_component_loads_loop_if_already_in_sys_modules(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
enable_custom_integrations: None,
) -> None:
"""Verify async_get_component does not create an executor job if the module is already in sys.modules."""
integration = await loader.async_get_integration(
hass, "test_package_loaded_executor"
)
assert integration.pkg_path == "custom_components.test_package_loaded_executor"
assert integration.import_executor is True
assert integration.config_flow is True
assert "test_package_loaded_executor" not in hass.config.components
assert "test_package_loaded_executor.config_flow" not in hass.config.components
config_flow_module_name = f"{integration.pkg_path}.config_flow"
module_mock = MagicMock(__file__="__init__.py")
config_flow_module_mock = MagicMock(__file__="config_flow.py")
def import_module(name: str) -> Any:
if name == integration.pkg_path:
return module_mock
if name == config_flow_module_name:
return config_flow_module_mock
raise ImportError
modules_without_config_flow = {
k: v for k, v in sys.modules.items() if k != config_flow_module_name
}
with (
patch.dict(
"sys.modules",
{**modules_without_config_flow, integration.pkg_path: module_mock},
clear=True,
),
patch("homeassistant.loader.importlib.import_module", import_module),
):
module = await integration.async_get_component()
# The config flow is missing so we should load
# in the executor
assert "loaded_executor=True" in caplog.text
assert "loaded_executor=False" not in caplog.text
assert module is module_mock
caplog.clear()
with (
patch.dict(
"sys.modules",
{
integration.pkg_path: module_mock,
config_flow_module_name: config_flow_module_mock,
},
),
patch("homeassistant.loader.importlib.import_module", import_module),
):
module = await integration.async_get_component()
# Everything is already in the integration cache
# so it should not have to call the load
assert "loaded_executor" not in caplog.text
assert module is module_mock
async def test_async_get_component_concurrent_loads(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
enable_custom_integrations: None,
) -> None:
"""Verify async_get_component waits if the first load if called again when still in progress."""
integration = await loader.async_get_integration(
hass, "test_package_loaded_executor"
)
assert integration.pkg_path == "custom_components.test_package_loaded_executor"
assert integration.import_executor is True
assert integration.config_flow is True
assert "test_package_loaded_executor" not in hass.config.components
assert "test_package_loaded_executor.config_flow" not in hass.config.components
config_flow_module_name = f"{integration.pkg_path}.config_flow"
module_mock = MagicMock(__file__="__init__.py")
config_flow_module_mock = MagicMock(__file__="config_flow.py")
imports = []
start_event = threading.Event()
import_event = asyncio.Event()
def import_module(name: str) -> Any:
hass.loop.call_soon_threadsafe(import_event.set)
imports.append(name)
start_event.wait()
if name == integration.pkg_path:
return module_mock
if name == config_flow_module_name:
return config_flow_module_mock
raise ImportError
modules_without_integration = {
k: v
for k, v in sys.modules.items()
if k not in (config_flow_module_name, integration.pkg_path)
}
with (
patch.dict(
"sys.modules",
{**modules_without_integration},
clear=True,
),
patch("homeassistant.loader.importlib.import_module", import_module),
):
load_task1 = asyncio.create_task(integration.async_get_component())
load_task2 = asyncio.create_task(integration.async_get_component())
await import_event.wait() # make sure the import is started
assert not integration._component_future.done()
start_event.set()
comp1 = await load_task1
comp2 = await load_task2
assert integration._component_future is None
assert comp1 is module_mock
assert comp2 is module_mock
assert integration.pkg_path in imports
assert config_flow_module_name in imports
async def test_async_get_component_deadlock_fallback(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Verify async_get_component fallback to importing in the event loop on deadlock."""
executor_import_integration = _get_test_integration(
hass, "executor_import", True, import_executor=True
)
assert executor_import_integration.import_executor is True
module_mock = MagicMock(__file__="__init__.py")
import_attempts = 0
def mock_import(module: str, *args: Any, **kwargs: Any) -> Any:
nonlocal import_attempts
if module == "homeassistant.components.executor_import":
import_attempts += 1
if import_attempts == 1:
# _DeadlockError inherits from RuntimeError
raise RuntimeError(
"Detected deadlock trying to import homeassistant.components.executor_import"
)
return module_mock
assert "homeassistant.components.executor_import" not in sys.modules
assert "custom_components.executor_import" not in sys.modules
with patch("homeassistant.loader.importlib.import_module", mock_import):
module = await executor_import_integration.async_get_component()
assert (
"Detected deadlock trying to import homeassistant.components.executor_import"
in caplog.text
)
assert "loaded_executor=False" in caplog.text
assert module is module_mock
async def test_async_get_component_deadlock_fallback_module_not_found(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Verify async_get_component fallback behavior.
Ensure that fallback is not triggered on ModuleNotFoundError.
"""
executor_import_integration = _get_test_integration(
hass, "executor_import", True, import_executor=True
)
assert executor_import_integration.import_executor is True
module_mock = MagicMock(__file__="__init__.py")
import_attempts = 0
def mock_import(module: str, *args: Any, **kwargs: Any) -> Any:
nonlocal import_attempts
if module == "homeassistant.components.executor_import":
import_attempts += 1
if import_attempts == 1:
raise ModuleNotFoundError(
"homeassistant.components.executor_import not found",
name="homeassistant.components.executor_import",
)
return module_mock
assert "homeassistant.components.executor_import" not in sys.modules
assert "custom_components.executor_import" not in sys.modules
with (
patch("homeassistant.loader.importlib.import_module", mock_import),
pytest.raises(
ModuleNotFoundError, match="homeassistant.components.executor_import"
),
):
await executor_import_integration.async_get_component()
# We should not have tried to fall back to the event loop import
assert "loaded_executor=False" not in caplog.text
assert "homeassistant.components.executor_import" not in sys.modules
assert "custom_components.executor_import" not in sys.modules
assert import_attempts == 1
async def test_async_get_component_raises_after_import_failure(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Verify async_get_component raises if we fail to import in both the executor and loop."""
executor_import_integration = _get_test_integration(
hass, "executor_import", True, import_executor=True
)
assert executor_import_integration.import_executor is True
module_mock = MagicMock()
import_attempts = 0
def mock_import(module: str, *args: Any, **kwargs: Any) -> Any:
nonlocal import_attempts
if module == "homeassistant.components.executor_import":
import_attempts += 1
if import_attempts == 1:
# _DeadlockError inherits from RuntimeError
raise RuntimeError(
"Detected deadlock trying to import homeassistant.components.executor_import"
)
if import_attempts == 2:
raise ImportError("Failed import homeassistant.components.executor_import")
return module_mock
assert "homeassistant.components.executor_import" not in sys.modules
assert "custom_components.executor_import" not in sys.modules
with (
patch("homeassistant.loader.importlib.import_module", mock_import),
pytest.raises(ImportError),
):
await executor_import_integration.async_get_component()
assert (
"Detected deadlock trying to import homeassistant.components.executor_import"
in caplog.text
)
assert "loaded_executor=False" not in caplog.text
async def test_async_get_platform_deadlock_fallback(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Verify async_get_platform fallback to importing in the event loop on deadlock."""
executor_import_integration = _get_test_integration(
hass, "executor_import", True, import_executor=True
)
assert executor_import_integration.import_executor is True
module_mock = MagicMock()
import_attempts = 0
def mock_import(module: str, *args: Any, **kwargs: Any) -> Any:
nonlocal import_attempts
if module == "homeassistant.components.executor_import.config_flow":
import_attempts += 1
if import_attempts == 1:
# _DeadlockError inherits from RuntimeError
raise RuntimeError(
"Detected deadlock trying to import homeassistant.components.executor_import"
)
return module_mock
assert "homeassistant.components.executor_import" not in sys.modules
assert "custom_components.executor_import" not in sys.modules
with patch("homeassistant.loader.importlib.import_module", mock_import):
module = await executor_import_integration.async_get_platform("config_flow")
assert (
"Detected deadlock trying to import homeassistant.components.executor_import"
in caplog.text
)
# We should have tried both the executor and loop
assert "executor=['config_flow']" in caplog.text
assert "loop=['config_flow']" in caplog.text
assert module is module_mock
async def test_async_get_platform_deadlock_fallback_module_not_found(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Verify async_get_platform fallback behavior.
Ensure that fallback is not triggered on ModuleNotFoundError.
"""
executor_import_integration = _get_test_integration(
hass, "executor_import", True, import_executor=True
)
assert executor_import_integration.import_executor is True
module_mock = MagicMock()
import_attempts = 0
def mock_import(module: str, *args: Any, **kwargs: Any) -> Any:
nonlocal import_attempts
if module == "homeassistant.components.executor_import.config_flow":
import_attempts += 1
if import_attempts == 1:
raise ModuleNotFoundError(
"Not found homeassistant.components.executor_import.config_flow",
name="homeassistant.components.executor_import.config_flow",
)
return module_mock
assert "homeassistant.components.executor_import" not in sys.modules
assert "custom_components.executor_import" not in sys.modules
with (
patch("homeassistant.loader.importlib.import_module", mock_import),
pytest.raises(
ModuleNotFoundError,
match="homeassistant.components.executor_import.config_flow",
),
):
await executor_import_integration.async_get_platform("config_flow")
# We should not have tried to fall back to the event loop import
assert "executor=['config_flow']" in caplog.text
assert "loop=['config_flow']" not in caplog.text
assert "homeassistant.components.executor_import" not in sys.modules
assert "custom_components.executor_import" not in sys.modules
assert import_attempts == 1
async def test_async_get_platform_raises_after_import_failure(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Verify async_get_platform raises if we fail to import in both the executor and loop."""
executor_import_integration = _get_test_integration(
hass, "executor_import", True, import_executor=True
)
assert executor_import_integration.import_executor is True
module_mock = MagicMock()
import_attempts = 0
def mock_import(module: str, *args: Any, **kwargs: Any) -> Any:
nonlocal import_attempts
if module == "homeassistant.components.executor_import.config_flow":
import_attempts += 1
if import_attempts == 1:
# _DeadlockError inherits from RuntimeError
raise RuntimeError(
"Detected deadlock trying to import homeassistant.components.executor_import"
)
if import_attempts == 2:
# _DeadlockError inherits from RuntimeError
raise ImportError(
"Error trying to import homeassistant.components.executor_import"
)
return module_mock
assert "homeassistant.components.executor_import" not in sys.modules
assert "custom_components.executor_import" not in sys.modules
with (
patch("homeassistant.loader.importlib.import_module", mock_import),
pytest.raises(ImportError),
):
await executor_import_integration.async_get_platform("config_flow")
assert (
"Detected deadlock trying to import homeassistant.components.executor_import"
in caplog.text
)
assert "loaded_executor=False" not in caplog.text
async def test_platforms_exists(
hass: HomeAssistant, enable_custom_integrations: None
) -> None:
"""Test platforms_exists."""
original_os_listdir = os.listdir
paths: list[str] = []
def mock_list_dir(path: str) -> list[str]:
paths.append(path)
return original_os_listdir(path)
with patch("homeassistant.loader.os.listdir", mock_list_dir):
integration = await loader.async_get_integration(
hass, "test_integration_platform"
)
assert integration.domain == "test_integration_platform"
# Verify the files cache is primed
assert integration.file_path in paths
# component is loaded, should now return False
with patch("homeassistant.loader.os.listdir", wraps=os.listdir) as mock_exists:
component = integration.get_component()
assert component.DOMAIN == "test_integration_platform"
# The files cache should be primed when
# the integration is resolved
assert mock_exists.call_count == 0
# component is loaded, should now return False
with patch("homeassistant.loader.os.listdir", wraps=os.listdir) as mock_exists:
assert integration.platforms_exists(("non_existing",)) == []
# We should remember which files exist
assert mock_exists.call_count == 0
# component is loaded, should now return False
with patch("homeassistant.loader.os.listdir", wraps=os.listdir) as mock_exists:
assert integration.platforms_exists(("non_existing",)) == []
# We should remember the file does not exist
assert mock_exists.call_count == 0
assert integration.platforms_exists(["group"]) == ["group"]
platform = await integration.async_get_platform("group")
assert platform.MAGIC == 1
platform = integration.get_platform("group")
assert platform.MAGIC == 1
assert integration.platforms_exists(["group"]) == ["group"]
assert integration.platforms_are_loaded(["group"]) is True
assert integration.platforms_are_loaded(["other"]) is False
async def test_async_get_platforms_loads_loop_if_already_in_sys_modules(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
enable_custom_integrations: None,
) -> None:
"""Verify async_get_platforms does not create an executor job.
Case is for when the module is already in sys.modules.
"""
integration = await loader.async_get_integration(
hass, "test_package_loaded_executor"
)
assert integration.pkg_path == "custom_components.test_package_loaded_executor"
assert integration.import_executor is True
assert integration.config_flow is True
assert "test_package_loaded_executor" not in hass.config.components
assert "test_package_loaded_executor.config_flow" not in hass.config.components
await integration.async_get_component()
button_module_name = f"{integration.pkg_path}.button"
switch_module_name = f"{integration.pkg_path}.switch"
light_module_name = f"{integration.pkg_path}.light"
button_module_mock = MagicMock()
switch_module_mock = MagicMock()
light_module_mock = MagicMock()
def import_module(name: str) -> Any:
if name == button_module_name:
return button_module_mock
if name == switch_module_name:
return switch_module_mock
if name == light_module_name:
return light_module_mock
raise ImportError
modules_without_button = {
k: v for k, v in sys.modules.items() if k != button_module_name
}
with (
patch.dict(
"sys.modules",
modules_without_button,
clear=True,
),
patch("homeassistant.loader.importlib.import_module", import_module),
):
module = (await integration.async_get_platforms(["button"]))["button"]
# The button module is missing so we should load
# in the executor
assert "executor=['button']" in caplog.text
assert "loop=[]" in caplog.text
assert module is button_module_mock
caplog.clear()
with (
patch.dict(
"sys.modules",
{
**modules_without_button,
button_module_name: button_module_mock,
},
),
patch("homeassistant.loader.importlib.import_module", import_module),
):
module = (await integration.async_get_platforms(["button"]))["button"]
# Everything is cached so there should be no logging
assert "loop=" not in caplog.text
assert "executor=" not in caplog.text
assert module is button_module_mock
caplog.clear()
modules_without_switch = {
k: v for k, v in sys.modules.items() if k not in switch_module_name
}
with (
patch.dict(
"sys.modules",
{**modules_without_switch, light_module_name: light_module_mock},
clear=True,
),
patch("homeassistant.loader.importlib.import_module", import_module),
):
modules = await integration.async_get_platforms(["button", "switch", "light"])
# The button module is already in the cache so nothing happens
# The switch module is loaded in the executor since its not in the cache
# The light module is in memory but not in the cache so its loaded in the loop
assert "['button']" not in caplog.text
assert "executor=['switch']" in caplog.text
assert "loop=['light']" in caplog.text
assert modules == {
"button": button_module_mock,
"switch": switch_module_mock,
"light": light_module_mock,
}
assert integration.get_platform_cached("button") is button_module_mock
assert integration.get_platform_cached("switch") is switch_module_mock
assert integration.get_platform_cached("light") is light_module_mock
async def test_async_get_platforms_concurrent_loads(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
enable_custom_integrations: None,
) -> None:
"""Verify async_get_platforms waits if the first load if called again.
Case is for when when a second load is called
and the first is still in progress.
"""
integration = await loader.async_get_integration(
hass, "test_package_loaded_executor"
)
assert integration.pkg_path == "custom_components.test_package_loaded_executor"
assert integration.import_executor is True
assert integration.config_flow is True
assert "test_package_loaded_executor" not in hass.config.components
assert "test_package_loaded_executor.config_flow" not in hass.config.components
await integration.async_get_component()
button_module_name = f"{integration.pkg_path}.button"
button_module_mock = MagicMock()
imports = []
start_event = threading.Event()
import_event = asyncio.Event()
def import_module(name: str) -> Any:
hass.loop.call_soon_threadsafe(import_event.set)
imports.append(name)
start_event.wait()
if name == button_module_name:
return button_module_mock
raise ImportError
modules_without_button = {
k: v
for k, v in sys.modules.items()
if k not in (button_module_name, integration.pkg_path)
}
with (
patch.dict(
"sys.modules",
modules_without_button,
clear=True,
),
patch("homeassistant.loader.importlib.import_module", import_module),
):
load_task1 = asyncio.create_task(integration.async_get_platforms(["button"]))
load_task2 = asyncio.create_task(integration.async_get_platforms(["button"]))
await import_event.wait() # make sure the import is started
assert not integration._import_futures["button"].done()
start_event.set()
load_result1 = await load_task1
load_result2 = await load_task2
assert integration._import_futures is not None
assert load_result1 == {"button": button_module_mock}
assert load_result2 == {"button": button_module_mock}
assert imports == [button_module_name]
assert integration.get_platform_cached("button") is button_module_mock
async def test_integration_warnings(
hass: HomeAssistant,
enable_custom_integrations: None,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test integration warnings."""
await loader.async_get_integration(hass, "test_package_loaded_loop")
assert "configured to to import its code in the event loop" in caplog.text
async def test_has_services(hass: HomeAssistant, enable_custom_integrations) -> None:
"""Test has_services."""
integration = await loader.async_get_integration(hass, "test")
assert integration.has_services is False
integration = await loader.async_get_integration(hass, "test_with_services")
assert integration.has_services is True
async def test_hass_helpers_use_reported(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture, mock_integration_frame: Mock
) -> None:
"""Test that use of hass.components is reported."""
integration_frame = frame.IntegrationFrame(
custom_integration=True,
_frame=mock_integration_frame,
integration="test_integration_frame",
module="custom_components.test_integration_frame",
relative_filename="custom_components/test_integration_frame/__init__.py",
)
with (
patch.object(frame, "_REPORTED_INTEGRATIONS", new=set()),
patch(
"homeassistant.helpers.frame.get_integration_frame",
return_value=integration_frame,
),
patch(
"homeassistant.helpers.aiohttp_client.async_get_clientsession",
return_value=None,
),
):
hass.helpers.aiohttp_client.async_get_clientsession()
assert (
"Detected that custom integration 'test_integration_frame' "
"accesses hass.helpers.aiohttp_client. This is deprecated"
) in caplog.text