Ensure dependencies are awaited correctly when setting up integrations (#91454)

* Do not wait

* Correct tests

* Manage after dependencies stage 1

* test bootstrap dependencies

* Assert log the dependenciy is waited for

* Improve docstrings

* Assert outside callback

* Patch async_get_integrations

* Revert changes made to snips integration

* Undo changes to mqtt_statestream
This commit is contained in:
Jan Bouwhuis 2023-04-21 08:33:50 +02:00 committed by GitHub
parent 2e18b37291
commit da26b0a930
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 139 additions and 3 deletions

View file

@ -629,6 +629,9 @@ async def _async_set_up_integrations(
- stage_1_domains - stage_1_domains
) )
# Enables after dependencies when setting up stage 1 domains
async_set_domains_to_be_loaded(hass, stage_1_domains)
# Start setup # Start setup
if stage_1_domains: if stage_1_domains:
_LOGGER.info("Setting up stage 1: %s", stage_1_domains) _LOGGER.info("Setting up stage 1: %s", stage_1_domains)
@ -640,7 +643,7 @@ async def _async_set_up_integrations(
except asyncio.TimeoutError: except asyncio.TimeoutError:
_LOGGER.warning("Setup timed out for stage 1 - moving forward") _LOGGER.warning("Setup timed out for stage 1 - moving forward")
# Enables after dependencies # Add after dependencies when setting up stage 2 domains
async_set_domains_to_be_loaded(hass, stage_2_domains) async_set_domains_to_be_loaded(hass, stage_2_domains)
if stage_2_domains: if stage_2_domains:

View file

@ -67,7 +67,8 @@ def async_set_domains_to_be_loaded(hass: core.HomeAssistant, domains: set[str])
- Properly handle after_dependencies. - Properly handle after_dependencies.
- Keep track of domains which will load but have not yet finished loading - Keep track of domains which will load but have not yet finished loading
""" """
hass.data[DATA_SETUP_DONE] = {domain: asyncio.Event() for domain in domains} hass.data.setdefault(DATA_SETUP_DONE, {})
hass.data[DATA_SETUP_DONE].update({domain: asyncio.Event() for domain in domains})
def setup_component(hass: core.HomeAssistant, domain: str, config: ConfigType) -> bool: def setup_component(hass: core.HomeAssistant, domain: str, config: ConfigType) -> bool:

View file

@ -1,6 +1,6 @@
"""Test the bootstrapping.""" """Test the bootstrapping."""
import asyncio import asyncio
from collections.abc import Generator from collections.abc import Generator, Iterable
import glob import glob
import os import os
from typing import Any from typing import Any
@ -10,12 +10,16 @@ import pytest
from homeassistant import bootstrap, runner from homeassistant import bootstrap, runner
import homeassistant.config as config_util import homeassistant.config as config_util
from homeassistant.config_entries import HANDLERS, ConfigEntry
from homeassistant.const import SIGNAL_BOOTSTRAP_INTEGRATIONS from homeassistant.const import SIGNAL_BOOTSTRAP_INTEGRATIONS
from homeassistant.core import HomeAssistant, async_get_hass, callback from homeassistant.core import HomeAssistant, async_get_hass, callback
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import Integration
from .common import ( from .common import (
MockConfigEntry,
MockModule, MockModule,
MockPlatform, MockPlatform,
get_test_config_dir, get_test_config_dir,
@ -825,3 +829,131 @@ async def test_bootstrap_empty_integrations(
"""Test setting up an empty integrations does not raise.""" """Test setting up an empty integrations does not raise."""
await bootstrap.async_setup_multi_components(hass, set(), {}) await bootstrap.async_setup_multi_components(hass, set(), {})
await hass.async_block_till_done() await hass.async_block_till_done()
@pytest.mark.parametrize("integration", ["mqtt_eventstream", "mqtt_statestream"])
@pytest.mark.parametrize("load_registries", [False])
async def test_bootstrap_dependencies(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
integration: str,
) -> None:
"""Test dependencies are set up correctly,."""
# Prepare MQTT config entry
@HANDLERS.register("mqtt")
class MockConfigFlow:
"""Mock the MQTT config flow."""
VERSION = 1
entry = MockConfigEntry(domain="mqtt", data={"broker": "test-broker"})
entry.add_to_hass(hass)
calls: list[str] = []
assertions: list[bool] = []
async def async_mqtt_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Assert the mqtt config entry was set up."""
calls.append("mqtt")
# assert the integration is not yet set up
assertions.append(hass.data["setup_done"][integration].is_set() is False)
assertions.append(
all(
dependency in hass.config.components
for dependency in integrations[integration]["dependencies"]
)
)
assertions.append(integration not in hass.config.components)
return True
async def async_integration_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Assert the mqtt config entry was set up."""
calls.append(integration)
# assert mqtt was already set up
assertions.append(
"mqtt" not in hass.data["setup_done"]
or hass.data["setup_done"]["mqtt"].is_set()
)
assertions.append("mqtt" in hass.config.components)
return True
mqtt_integration = mock_integration(
hass,
MockModule(
"mqtt",
async_setup_entry=async_mqtt_setup_entry,
dependencies=["file_upload", "http"],
),
)
mqtt_integration._import_platform = Mock()
# mqtt_integration.async_migrate = AsyncMock(return_value=False)
integrations = {
"mqtt": {
"dependencies": {"file_upload", "http"},
"integration": mqtt_integration,
},
"mqtt_eventstream": {
"dependencies": {"mqtt"},
"integration": mock_integration(
hass,
MockModule(
"mqtt_eventstream",
async_setup=async_integration_setup,
dependencies=["mqtt"],
),
),
},
"mqtt_statestream": {
"dependencies": {"mqtt"},
"integration": mock_integration(
hass,
MockModule(
"mqtt_statestream",
async_setup=async_integration_setup,
dependencies=["mqtt"],
),
),
},
"file_upload": {
"dependencies": {"http"},
"integration": mock_integration(
hass,
MockModule(
"file_upload",
dependencies=["http"],
),
),
},
"http": {
"dependencies": set(),
"integration": mock_integration(
hass,
MockModule("http", dependencies=[]),
),
},
}
async def mock_async_get_integrations(
hass: HomeAssistant, domains: Iterable[str]
) -> dict[str, Integration | Exception]:
"""Mock integrations."""
return {domain: integrations[domain]["integration"] for domain in domains}
with patch(
"homeassistant.setup.loader.async_get_integrations",
side_effect=mock_async_get_integrations,
), patch("homeassistant.config.async_process_component_config", return_value={}):
bootstrap.async_set_domains_to_be_loaded(hass, {integration})
await bootstrap.async_setup_multi_components(hass, {integration}, {})
await hass.async_block_till_done()
for assertion in assertions:
assert assertion
assert calls == ["mqtt", integration]
assert (
f"Dependency {integration} will wait for dependencies ['mqtt']" in caplog.text
)