From 5642027ffb8777ab807f3a3c6cbbdf0ec60d5108 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 19 Jun 2020 17:24:33 -0700 Subject: [PATCH] Improve after_dependencies handling (#36898) --- homeassistant/bootstrap.py | 225 +++++++++++--------- homeassistant/components/auth/manifest.json | 1 - homeassistant/loader.py | 84 ++++++-- homeassistant/setup.py | 62 +++--- script/hassfest/dependencies.py | 8 + tests/common.py | 2 + tests/test_bootstrap.py | 121 +++++++++-- tests/test_loader.py | 34 ++- tests/test_setup.py | 6 - 9 files changed, 377 insertions(+), 166 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 90372c5e3ba..a73cebce085 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -1,6 +1,7 @@ """Provide methods to bootstrap a Home Assistant instance.""" import asyncio import contextlib +from datetime import datetime import logging import logging.handlers import os @@ -20,7 +21,12 @@ from homeassistant.const import ( ) from homeassistant.exceptions import HomeAssistantError 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.package import async_get_user_site, is_virtual_env from homeassistant.util.yaml import clear_secret_cache @@ -36,10 +42,16 @@ LOG_SLOW_STARTUP_INTERVAL = 60 DEBUGGER_INTEGRATIONS = {"ptvsd"} CORE_INTEGRATIONS = ("homeassistant", "persistent_notification") -LOGGING_INTEGRATIONS = {"logger", "system_log", "sentry"} -STAGE_1_INTEGRATIONS = { +LOGGING_INTEGRATIONS = { + # Set log levels + "logger", + # Error logging + "system_log", + "sentry", # To record data "recorder", +} +STAGE_1_INTEGRATIONS = { # To make sure we forward data to other instances "mqtt_eventstream", # To provide account link implementations @@ -330,77 +342,130 @@ def _get_domains(hass: core.HomeAssistant, config: Dict[str, Any]) -> Set[str]: 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( hass: core.HomeAssistant, config: Dict[str, Any] ) -> None: """Set up all the integrations.""" - setup_started = hass.data[DATA_SETUP_STARTED] = {} + domains_to_setup = _get_domains(hass, config) - async def async_setup_multi_components(domains: Set[str]) -> None: - """Set up multiple domains. Log on failure.""" + # Resolve all dependencies so we know all integrations + # 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: - """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), - ) - - 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__), + integrations_to_process = [ + int_or_exc + for int_or_exc in await asyncio.gather( + *( + loader.async_get_integration(hass, domain) + for domain in old_to_resolve + ), + return_exceptions=True, ) + 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. - debuggers = domains & DEBUGGER_INTEGRATIONS + debuggers = domains_to_setup & DEBUGGER_INTEGRATIONS + if debuggers: - _LOGGER.debug("Starting up debuggers %s", debuggers) - await async_setup_multi_components(debuggers) - domains -= DEBUGGER_INTEGRATIONS + _LOGGER.debug("Setting up debuggers: %s", debuggers) + await async_setup_multi_components(hass, debuggers, config, setup_started) - # Resolve all dependencies of all components so we can find the logging - # and integrations that need faster initialization. - resolved_domains_task = asyncio.gather( - *(loader.async_component_dependencies(hass, domain) for domain in domains), - return_exceptions=True, - ) + # calculate what components to setup in what stage + stage_1_domains = set() - # Finish resolving domains - for dep_domains in await resolved_domains_task: - # Result is either a set or an exception. We ignore exceptions - # It will be properly handled during setup of the domain. - if isinstance(dep_domains, set): - domains.update(dep_domains) + # Find all dependencies of any dependency of any stage 1 integration that + # we plan on loading and promote them to stage 1 + deps_promotion = STAGE_1_INTEGRATIONS + while deps_promotion: + old_deps_promotion = deps_promotion + deps_promotion = set() - # setup components - logging_domains = domains & LOGGING_INTEGRATIONS - stage_1_domains = domains & STAGE_1_INTEGRATIONS - stage_2_domains = domains - logging_domains - stage_1_domains + for domain in old_deps_promotion: + if domain not in domains_to_setup or domain in stage_1_domains: + continue - if logging_domains: - _LOGGER.info("Setting up %s", logging_domains) + stage_1_domains.add(domain) - 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. asyncio.gather( @@ -409,49 +474,17 @@ async def _async_set_up_integrations( hass.helpers.area_registry.async_get_registry(), ) + # Start setup 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: - _LOGGER.debug("Final set up: %s", stage_2_domains) - - await async_setup_multi_components(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) # Wrap up startup _LOGGER.debug("Waiting for startup to wrap up") diff --git a/homeassistant/components/auth/manifest.json b/homeassistant/components/auth/manifest.json index b8c711c1dda..2674bdfb032 100644 --- a/homeassistant/components/auth/manifest.json +++ b/homeassistant/components/auth/manifest.json @@ -3,7 +3,6 @@ "name": "Auth", "documentation": "https://www.home-assistant.io/integrations/auth", "dependencies": ["http"], - "after_dependencies": ["onboarding"], "codeowners": ["@home-assistant/core"], "quality_scale": "internal" } diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 46db5c232d8..315165bf27f 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -203,6 +203,14 @@ class Integration: self.file_path = file_path self.manifest = manifest 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) @property @@ -255,6 +263,49 @@ class Integration: """Test if package is a built-in integration.""" 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: """Return the component.""" cache = self.hass.data.setdefault(DATA_COMPONENTS, {}) @@ -488,23 +539,18 @@ def bind_hass(func: CALLABLE_T) -> CALLABLE_T: 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( - 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]: """Recursive function to get component dependencies. Async friendly. """ - integration = await async_get_integration(hass, domain) - + domain = integration.domain loading.add(domain) for dependency_domain in integration.dependencies: @@ -516,11 +562,19 @@ async def _async_component_dependencies( if dependency_domain in loading: raise CircularDependency(domain, dependency_domain) - dep_loaded = await _async_component_dependencies( - hass, dependency_domain, loaded, loading - ) + loaded.add(dependency_domain) - 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) loading.remove(domain) diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 8ea249756f3..fbacf672e1f 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -3,7 +3,7 @@ import asyncio import logging.handlers from timeit import default_timer as timer 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.config import async_notify_setup_error @@ -16,6 +16,7 @@ _LOGGER = logging.getLogger(__name__) ATTR_COMPONENT = "component" +DATA_SETUP_DONE = "setup_done" DATA_SETUP_STARTED = "setup_started" DATA_SETUP = "setup_tasks" DATA_DEPS_REQS = "deps_reqs_processed" @@ -26,6 +27,15 @@ SLOW_SETUP_WARNING = 10 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: """Set up a component and all its dependencies.""" return asyncio.run_coroutine_threadsafe( @@ -52,26 +62,43 @@ async def async_setup_component( _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( - hass: core.HomeAssistant, config: ConfigType, name: str, dependencies: List[str] + hass: core.HomeAssistant, config: ConfigType, integration: loader.Integration ) -> bool: """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: 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: _LOGGER.error( "Unable to set up dependencies of %s. Setup failed for dependencies: %s", - name, + integration.domain, ", ".join(failed), ) @@ -99,22 +126,7 @@ async def _async_setup_component( return False # Validate all dependencies exist and there are no circular dependencies - try: - 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, - ) + if not await integration.resolve_dependencies(): return False # 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: return - if integration.dependencies and not await _async_process_dependencies( - hass, config, integration.domain, integration.dependencies - ): + if not await _async_process_dependencies(hass, config, integration): raise HomeAssistantError("Could not set up all dependencies.") if not hass.config.skip_pip and integration.requirements: diff --git a/script/hassfest/dependencies.py b/script/hassfest/dependencies.py index ba9e971d02e..2335270dc4a 100644 --- a/script/hassfest/dependencies.py +++ b/script/hassfest/dependencies.py @@ -103,6 +103,7 @@ ALLOWED_USED_COMPONENTS = { "input_number", "input_select", "input_text", + "onboarding", "persistent_notification", "person", "script", @@ -253,7 +254,14 @@ def validate(integrations: Dict[str, Integration], config): continue # check that all referenced dependencies exist + after_deps = integration.manifest.get("after_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: integration.add_error( "dependencies", f"Dependency {dep} does not exist" diff --git a/tests/common.py b/tests/common.py index 2136de3584f..4e457496dee 100644 --- a/tests/common.py +++ b/tests/common.py @@ -991,6 +991,8 @@ def mock_integration(hass, module): hass.data.setdefault(loader.DATA_INTEGRATIONS, {})[module.DOMAIN] = integration hass.data.setdefault(loader.DATA_COMPONENTS, {})[module.DOMAIN] = module + return integration + def mock_entity_platform(hass, platform_path, module): """Mock a entity platform. diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index 6ff145786d2..f08ed5746b5 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -7,7 +7,7 @@ from unittest.mock import Mock import pytest -from homeassistant import bootstrap +from homeassistant import bootstrap, core import homeassistant.config as config_util from homeassistant.exceptions import HomeAssistantError import homeassistant.util.dt as dt_util @@ -16,9 +16,11 @@ from tests.async_mock import patch from tests.common import ( MockConfigEntry, MockModule, + MockPlatform, flush_store, get_test_config_dir, mock_coro, + mock_entity_platform, mock_integration, ) @@ -81,7 +83,7 @@ async def test_core_failure_loads_safe_mode(hass, caplog): 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.""" await bootstrap._async_set_up_integrations( hass, {"group hello": {}, "homeassistant": {}} @@ -90,9 +92,8 @@ async def test_setting_up_config(hass, caplog): 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.""" - caplog.set_level(logging.DEBUG) order = [] 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( - hass, {"root": {}, "first_dep": {}, "second_dep": {}} - ) + with patch( + "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 "first_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.""" - caplog.set_level(logging.DEBUG) order = [] 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 "first_dep" not 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.""" - caplog.set_level(logging.DEBUG) order = [] def gen_domain_setup(domain): diff --git a/tests/test_loader.py b/tests/test_loader.py index eb99cb3a8ea..20669588180 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -13,27 +13,43 @@ async def test_component_dependencies(hass): """Test if we can get the proper load order of components.""" mock_integration(hass, MockModule("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( - hass, "mod3" + assert {"mod1", "mod2", "mod3"} == await loader._async_component_dependencies( + hass, "mod_3", mod_3, set(), set() ) # Create circular dependency mock_integration(hass, MockModule("mod1", ["mod3"])) 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 - mock_integration(hass, MockModule("mod1", ["nonexisting"])) + mod_1 = mock_integration(hass, MockModule("mod1", ["nonexisting"])) 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 - with pytest.raises(loader.IntegrationNotFound): - print(await loader.async_component_dependencies(hass, "nonexisting")) + # Having an after dependency 2 deps down that is circular + mod_1 = mock_integration( + 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): diff --git a/tests/test_setup.py b/tests/test_setup.py index 4ff380d0cc8..cb63f8fa865 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -480,12 +480,6 @@ class TestSetup: 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): """Warn we log when a component setup takes a long time.""" mock_integration(hass, MockModule("test_component1"))