2042 lines
73 KiB
Python
2042 lines
73 KiB
Python
"""Test to verify that we can load components."""
|
|
|
|
import asyncio
|
|
import os
|
|
import pathlib
|
|
import sys
|
|
import threading
|
|
from typing import Any
|
|
from unittest.mock import MagicMock, 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 homeassistant.helpers.json import json_dumps
|
|
from homeassistant.util.json import json_loads
|
|
|
|
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", dependencies=["mod1"]))
|
|
mock_integration(hass, MockModule("mod3", dependencies=["mod1"]))
|
|
mod_4 = mock_integration(hass, MockModule("mod4", dependencies=["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", dependencies=["mod4"]))
|
|
with pytest.raises(loader.CircularDependency):
|
|
await loader._async_component_dependencies(hass, mod_4)
|
|
|
|
# Create a different circular dependency
|
|
mock_integration(hass, MockModule("mod1", dependencies=["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", dependencies=["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"]
|
|
|
|
|
|
@pytest.mark.usefixtures("enable_custom_integrations")
|
|
async def test_custom_component_name(hass: HomeAssistant) -> 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
|
|
# pylint: disable-next=import-outside-toplevel
|
|
from custom_components.test_package import TEST
|
|
|
|
assert TEST == 5
|
|
|
|
|
|
@pytest.mark.usefixtures("enable_custom_integrations")
|
|
async def test_log_warning_custom_component(
|
|
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
|
|
) -> 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
|
|
|
|
|
|
@pytest.mark.usefixtures("enable_custom_integrations")
|
|
async def test_custom_integration_version_not_valid(
|
|
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
|
|
) -> 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"),
|
|
],
|
|
)
|
|
@pytest.mark.usefixtures("enable_custom_integrations")
|
|
async def test_custom_integration_version_blocked(
|
|
hass: HomeAssistant,
|
|
caplog: pytest.LogCaptureFixture,
|
|
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"),
|
|
],
|
|
)
|
|
@pytest.mark.usefixtures("enable_custom_integrations")
|
|
async def test_custom_integration_version_not_blocked(
|
|
hass: HomeAssistant,
|
|
caplog: pytest.LogCaptureFixture,
|
|
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
|
|
|
|
|
|
@pytest.mark.usefixtures("enable_custom_integrations")
|
|
async def test_get_integration_legacy(hass: HomeAssistant) -> 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
|
|
|
|
|
|
@pytest.mark.usefixtures("enable_custom_integrations")
|
|
async def test_get_integration_custom_component(hass: HomeAssistant) -> 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",
|
|
"quality_scale": "gold",
|
|
},
|
|
)
|
|
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.overwrites_built_in is False
|
|
assert integration.version == "1.0.0"
|
|
assert integration.quality_scale == "gold"
|
|
|
|
integration = loader.Integration(
|
|
hass,
|
|
"custom_components.hue",
|
|
None,
|
|
{
|
|
"name": "Philips Hue",
|
|
"domain": "hue",
|
|
"dependencies": ["test-dep"],
|
|
"requirements": ["test-req==1.0.0"],
|
|
"quality_scale": "gold",
|
|
},
|
|
)
|
|
assert integration.is_built_in is False
|
|
assert integration.overwrites_built_in is True
|
|
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
|
|
assert integration.quality_scale == "custom"
|
|
|
|
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.overwrites_built_in is True
|
|
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: HomeAssistant, name: str
|
|
) -> loader.Integration:
|
|
"""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: HomeAssistant, name: str, config_flow: bool
|
|
) -> loader.Integration:
|
|
"""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: HomeAssistant, name: str, config_flow: bool
|
|
) -> loader.Integration:
|
|
"""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: HomeAssistant, name: str, config_flow: bool
|
|
) -> loader.Integration:
|
|
"""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: HomeAssistant, name: str, config_flow: bool
|
|
) -> loader.Integration:
|
|
"""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: HomeAssistant, name: str, config_flow: bool
|
|
) -> loader.Integration:
|
|
"""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"]},
|
|
],
|
|
},
|
|
)
|
|
|
|
|
|
@pytest.mark.usefixtures("enable_custom_integrations")
|
|
async def test_get_custom_components(hass: HomeAssistant) -> 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._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)
|
|
|
|
|
|
@pytest.mark.usefixtures("enable_custom_integrations")
|
|
async def test_custom_component_overwriting_core(hass: HomeAssistant) -> None:
|
|
"""Test loading a custom component that overwrites a core component."""
|
|
# First load the core 'light' component
|
|
core_light = await loader.async_get_integration(hass, "light")
|
|
assert core_light.is_built_in is True
|
|
|
|
# create a mock custom 'light' component
|
|
mock_integration(
|
|
hass,
|
|
MockModule("light", partial_manifest={"version": "1.0.0"}),
|
|
built_in=False,
|
|
)
|
|
|
|
# Try to load the 'light' component again
|
|
custom_light = await loader.async_get_integration(hass, "light")
|
|
|
|
# Assert that we got the custom component instead of the core one
|
|
assert custom_light.is_built_in is False
|
|
assert custom_light.overwrites_built_in is True
|
|
assert custom_light.version == "1.0.0"
|
|
|
|
|
|
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"]
|
|
|
|
|
|
@pytest.mark.usefixtures("enable_custom_integrations")
|
|
async def test_import_platform_executor(hass: HomeAssistant) -> 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) -> 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) -> 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),
|
|
# Loaded custom integration with known issue tracker
|
|
("bla_custom", "custom_components.bla_custom.sensor", CUSTOM_ISSUE_TRACKER),
|
|
("bla_custom", None, CUSTOM_ISSUE_TRACKER),
|
|
# Loaded 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),
|
|
# Unloaded custom integration with known issue tracker
|
|
("bla_custom_not_loaded", None, CUSTOM_ISSUE_TRACKER),
|
|
# Unloaded custom integration without known issue tracker
|
|
("bla_custom_not_loaded_no_tracker", None, 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: HomeAssistant,
|
|
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)
|
|
|
|
cust_unloaded_module = MockModule(
|
|
"bla_custom_not_loaded",
|
|
partial_manifest={"issue_tracker": CUSTOM_ISSUE_TRACKER},
|
|
)
|
|
cust_unloaded = loader.Integration(
|
|
hass,
|
|
f"{loader.PACKAGE_CUSTOM_COMPONENTS}.{cust_unloaded_module.DOMAIN}",
|
|
pathlib.Path(""),
|
|
cust_unloaded_module.mock_manifest(),
|
|
set(),
|
|
)
|
|
|
|
cust_unloaded_no_tracker_module = MockModule("bla_custom_not_loaded_no_tracker")
|
|
cust_unloaded_no_tracker = loader.Integration(
|
|
hass,
|
|
f"{loader.PACKAGE_CUSTOM_COMPONENTS}.{cust_unloaded_no_tracker_module.DOMAIN}",
|
|
pathlib.Path(""),
|
|
cust_unloaded_no_tracker_module.mock_manifest(),
|
|
set(),
|
|
)
|
|
hass.data["custom_components"] = {
|
|
"bla_custom_not_loaded": cust_unloaded,
|
|
"bla_custom_not_loaded_no_tracker": cust_unloaded_no_tracker,
|
|
}
|
|
|
|
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: HomeAssistant, 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: HomeAssistant, 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() -> None:
|
|
"""Test that config folder is not in path."""
|
|
|
|
# Verify that we are unable to import this file from top level
|
|
with pytest.raises(ImportError):
|
|
# pylint: disable-next=import-outside-toplevel
|
|
import check_config_not_in_path # noqa: F401
|
|
|
|
# Verify that we are able to load the file with absolute path
|
|
# pylint: disable-next=import-outside-toplevel,hass-relative-import
|
|
import tests.testing_config.check_config_not_in_path # noqa: F401
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("integration_frame_path", "expected"),
|
|
[
|
|
pytest.param(
|
|
"custom_components/test_integration_frame", True, id="custom integration"
|
|
),
|
|
pytest.param(
|
|
"homeassistant/components/test_integration_frame",
|
|
False,
|
|
id="core integration",
|
|
),
|
|
pytest.param("homeassistant/test_integration_frame", False, id="core"),
|
|
],
|
|
)
|
|
@pytest.mark.usefixtures("mock_integration_frame")
|
|
@patch.object(frame, "_REPORTED_INTEGRATIONS", set())
|
|
async def test_hass_components_use_reported(
|
|
hass: HomeAssistant,
|
|
caplog: pytest.LogCaptureFixture,
|
|
expected: bool,
|
|
) -> None:
|
|
"""Test whether use of hass.components is reported."""
|
|
with (
|
|
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)
|
|
|
|
reported = (
|
|
"Detected that custom integration 'test_integration_frame'"
|
|
" accesses hass.components.http, which should be updated"
|
|
) in caplog.text
|
|
assert reported == expected
|
|
|
|
|
|
async def test_async_get_component_preloads_config_and_config_flow(
|
|
hass: HomeAssistant,
|
|
) -> 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
|
|
),
|
|
}
|
|
|
|
|
|
@pytest.mark.usefixtures("enable_custom_integrations")
|
|
async def test_async_get_component_loads_loop_if_already_in_sys_modules(
|
|
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
|
|
) -> 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
|
|
|
|
|
|
@pytest.mark.usefixtures("enable_custom_integrations")
|
|
async def test_async_get_component_concurrent_loads(hass: HomeAssistant) -> 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
|
|
|
|
|
|
@pytest.mark.usefixtures("enable_custom_integrations")
|
|
async def test_platforms_exists(hass: HomeAssistant) -> 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
|
|
|
|
|
|
@pytest.mark.usefixtures("enable_custom_integrations")
|
|
async def test_async_get_platforms_loads_loop_if_already_in_sys_modules(
|
|
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
|
|
) -> 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
|
|
|
|
|
|
@pytest.mark.usefixtures("enable_custom_integrations")
|
|
async def test_async_get_platforms_concurrent_loads(hass: HomeAssistant) -> 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
|
|
|
|
|
|
@pytest.mark.usefixtures("enable_custom_integrations")
|
|
async def test_integration_warnings(
|
|
hass: HomeAssistant, 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
|
|
|
|
|
|
@pytest.mark.usefixtures("enable_custom_integrations")
|
|
async def test_has_services(hass: HomeAssistant) -> 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
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("integration_frame_path", "expected"),
|
|
[
|
|
pytest.param(
|
|
"custom_components/test_integration_frame", True, id="custom integration"
|
|
),
|
|
pytest.param(
|
|
"homeassistant/components/test_integration_frame",
|
|
False,
|
|
id="core integration",
|
|
),
|
|
pytest.param("homeassistant/test_integration_frame", False, id="core"),
|
|
],
|
|
)
|
|
@pytest.mark.usefixtures("mock_integration_frame")
|
|
@patch.object(frame, "_REPORTED_INTEGRATIONS", set())
|
|
async def test_hass_helpers_use_reported(
|
|
hass: HomeAssistant,
|
|
caplog: pytest.LogCaptureFixture,
|
|
expected: bool,
|
|
) -> None:
|
|
"""Test whether use of hass.helpers is reported."""
|
|
with (
|
|
patch(
|
|
"homeassistant.helpers.aiohttp_client.async_get_clientsession",
|
|
return_value=None,
|
|
),
|
|
):
|
|
hass.helpers.aiohttp_client.async_get_clientsession()
|
|
|
|
reported = (
|
|
"Detected that custom integration 'test_integration_frame' "
|
|
"accesses hass.helpers.aiohttp_client, which should be updated"
|
|
) in caplog.text
|
|
assert reported == expected
|
|
|
|
|
|
async def test_manifest_json_fragment_round_trip(hass: HomeAssistant) -> None:
|
|
"""Test json_fragment roundtrip."""
|
|
integration = await loader.async_get_integration(hass, "hue")
|
|
assert (
|
|
json_loads(json_dumps(integration.manifest_json_fragment))
|
|
== integration.manifest
|
|
)
|