Improve after_dependencies handling (#36898)

This commit is contained in:
Paulus Schoutsen 2020-06-19 17:24:33 -07:00 committed by GitHub
parent 93272e3083
commit 5642027ffb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 377 additions and 166 deletions

View File

@ -1,6 +1,7 @@
"""Provide methods to bootstrap a Home Assistant instance.""" """Provide methods to bootstrap a Home Assistant instance."""
import asyncio import asyncio
import contextlib import contextlib
from datetime import datetime
import logging import logging
import logging.handlers import logging.handlers
import os import os
@ -20,7 +21,12 @@ from homeassistant.const import (
) )
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from homeassistant.setup import DATA_SETUP, DATA_SETUP_STARTED, async_setup_component from homeassistant.setup import (
DATA_SETUP,
DATA_SETUP_STARTED,
async_set_domains_to_be_loaded,
async_setup_component,
)
from homeassistant.util.logging import async_activate_log_queue_handler from homeassistant.util.logging import async_activate_log_queue_handler
from homeassistant.util.package import async_get_user_site, is_virtual_env from homeassistant.util.package import async_get_user_site, is_virtual_env
from homeassistant.util.yaml import clear_secret_cache from homeassistant.util.yaml import clear_secret_cache
@ -36,10 +42,16 @@ LOG_SLOW_STARTUP_INTERVAL = 60
DEBUGGER_INTEGRATIONS = {"ptvsd"} DEBUGGER_INTEGRATIONS = {"ptvsd"}
CORE_INTEGRATIONS = ("homeassistant", "persistent_notification") CORE_INTEGRATIONS = ("homeassistant", "persistent_notification")
LOGGING_INTEGRATIONS = {"logger", "system_log", "sentry"} LOGGING_INTEGRATIONS = {
STAGE_1_INTEGRATIONS = { # Set log levels
"logger",
# Error logging
"system_log",
"sentry",
# To record data # To record data
"recorder", "recorder",
}
STAGE_1_INTEGRATIONS = {
# To make sure we forward data to other instances # To make sure we forward data to other instances
"mqtt_eventstream", "mqtt_eventstream",
# To provide account link implementations # To provide account link implementations
@ -330,77 +342,130 @@ def _get_domains(hass: core.HomeAssistant, config: Dict[str, Any]) -> Set[str]:
return domains return domains
async def _async_log_pending_setups(
domains: Set[str], setup_started: Dict[str, datetime]
) -> None:
"""Periodic log of setups that are pending for longer than LOG_SLOW_STARTUP_INTERVAL."""
while True:
await asyncio.sleep(LOG_SLOW_STARTUP_INTERVAL)
remaining = [domain for domain in domains if domain in setup_started]
if remaining:
_LOGGER.info(
"Waiting on integrations to complete setup: %s", ", ".join(remaining),
)
async def async_setup_multi_components(
hass: core.HomeAssistant,
domains: Set[str],
config: Dict[str, Any],
setup_started: Dict[str, datetime],
) -> None:
"""Set up multiple domains. Log on failure."""
futures = {
domain: hass.async_create_task(async_setup_component(hass, domain, config))
for domain in domains
}
log_task = asyncio.create_task(_async_log_pending_setups(domains, setup_started))
await asyncio.wait(futures.values())
log_task.cancel()
errors = [domain for domain in domains if futures[domain].exception()]
for domain in errors:
exception = futures[domain].exception()
assert exception is not None
_LOGGER.error(
"Error setting up integration %s - received exception",
domain,
exc_info=(type(exception), exception, exception.__traceback__),
)
async def _async_set_up_integrations( async def _async_set_up_integrations(
hass: core.HomeAssistant, config: Dict[str, Any] hass: core.HomeAssistant, config: Dict[str, Any]
) -> None: ) -> None:
"""Set up all the integrations.""" """Set up all the integrations."""
setup_started = hass.data[DATA_SETUP_STARTED] = {} setup_started = hass.data[DATA_SETUP_STARTED] = {}
domains_to_setup = _get_domains(hass, config)
async def async_setup_multi_components(domains: Set[str]) -> None: # Resolve all dependencies so we know all integrations
"""Set up multiple domains. Log on failure.""" # that will have to be loaded and start rightaway
integration_cache: Dict[str, loader.Integration] = {}
to_resolve = domains_to_setup
while to_resolve:
old_to_resolve = to_resolve
to_resolve = set()
async def _async_log_pending_setups() -> None: integrations_to_process = [
"""Periodic log of setups that are pending for longer than LOG_SLOW_STARTUP_INTERVAL.""" int_or_exc
while True: for int_or_exc in await asyncio.gather(
await asyncio.sleep(LOG_SLOW_STARTUP_INTERVAL) *(
remaining = [domain for domain in domains if domain in setup_started] loader.async_get_integration(hass, domain)
for domain in old_to_resolve
if remaining: ),
_LOGGER.info( return_exceptions=True,
"Waiting on integrations to complete setup: %s",
", ".join(remaining),
)
futures = {
domain: hass.async_create_task(async_setup_component(hass, domain, config))
for domain in domains
}
log_task = asyncio.create_task(_async_log_pending_setups())
await asyncio.wait(futures.values())
log_task.cancel()
errors = [domain for domain in domains if futures[domain].exception()]
for domain in errors:
exception = futures[domain].exception()
assert exception is not None
_LOGGER.error(
"Error setting up integration %s - received exception",
domain,
exc_info=(type(exception), exception, exception.__traceback__),
) )
if isinstance(int_or_exc, loader.Integration)
]
resolve_dependencies_tasks = [
itg.resolve_dependencies()
for itg in integrations_to_process
if not itg.all_dependencies_resolved
]
domains = _get_domains(hass, config) if resolve_dependencies_tasks:
await asyncio.gather(*resolve_dependencies_tasks)
for itg in integrations_to_process:
integration_cache[itg.domain] = itg
for dep in itg.all_dependencies:
if dep in domains_to_setup:
continue
domains_to_setup.add(dep)
to_resolve.add(dep)
_LOGGER.info("Domains to be set up: %s", domains_to_setup)
logging_domains = domains_to_setup & LOGGING_INTEGRATIONS
# Load logging as soon as possible
if logging_domains:
_LOGGER.info("Setting up logging: %s", logging_domains)
await async_setup_multi_components(hass, logging_domains, config, setup_started)
# Start up debuggers. Start these first in case they want to wait. # Start up debuggers. Start these first in case they want to wait.
debuggers = domains & DEBUGGER_INTEGRATIONS debuggers = domains_to_setup & DEBUGGER_INTEGRATIONS
if debuggers: if debuggers:
_LOGGER.debug("Starting up debuggers %s", debuggers) _LOGGER.debug("Setting up debuggers: %s", debuggers)
await async_setup_multi_components(debuggers) await async_setup_multi_components(hass, debuggers, config, setup_started)
domains -= DEBUGGER_INTEGRATIONS
# Resolve all dependencies of all components so we can find the logging # calculate what components to setup in what stage
# and integrations that need faster initialization. stage_1_domains = set()
resolved_domains_task = asyncio.gather(
*(loader.async_component_dependencies(hass, domain) for domain in domains),
return_exceptions=True,
)
# Finish resolving domains # Find all dependencies of any dependency of any stage 1 integration that
for dep_domains in await resolved_domains_task: # we plan on loading and promote them to stage 1
# Result is either a set or an exception. We ignore exceptions deps_promotion = STAGE_1_INTEGRATIONS
# It will be properly handled during setup of the domain. while deps_promotion:
if isinstance(dep_domains, set): old_deps_promotion = deps_promotion
domains.update(dep_domains) deps_promotion = set()
# setup components for domain in old_deps_promotion:
logging_domains = domains & LOGGING_INTEGRATIONS if domain not in domains_to_setup or domain in stage_1_domains:
stage_1_domains = domains & STAGE_1_INTEGRATIONS continue
stage_2_domains = domains - logging_domains - stage_1_domains
if logging_domains: stage_1_domains.add(domain)
_LOGGER.info("Setting up %s", logging_domains)
await async_setup_multi_components(logging_domains) dep_itg = integration_cache.get(domain)
if dep_itg is None:
continue
deps_promotion.update(dep_itg.all_dependencies)
stage_2_domains = domains_to_setup - logging_domains - debuggers - stage_1_domains
# Kick off loading the registries. They don't need to be awaited. # Kick off loading the registries. They don't need to be awaited.
asyncio.gather( asyncio.gather(
@ -409,49 +474,17 @@ async def _async_set_up_integrations(
hass.helpers.area_registry.async_get_registry(), hass.helpers.area_registry.async_get_registry(),
) )
# Start setup
if stage_1_domains: if stage_1_domains:
_LOGGER.info("Setting up %s", stage_1_domains) _LOGGER.info("Setting up stage 1: %s", stage_1_domains)
await async_setup_multi_components(hass, stage_1_domains, config, setup_started)
await async_setup_multi_components(stage_1_domains) # Enables after dependencies
async_set_domains_to_be_loaded(hass, stage_1_domains | stage_2_domains)
# Load all integrations
after_dependencies: Dict[str, Set[str]] = {}
for int_or_exc in await asyncio.gather(
*(loader.async_get_integration(hass, domain) for domain in stage_2_domains),
return_exceptions=True,
):
# Exceptions are handled in async_setup_component.
if isinstance(int_or_exc, loader.Integration) and int_or_exc.after_dependencies:
after_dependencies[int_or_exc.domain] = set(int_or_exc.after_dependencies)
last_load = None
while stage_2_domains:
domains_to_load = set()
for domain in stage_2_domains:
after_deps = after_dependencies.get(domain)
# Load if integration has no after_dependencies or they are
# all loaded
if not after_deps or not after_deps - hass.config.components:
domains_to_load.add(domain)
if not domains_to_load or domains_to_load == last_load:
break
_LOGGER.debug("Setting up %s", domains_to_load)
await async_setup_multi_components(domains_to_load)
last_load = domains_to_load
stage_2_domains -= domains_to_load
# These are stage 2 domains that never have their after_dependencies
# satisfied.
if stage_2_domains: if stage_2_domains:
_LOGGER.debug("Final set up: %s", stage_2_domains) _LOGGER.info("Setting up stage 2: %s", stage_2_domains)
await async_setup_multi_components(hass, stage_2_domains, config, setup_started)
await async_setup_multi_components(stage_2_domains)
# Wrap up startup # Wrap up startup
_LOGGER.debug("Waiting for startup to wrap up") _LOGGER.debug("Waiting for startup to wrap up")

View File

@ -3,7 +3,6 @@
"name": "Auth", "name": "Auth",
"documentation": "https://www.home-assistant.io/integrations/auth", "documentation": "https://www.home-assistant.io/integrations/auth",
"dependencies": ["http"], "dependencies": ["http"],
"after_dependencies": ["onboarding"],
"codeowners": ["@home-assistant/core"], "codeowners": ["@home-assistant/core"],
"quality_scale": "internal" "quality_scale": "internal"
} }

View File

@ -203,6 +203,14 @@ class Integration:
self.file_path = file_path self.file_path = file_path
self.manifest = manifest self.manifest = manifest
manifest["is_built_in"] = self.is_built_in manifest["is_built_in"] = self.is_built_in
if self.dependencies:
self._all_dependencies_resolved: Optional[bool] = None
self._all_dependencies: Optional[Set[str]] = None
else:
self._all_dependencies_resolved = True
self._all_dependencies = set()
_LOGGER.info("Loaded %s from %s", self.domain, pkg_path) _LOGGER.info("Loaded %s from %s", self.domain, pkg_path)
@property @property
@ -255,6 +263,49 @@ class Integration:
"""Test if package is a built-in integration.""" """Test if package is a built-in integration."""
return self.pkg_path.startswith(PACKAGE_BUILTIN) return self.pkg_path.startswith(PACKAGE_BUILTIN)
@property
def all_dependencies(self) -> Set[str]:
"""Return all dependencies including sub-dependencies."""
if self._all_dependencies is None:
raise RuntimeError("Dependencies not resolved!")
return self._all_dependencies
@property
def all_dependencies_resolved(self) -> bool:
"""Return if all dependencies have been resolved."""
return self._all_dependencies_resolved is not None
async def resolve_dependencies(self) -> bool:
"""Resolve all dependencies."""
if self._all_dependencies_resolved is not None:
return self._all_dependencies_resolved
try:
dependencies = await _async_component_dependencies(
self.hass, self.domain, self, set(), set()
)
dependencies.discard(self.domain)
self._all_dependencies = dependencies
self._all_dependencies_resolved = True
except IntegrationNotFound as err:
_LOGGER.error(
"Unable to resolve dependencies for %s: we are unable to resolve (sub)dependency %s",
self.domain,
err.domain,
)
self._all_dependencies_resolved = False
except CircularDependency as err:
_LOGGER.error(
"Unable to resolve dependencies for %s: it contains a circular dependency: %s -> %s",
self.domain,
err.from_domain,
err.to_domain,
)
self._all_dependencies_resolved = False
return self._all_dependencies_resolved
def get_component(self) -> ModuleType: def get_component(self) -> ModuleType:
"""Return the component.""" """Return the component."""
cache = self.hass.data.setdefault(DATA_COMPONENTS, {}) cache = self.hass.data.setdefault(DATA_COMPONENTS, {})
@ -488,23 +539,18 @@ def bind_hass(func: CALLABLE_T) -> CALLABLE_T:
return func return func
async def async_component_dependencies(hass: "HomeAssistant", domain: str) -> Set[str]:
"""Return all dependencies and subdependencies of components.
Raises CircularDependency if a circular dependency is found.
"""
return await _async_component_dependencies(hass, domain, set(), set())
async def _async_component_dependencies( async def _async_component_dependencies(
hass: "HomeAssistant", domain: str, loaded: Set[str], loading: Set[str] hass: "HomeAssistant",
start_domain: str,
integration: Integration,
loaded: Set[str],
loading: Set[str],
) -> Set[str]: ) -> Set[str]:
"""Recursive function to get component dependencies. """Recursive function to get component dependencies.
Async friendly. Async friendly.
""" """
integration = await async_get_integration(hass, domain) domain = integration.domain
loading.add(domain) loading.add(domain)
for dependency_domain in integration.dependencies: for dependency_domain in integration.dependencies:
@ -516,11 +562,19 @@ async def _async_component_dependencies(
if dependency_domain in loading: if dependency_domain in loading:
raise CircularDependency(domain, dependency_domain) raise CircularDependency(domain, dependency_domain)
dep_loaded = await _async_component_dependencies( loaded.add(dependency_domain)
hass, dependency_domain, loaded, loading
)
loaded.update(dep_loaded) dep_integration = await async_get_integration(hass, dependency_domain)
if start_domain in dep_integration.after_dependencies:
raise CircularDependency(start_domain, dependency_domain)
if dep_integration.dependencies:
dep_loaded = await _async_component_dependencies(
hass, start_domain, dep_integration, loaded, loading
)
loaded.update(dep_loaded)
loaded.add(domain) loaded.add(domain)
loading.remove(domain) loading.remove(domain)

View File

@ -3,7 +3,7 @@ import asyncio
import logging.handlers import logging.handlers
from timeit import default_timer as timer from timeit import default_timer as timer
from types import ModuleType from types import ModuleType
from typing import Awaitable, Callable, List, Optional from typing import Awaitable, Callable, Optional, Set
from homeassistant import config as conf_util, core, loader, requirements from homeassistant import config as conf_util, core, loader, requirements
from homeassistant.config import async_notify_setup_error from homeassistant.config import async_notify_setup_error
@ -16,6 +16,7 @@ _LOGGER = logging.getLogger(__name__)
ATTR_COMPONENT = "component" ATTR_COMPONENT = "component"
DATA_SETUP_DONE = "setup_done"
DATA_SETUP_STARTED = "setup_started" DATA_SETUP_STARTED = "setup_started"
DATA_SETUP = "setup_tasks" DATA_SETUP = "setup_tasks"
DATA_DEPS_REQS = "deps_reqs_processed" DATA_DEPS_REQS = "deps_reqs_processed"
@ -26,6 +27,15 @@ SLOW_SETUP_WARNING = 10
SLOW_SETUP_MAX_WAIT = 1800 SLOW_SETUP_MAX_WAIT = 1800
@core.callback
def async_set_domains_to_be_loaded(hass: core.HomeAssistant, domains: Set[str]) -> None:
"""Set domains that are going to be loaded from the config.
This will allow us to properly handle after_dependencies.
"""
hass.data[DATA_SETUP_DONE] = {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:
"""Set up a component and all its dependencies.""" """Set up a component and all its dependencies."""
return asyncio.run_coroutine_threadsafe( return asyncio.run_coroutine_threadsafe(
@ -52,26 +62,43 @@ async def async_setup_component(
_async_setup_component(hass, domain, config) _async_setup_component(hass, domain, config)
) )
return await task # type: ignore try:
return await task # type: ignore
finally:
if domain in hass.data.get(DATA_SETUP_DONE, {}):
hass.data[DATA_SETUP_DONE].pop(domain).set()
async def _async_process_dependencies( async def _async_process_dependencies(
hass: core.HomeAssistant, config: ConfigType, name: str, dependencies: List[str] hass: core.HomeAssistant, config: ConfigType, integration: loader.Integration
) -> bool: ) -> bool:
"""Ensure all dependencies are set up.""" """Ensure all dependencies are set up."""
tasks = [async_setup_component(hass, dep, config) for dep in dependencies] tasks = {
dep: hass.loop.create_task(async_setup_component(hass, dep, config))
for dep in integration.dependencies
}
to_be_loaded = hass.data.get(DATA_SETUP_DONE, {})
for dep in integration.after_dependencies:
if dep in to_be_loaded and dep not in hass.config.components:
tasks[dep] = hass.loop.create_task(to_be_loaded[dep].wait())
if not tasks: if not tasks:
return True return True
results = await asyncio.gather(*tasks) _LOGGER.debug("Dependency %s will wait for %s", integration.domain, list(tasks))
results = await asyncio.gather(*tasks.values())
failed = [dependencies[idx] for idx, res in enumerate(results) if not res] failed = [
domain
for idx, domain in enumerate(integration.dependencies)
if not results[idx]
]
if failed: if failed:
_LOGGER.error( _LOGGER.error(
"Unable to set up dependencies of %s. Setup failed for dependencies: %s", "Unable to set up dependencies of %s. Setup failed for dependencies: %s",
name, integration.domain,
", ".join(failed), ", ".join(failed),
) )
@ -99,22 +126,7 @@ async def _async_setup_component(
return False return False
# Validate all dependencies exist and there are no circular dependencies # Validate all dependencies exist and there are no circular dependencies
try: if not await integration.resolve_dependencies():
await loader.async_component_dependencies(hass, domain)
except loader.IntegrationNotFound as err:
_LOGGER.error(
"Not setting up %s because we are unable to resolve (sub)dependency %s",
domain,
err.domain,
)
return False
except loader.CircularDependency as err:
_LOGGER.error(
"Not setting up %s because it contains a circular dependency: %s -> %s",
domain,
err.from_domain,
err.to_domain,
)
return False return False
# Process requirements as soon as possible, so we can import the component # Process requirements as soon as possible, so we can import the component
@ -301,9 +313,7 @@ async def async_process_deps_reqs(
elif integration.domain in processed: elif integration.domain in processed:
return return
if integration.dependencies and not await _async_process_dependencies( if not await _async_process_dependencies(hass, config, integration):
hass, config, integration.domain, integration.dependencies
):
raise HomeAssistantError("Could not set up all dependencies.") raise HomeAssistantError("Could not set up all dependencies.")
if not hass.config.skip_pip and integration.requirements: if not hass.config.skip_pip and integration.requirements:

View File

@ -103,6 +103,7 @@ ALLOWED_USED_COMPONENTS = {
"input_number", "input_number",
"input_select", "input_select",
"input_text", "input_text",
"onboarding",
"persistent_notification", "persistent_notification",
"person", "person",
"script", "script",
@ -253,7 +254,14 @@ def validate(integrations: Dict[str, Integration], config):
continue continue
# check that all referenced dependencies exist # check that all referenced dependencies exist
after_deps = integration.manifest.get("after_dependencies", [])
for dep in integration.manifest.get("dependencies", []): for dep in integration.manifest.get("dependencies", []):
if dep in after_deps:
integration.add_error(
"dependencies",
f"Dependency {dep} is both in dependencies and after_dependencies",
)
if dep not in integrations: if dep not in integrations:
integration.add_error( integration.add_error(
"dependencies", f"Dependency {dep} does not exist" "dependencies", f"Dependency {dep} does not exist"

View File

@ -991,6 +991,8 @@ def mock_integration(hass, module):
hass.data.setdefault(loader.DATA_INTEGRATIONS, {})[module.DOMAIN] = integration hass.data.setdefault(loader.DATA_INTEGRATIONS, {})[module.DOMAIN] = integration
hass.data.setdefault(loader.DATA_COMPONENTS, {})[module.DOMAIN] = module hass.data.setdefault(loader.DATA_COMPONENTS, {})[module.DOMAIN] = module
return integration
def mock_entity_platform(hass, platform_path, module): def mock_entity_platform(hass, platform_path, module):
"""Mock a entity platform. """Mock a entity platform.

View File

@ -7,7 +7,7 @@ from unittest.mock import Mock
import pytest import pytest
from homeassistant import bootstrap from homeassistant import bootstrap, core
import homeassistant.config as config_util import homeassistant.config as config_util
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
@ -16,9 +16,11 @@ from tests.async_mock import patch
from tests.common import ( from tests.common import (
MockConfigEntry, MockConfigEntry,
MockModule, MockModule,
MockPlatform,
flush_store, flush_store,
get_test_config_dir, get_test_config_dir,
mock_coro, mock_coro,
mock_entity_platform,
mock_integration, mock_integration,
) )
@ -81,7 +83,7 @@ async def test_core_failure_loads_safe_mode(hass, caplog):
assert "group" not in hass.config.components assert "group" not in hass.config.components
async def test_setting_up_config(hass, caplog): async def test_setting_up_config(hass):
"""Test we set up domains in config.""" """Test we set up domains in config."""
await bootstrap._async_set_up_integrations( await bootstrap._async_set_up_integrations(
hass, {"group hello": {}, "homeassistant": {}} hass, {"group hello": {}, "homeassistant": {}}
@ -90,9 +92,8 @@ async def test_setting_up_config(hass, caplog):
assert "group" in hass.config.components assert "group" in hass.config.components
async def test_setup_after_deps_all_present(hass, caplog): async def test_setup_after_deps_all_present(hass):
"""Test after_dependencies when all present.""" """Test after_dependencies when all present."""
caplog.set_level(logging.DEBUG)
order = [] order = []
def gen_domain_setup(domain): def gen_domain_setup(domain):
@ -122,19 +123,115 @@ async def test_setup_after_deps_all_present(hass, caplog):
), ),
) )
await bootstrap._async_set_up_integrations( with patch(
hass, {"root": {}, "first_dep": {}, "second_dep": {}} "homeassistant.components.logger.async_setup", gen_domain_setup("logger")
) ):
await bootstrap._async_set_up_integrations(
hass, {"root": {}, "first_dep": {}, "second_dep": {}, "logger": {}}
)
assert "root" in hass.config.components assert "root" in hass.config.components
assert "first_dep" in hass.config.components assert "first_dep" in hass.config.components
assert "second_dep" in hass.config.components assert "second_dep" in hass.config.components
assert order == ["root", "first_dep", "second_dep"] assert order == ["logger", "root", "first_dep", "second_dep"]
async def test_setup_after_deps_not_trigger_load(hass, caplog): async def test_setup_after_deps_in_stage_1_ignored(hass):
"""Test after_dependencies are ignored in stage 1."""
# This test relies on this
assert "cloud" in bootstrap.STAGE_1_INTEGRATIONS
order = []
def gen_domain_setup(domain):
async def async_setup(hass, config):
order.append(domain)
return True
return async_setup
mock_integration(
hass,
MockModule(
domain="normal_integration",
async_setup=gen_domain_setup("normal_integration"),
partial_manifest={"after_dependencies": ["an_after_dep"]},
),
)
mock_integration(
hass,
MockModule(
domain="an_after_dep", async_setup=gen_domain_setup("an_after_dep"),
),
)
mock_integration(
hass,
MockModule(
domain="cloud",
async_setup=gen_domain_setup("cloud"),
partial_manifest={"after_dependencies": ["normal_integration"]},
),
)
await bootstrap._async_set_up_integrations(
hass, {"cloud": {}, "normal_integration": {}, "an_after_dep": {}}
)
assert "normal_integration" in hass.config.components
assert "cloud" in hass.config.components
assert order == ["cloud", "an_after_dep", "normal_integration"]
async def test_setup_after_deps_via_platform(hass):
"""Test after_dependencies set up via platform."""
order = []
after_dep_event = asyncio.Event()
def gen_domain_setup(domain):
async def async_setup(hass, config):
if domain == "after_dep_of_platform_int":
await after_dep_event.wait()
order.append(domain)
return True
return async_setup
mock_integration(
hass,
MockModule(
domain="after_dep_of_platform_int",
async_setup=gen_domain_setup("after_dep_of_platform_int"),
),
)
mock_integration(
hass,
MockModule(
domain="platform_int",
async_setup=gen_domain_setup("platform_int"),
partial_manifest={"after_dependencies": ["after_dep_of_platform_int"]},
),
)
mock_entity_platform(hass, "light.platform_int", MockPlatform())
@core.callback
def continue_loading(_):
"""When light component loaded, continue other loading."""
after_dep_event.set()
hass.bus.async_listen_once("component_loaded", continue_loading)
await bootstrap._async_set_up_integrations(
hass, {"light": {"platform": "platform_int"}, "after_dep_of_platform_int": {}}
)
assert "light" in hass.config.components
assert "after_dep_of_platform_int" in hass.config.components
assert "platform_int" in hass.config.components
assert order == ["after_dep_of_platform_int", "platform_int"]
async def test_setup_after_deps_not_trigger_load(hass):
"""Test after_dependencies does not trigger loading it.""" """Test after_dependencies does not trigger loading it."""
caplog.set_level(logging.DEBUG)
order = [] order = []
def gen_domain_setup(domain): def gen_domain_setup(domain):
@ -169,12 +266,10 @@ async def test_setup_after_deps_not_trigger_load(hass, caplog):
assert "root" in hass.config.components assert "root" in hass.config.components
assert "first_dep" not in hass.config.components assert "first_dep" not in hass.config.components
assert "second_dep" in hass.config.components assert "second_dep" in hass.config.components
assert order == ["root", "second_dep"]
async def test_setup_after_deps_not_present(hass, caplog): async def test_setup_after_deps_not_present(hass):
"""Test after_dependencies when referenced integration doesn't exist.""" """Test after_dependencies when referenced integration doesn't exist."""
caplog.set_level(logging.DEBUG)
order = [] order = []
def gen_domain_setup(domain): def gen_domain_setup(domain):

View File

@ -13,27 +13,43 @@ async def test_component_dependencies(hass):
"""Test if we can get the proper load order of components.""" """Test if we can get the proper load order of components."""
mock_integration(hass, MockModule("mod1")) mock_integration(hass, MockModule("mod1"))
mock_integration(hass, MockModule("mod2", ["mod1"])) mock_integration(hass, MockModule("mod2", ["mod1"]))
mock_integration(hass, MockModule("mod3", ["mod2"])) mod_3 = mock_integration(hass, MockModule("mod3", ["mod2"]))
assert {"mod1", "mod2", "mod3"} == await loader.async_component_dependencies( assert {"mod1", "mod2", "mod3"} == await loader._async_component_dependencies(
hass, "mod3" hass, "mod_3", mod_3, set(), set()
) )
# Create circular dependency # Create circular dependency
mock_integration(hass, MockModule("mod1", ["mod3"])) mock_integration(hass, MockModule("mod1", ["mod3"]))
with pytest.raises(loader.CircularDependency): with pytest.raises(loader.CircularDependency):
print(await loader.async_component_dependencies(hass, "mod3")) print(
await loader._async_component_dependencies(
hass, "mod_3", mod_3, set(), set()
)
)
# Depend on non-existing component # Depend on non-existing component
mock_integration(hass, MockModule("mod1", ["nonexisting"])) mod_1 = mock_integration(hass, MockModule("mod1", ["nonexisting"]))
with pytest.raises(loader.IntegrationNotFound): with pytest.raises(loader.IntegrationNotFound):
print(await loader.async_component_dependencies(hass, "mod1")) print(
await loader._async_component_dependencies(
hass, "mod_1", mod_1, set(), set()
)
)
# Try to get dependencies for non-existing component # Having an after dependency 2 deps down that is circular
with pytest.raises(loader.IntegrationNotFound): mod_1 = mock_integration(
print(await loader.async_component_dependencies(hass, "nonexisting")) hass, MockModule("mod1", partial_manifest={"after_dependencies": ["mod_3"]})
)
with pytest.raises(loader.CircularDependency):
print(
await loader._async_component_dependencies(
hass, "mod_3", mod_3, set(), set()
)
)
def test_component_loader(hass): def test_component_loader(hass):

View File

@ -480,12 +480,6 @@ class TestSetup:
assert call_order == [1, 1, 2] assert call_order == [1, 1, 2]
async def test_component_cannot_depend_config(hass):
"""Test config is not allowed to be a dependency."""
result = await setup._async_process_dependencies(hass, None, "test", ["config"])
assert not result
async def test_component_warn_slow_setup(hass): async def test_component_warn_slow_setup(hass):
"""Warn we log when a component setup takes a long time.""" """Warn we log when a component setup takes a long time."""
mock_integration(hass, MockModule("test_component1")) mock_integration(hass, MockModule("test_component1"))