From 4813b5c88251e1f173cb2ed6d53ec0c309cc46a7 Mon Sep 17 00:00:00 2001 From: Artur Pragacz <49985303+arturpragacz@users.noreply.github.com> Date: Mon, 7 Apr 2025 20:16:48 +0200 Subject: [PATCH] Fix wait for a dependency with config entries (#142318) * Fix wait for dependency with config entries * test types * test coverage --------- Co-authored-by: J. Nick Koston --- homeassistant/setup.py | 37 ++++++++++++---------- tests/test_setup.py | 70 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 16 deletions(-) diff --git a/homeassistant/setup.py b/homeassistant/setup.py index aeaea1146a1..76061b72b73 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -202,16 +202,19 @@ async def _async_process_dependencies( """ setup_futures = hass.data.setdefault(DATA_SETUP, {}) - dependencies_tasks = { - dep: setup_futures.get(dep) - or create_eager_task( - async_setup_component(hass, dep, config), - name=f"setup {dep} as dependency of {integration.domain}", - loop=hass.loop, - ) - for dep in integration.dependencies - if dep not in hass.config.components - } + dependencies_tasks: dict[str, asyncio.Future[bool]] = {} + + for dep in integration.dependencies: + fut = setup_futures.get(dep) + if fut is None: + if dep in hass.config.components: + continue + fut = create_eager_task( + async_setup_component(hass, dep, config), + name=f"setup {dep} as dependency of {integration.domain}", + loop=hass.loop, + ) + dependencies_tasks[dep] = fut to_be_loaded = hass.data.get(DATA_SETUP_DONE, {}) # We don't want to just wait for the futures from `to_be_loaded` here. @@ -219,16 +222,18 @@ async def _async_process_dependencies( # scheduled to be set up, as if for whatever reason they had not been, # we would deadlock waiting for them here. for dep in integration.after_dependencies: - if ( - dep not in dependencies_tasks - and dep in to_be_loaded - and dep not in hass.config.components - ): - dependencies_tasks[dep] = setup_futures.get(dep) or create_eager_task( + if dep not in to_be_loaded or dep in dependencies_tasks: + continue + fut = setup_futures.get(dep) + if fut is None: + if dep in hass.config.components: + continue + fut = create_eager_task( async_setup_component(hass, dep, config), name=f"setup {dep} as after dependency of {integration.domain}", loop=hass.loop, ) + dependencies_tasks[dep] = fut if not dependencies_tasks: return [] diff --git a/tests/test_setup.py b/tests/test_setup.py index 1f0e668d4e2..084b657a2f2 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -353,6 +353,76 @@ async def test_component_not_setup_missing_dependencies(hass: HomeAssistant) -> assert await setup.async_setup_component(hass, "comp2", {}) +async def test_component_not_setup_already_setup_dependencies( + hass: HomeAssistant, +) -> None: + """Test we do not set up component dependencies if they are already set up.""" + mock_integration( + hass, + MockModule( + "comp", + dependencies=["dep1"], + partial_manifest={"after_dependencies": ["dep2"]}, + ), + ) + mock_integration(hass, MockModule("dep1")) + mock_integration(hass, MockModule("dep2")) + + setup.async_set_domains_to_be_loaded(hass, {"comp", "dep2"}) + + hass.config.components.add("dep1") + hass.config.components.add("dep2") + + with patch( + "homeassistant.setup.async_setup_component", + side_effect=setup.async_setup_component, + ) as mock_setup: + await mock_setup(hass, "comp", {}) + + assert mock_setup.call_count == 1 + + +@pytest.mark.usefixtures("mock_handlers") +async def test_component_setup_dependencies_with_config_entry( + hass: HomeAssistant, +) -> None: + """Test we wait for a dependency with config entry.""" + calls: list[str] = [] + + async def mock_async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + await asyncio.sleep(0) + calls.append("entry") + return True + + mock_integration(hass, MockModule("comp", async_setup_entry=mock_async_setup_entry)) + mock_platform(hass, "comp.config_flow", None) + MockConfigEntry(domain="comp").add_to_hass(hass) + + async def mock_async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + calls.append("comp") + return True + + mock_integration( + hass, + MockModule("comp2", dependencies=["comp"], async_setup=mock_async_setup), + ) + mock_integration( + hass, + MockModule("comp3", dependencies=["comp"], async_setup=mock_async_setup), + ) + + await asyncio.gather( + setup.async_setup_component(hass, "comp2", {}), + setup.async_setup_component(hass, "comp3", {}), + ) + + assert "comp" in hass.config.components + assert "comp2" in hass.config.components + assert "comp3" in hass.config.components + + assert calls == ["entry", "comp", "comp"] + + async def test_component_failing_setup(hass: HomeAssistant) -> None: """Test component that fails setup.""" mock_integration(hass, MockModule("comp", setup=lambda hass, config: False))