diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index d33f66538f4f..dd366c524eb6 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -1858,7 +1858,7 @@ class ConfigEntries: await asyncio.gather( *( create_eager_task( - self.async_forward_entry_setup(entry, platform), + self._async_forward_entry_setup(entry, platform, False), name=f"config entry forward setup {entry.title} {entry.domain} {entry.entry_id} {platform}", ) for platform in platforms @@ -1874,6 +1874,12 @@ class ConfigEntries: component also has related platforms, the component will have to forward the entry to be setup by that component. """ + return await self._async_forward_entry_setup(entry, domain, True) + + async def _async_forward_entry_setup( + self, entry: ConfigEntry, domain: Platform | str, preload_platform: bool + ) -> bool: + """Forward the setup of an entry to a different component.""" # Setup Component if not set up yet if domain not in self.hass.config.components: with async_pause_setup(self.hass, SetupPhases.WAIT_BASE_PLATFORM_SETUP): @@ -1884,8 +1890,16 @@ class ConfigEntries: if not result: return False - integration = await loader.async_get_integration(self.hass, domain) + if preload_platform: + # If this is a late setup, we need to make sure the platform is loaded + # so we do not end up waiting for when the EntityComponent calls + # async_prepare_setup_platform + integration = await loader.async_get_integration(self.hass, entry.domain) + if not integration.platforms_are_loaded((domain,)): + with async_pause_setup(self.hass, SetupPhases.WAIT_IMPORT_PLATFORMS): + await integration.async_get_platform(domain) + integration = await loader.async_get_integration(self.hass, domain) await entry.async_setup(self.hass, integration=integration) return True diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 97aa79512f27..07b7b40deab4 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -10,7 +10,6 @@ import contextvars from enum import StrEnum import logging.handlers import time -from timeit import default_timer as timer from types import ModuleType from typing import Any, Final, TypedDict @@ -351,7 +350,6 @@ async def _async_setup_component( # noqa: C901 }, ) - start = timer() _LOGGER.info("Setting up %s", domain) integration_set = {domain} @@ -412,11 +410,8 @@ async def _async_setup_component( # noqa: C901 async_notify_setup_error(hass, domain, integration.documentation) return False finally: - end = timer() if warn_task: warn_task.cancel() - _LOGGER.info("Setup of domain %s took %.1f seconds", domain, end - start) - if result is False: log_error("Integration failed to initialize.") return False @@ -663,6 +658,15 @@ class SetupPhases(StrEnum): """Wait time for the packages to import.""" +def _setup_started( + hass: core.HomeAssistant, +) -> dict[tuple[str, str | None], float]: + """Return the setup started dict.""" + if DATA_SETUP_STARTED not in hass.data: + hass.data[DATA_SETUP_STARTED] = {} + return hass.data[DATA_SETUP_STARTED] # type: ignore[no-any-return] + + @contextlib.contextmanager def async_pause_setup( hass: core.HomeAssistant, phase: SetupPhases @@ -673,7 +677,9 @@ def async_pause_setup( setting up the base components so we can subtract it from the total setup time. """ - if not (running := current_setup_group.get()): + if not (running := current_setup_group.get()) or running not in _setup_started( + hass + ): # This means we are likely in a late platform setup # that is running in a task so we do not want # to subtract out the time later as nothing is waiting @@ -689,6 +695,13 @@ def async_pause_setup( integration, group = running # Add negative time for the time we waited _setup_times(hass)[integration][group][phase] = -time_taken + _LOGGER.debug( + "Adding wait for %s for %s (%s) of %.2f", + phase, + integration, + group, + time_taken, + ) def _setup_times( @@ -726,8 +739,7 @@ def async_start_setup( yield return - setup_started: dict[tuple[str, str | None], float] - setup_started = hass.data.setdefault(DATA_SETUP_STARTED, {}) + setup_started = _setup_started(hass) current = (integration, group) if current in setup_started: # We are already inside another async_start_setup, this like means we @@ -745,7 +757,26 @@ def async_start_setup( finally: time_taken = time.monotonic() - started del setup_started[current] - _setup_times(hass)[integration][group][phase] = time_taken + group_setup_times = _setup_times(hass)[integration][group] + # We may see the phase multiple times if there are multiple + # platforms, but we only care about the longest time. + group_setup_times[phase] = max(group_setup_times[phase], time_taken) + if group is None: + _LOGGER.info( + "Setup of domain %s took %.2f seconds", integration, time_taken + ) + elif _LOGGER.isEnabledFor(logging.DEBUG): + wait_time = -sum(value for value in group_setup_times.values() if value < 0) + calculated_time = time_taken - wait_time + _LOGGER.debug( + "Phase %s for %s (%s) took %.2fs (elapsed=%.2fs) (wait_time=%.2fs)", + phase, + integration, + group, + calculated_time, + time_taken, + wait_time, + ) @callback diff --git a/tests/components/axis/test_hub.py b/tests/components/axis/test_hub.py index b2fc210f5417..f12665a995de 100644 --- a/tests/components/axis/test_hub.py +++ b/tests/components/axis/test_hub.py @@ -35,16 +35,18 @@ from tests.common import async_fire_mqtt_message from tests.typing import MqttMockHAClient -@pytest.fixture(name="forward_entry_setup") +@pytest.fixture(name="forward_entry_setups") def hass_mock_forward_entry_setup(hass): - """Mock async_forward_entry_setup.""" - with patch.object(hass.config_entries, "async_forward_entry_setup") as forward_mock: + """Mock async_forward_entry_setups.""" + with patch.object( + hass.config_entries, "async_forward_entry_setups" + ) as forward_mock: yield forward_mock async def test_device_setup( hass: HomeAssistant, - forward_entry_setup, + forward_entry_setups, config_entry_data, setup_config_entry, device_registry: dr.DeviceRegistry, @@ -57,11 +59,9 @@ async def test_device_setup( assert hub.api.vapix.product_type == "Network Camera" assert hub.api.vapix.serial_number == "00408C123456" - assert len(forward_entry_setup.mock_calls) == 4 - assert forward_entry_setup.mock_calls[0][1][1] == "binary_sensor" - assert forward_entry_setup.mock_calls[1][1][1] == "camera" - assert forward_entry_setup.mock_calls[2][1][1] == "light" - assert forward_entry_setup.mock_calls[3][1][1] == "switch" + assert len(forward_entry_setups.mock_calls) == 1 + platforms = set(forward_entry_setups.mock_calls[0][1][1]) + assert platforms == {"binary_sensor", "camera", "light", "switch"} assert hub.config.host == config_entry_data[CONF_HOST] assert hub.config.model == config_entry_data[CONF_MODEL] diff --git a/tests/components/broadlink/test_device.py b/tests/components/broadlink/test_device.py index 52aad8f4f630..365d61a9e694 100644 --- a/tests/components/broadlink/test_device.py +++ b/tests/components/broadlink/test_device.py @@ -21,7 +21,7 @@ async def test_device_setup(hass: HomeAssistant) -> None: device = get_device("Office") with patch.object( - hass.config_entries, "async_forward_entry_setup" + hass.config_entries, "async_forward_entry_setups" ) as mock_forward, patch.object( hass.config_entries.flow, "async_init" ) as mock_init: @@ -32,9 +32,9 @@ async def test_device_setup(hass: HomeAssistant) -> None: assert mock_setup.api.get_fwversion.call_count == 1 assert mock_setup.factory.call_count == 1 - forward_entries = {c[1][1] for c in mock_forward.mock_calls} + forward_entries = set(mock_forward.mock_calls[0][1][1]) domains = get_domains(mock_setup.api.type) - assert mock_forward.call_count == len(domains) + assert mock_forward.call_count == 1 assert forward_entries == domains assert mock_init.call_count == 0 @@ -46,7 +46,7 @@ async def test_device_setup_authentication_error(hass: HomeAssistant) -> None: mock_api.auth.side_effect = blke.AuthenticationError() with patch.object( - hass.config_entries, "async_forward_entry_setup" + hass.config_entries, "async_forward_entry_setups" ) as mock_forward, patch.object( hass.config_entries.flow, "async_init" ) as mock_init: @@ -70,7 +70,7 @@ async def test_device_setup_network_timeout(hass: HomeAssistant) -> None: mock_api.auth.side_effect = blke.NetworkTimeoutError() with patch.object( - hass.config_entries, "async_forward_entry_setup" + hass.config_entries, "async_forward_entry_setups" ) as mock_forward, patch.object( hass.config_entries.flow, "async_init" ) as mock_init: @@ -89,7 +89,7 @@ async def test_device_setup_os_error(hass: HomeAssistant) -> None: mock_api.auth.side_effect = OSError() with patch.object( - hass.config_entries, "async_forward_entry_setup" + hass.config_entries, "async_forward_entry_setups" ) as mock_forward, patch.object( hass.config_entries.flow, "async_init" ) as mock_init: @@ -108,7 +108,7 @@ async def test_device_setup_broadlink_exception(hass: HomeAssistant) -> None: mock_api.auth.side_effect = blke.BroadlinkException() with patch.object( - hass.config_entries, "async_forward_entry_setup" + hass.config_entries, "async_forward_entry_setups" ) as mock_forward, patch.object( hass.config_entries.flow, "async_init" ) as mock_init: @@ -127,7 +127,7 @@ async def test_device_setup_update_network_timeout(hass: HomeAssistant) -> None: mock_api.check_sensors.side_effect = blke.NetworkTimeoutError() with patch.object( - hass.config_entries, "async_forward_entry_setup" + hass.config_entries, "async_forward_entry_setups" ) as mock_forward, patch.object( hass.config_entries.flow, "async_init" ) as mock_init: @@ -150,7 +150,7 @@ async def test_device_setup_update_authorization_error(hass: HomeAssistant) -> N ) with patch.object( - hass.config_entries, "async_forward_entry_setup" + hass.config_entries, "async_forward_entry_setups" ) as mock_forward, patch.object( hass.config_entries.flow, "async_init" ) as mock_init: @@ -160,9 +160,9 @@ async def test_device_setup_update_authorization_error(hass: HomeAssistant) -> N assert mock_setup.api.auth.call_count == 2 assert mock_setup.api.check_sensors.call_count == 2 - forward_entries = {c[1][1] for c in mock_forward.mock_calls} + forward_entries = set(mock_forward.mock_calls[0][1][1]) domains = get_domains(mock_api.type) - assert mock_forward.call_count == len(domains) + assert mock_forward.call_count == 1 assert forward_entries == domains assert mock_init.call_count == 0 @@ -175,7 +175,7 @@ async def test_device_setup_update_authentication_error(hass: HomeAssistant) -> mock_api.auth.side_effect = (None, blke.AuthenticationError()) with patch.object( - hass.config_entries, "async_forward_entry_setup" + hass.config_entries, "async_forward_entry_setups" ) as mock_forward, patch.object( hass.config_entries.flow, "async_init" ) as mock_init: @@ -200,7 +200,7 @@ async def test_device_setup_update_broadlink_exception(hass: HomeAssistant) -> N mock_api.check_sensors.side_effect = blke.BroadlinkException() with patch.object( - hass.config_entries, "async_forward_entry_setup" + hass.config_entries, "async_forward_entry_setups" ) as mock_forward, patch.object( hass.config_entries.flow, "async_init" ) as mock_init: @@ -221,13 +221,15 @@ async def test_device_setup_get_fwversion_broadlink_exception( mock_api = device.get_mock_api() mock_api.get_fwversion.side_effect = blke.BroadlinkException() - with patch.object(hass.config_entries, "async_forward_entry_setup") as mock_forward: + with patch.object( + hass.config_entries, "async_forward_entry_setups" + ) as mock_forward: mock_setup = await device.setup_entry(hass, mock_api=mock_api) assert mock_setup.entry.state is ConfigEntryState.LOADED - forward_entries = {c[1][1] for c in mock_forward.mock_calls} + forward_entries = set(mock_forward.mock_calls[0][1][1]) domains = get_domains(mock_setup.api.type) - assert mock_forward.call_count == len(domains) + assert mock_forward.call_count == 1 assert forward_entries == domains @@ -237,13 +239,15 @@ async def test_device_setup_get_fwversion_os_error(hass: HomeAssistant) -> None: mock_api = device.get_mock_api() mock_api.get_fwversion.side_effect = OSError() - with patch.object(hass.config_entries, "async_forward_entry_setup") as mock_forward: + with patch.object( + hass.config_entries, "async_forward_entry_setups" + ) as mock_forward: mock_setup = await device.setup_entry(hass, mock_api=mock_api) assert mock_setup.entry.state is ConfigEntryState.LOADED - forward_entries = {c[1][1] for c in mock_forward.mock_calls} + forward_entries = set(mock_forward.mock_calls[0][1][1]) domains = get_domains(mock_setup.api.type) - assert mock_forward.call_count == len(domains) + assert mock_forward.call_count == 1 assert forward_entries == domains @@ -281,7 +285,7 @@ async def test_device_unload_works(hass: HomeAssistant) -> None: """Test we unload the device.""" device = get_device("Office") - with patch.object(hass.config_entries, "async_forward_entry_setup"): + with patch.object(hass.config_entries, "async_forward_entry_setups"): mock_setup = await device.setup_entry(hass) with patch.object( @@ -302,7 +306,7 @@ async def test_device_unload_authentication_error(hass: HomeAssistant) -> None: mock_api = device.get_mock_api() mock_api.auth.side_effect = blke.AuthenticationError() - with patch.object(hass.config_entries, "async_forward_entry_setup"), patch.object( + with patch.object(hass.config_entries, "async_forward_entry_setups"), patch.object( hass.config_entries.flow, "async_init" ): mock_setup = await device.setup_entry(hass, mock_api=mock_api) @@ -322,7 +326,7 @@ async def test_device_unload_update_failed(hass: HomeAssistant) -> None: mock_api = device.get_mock_api() mock_api.check_sensors.side_effect = blke.NetworkTimeoutError() - with patch.object(hass.config_entries, "async_forward_entry_setup"): + with patch.object(hass.config_entries, "async_forward_entry_setups"): mock_setup = await device.setup_entry(hass, mock_api=mock_api) with patch.object( diff --git a/tests/components/deconz/test_gateway.py b/tests/components/deconz/test_gateway.py index d984354adca5..447c1406bf41 100644 --- a/tests/components/deconz/test_gateway.py +++ b/tests/components/deconz/test_gateway.py @@ -142,7 +142,7 @@ async def test_gateway_setup( ) -> None: """Successful setup.""" with patch( - "homeassistant.config_entries.ConfigEntries.async_forward_entry_setup", + "homeassistant.config_entries.ConfigEntries.async_forward_entry_setups", return_value=True, ) as forward_entry_setup: config_entry = await setup_deconz_integration(hass, aioclient_mock) @@ -158,24 +158,23 @@ async def test_gateway_setup( assert forward_entry_setup.mock_calls[0][1] == ( config_entry, - ALARM_CONTROL_PANEL_DOMAIN, + [ + ALARM_CONTROL_PANEL_DOMAIN, + BINARY_SENSOR_DOMAIN, + BUTTON_DOMAIN, + CLIMATE_DOMAIN, + COVER_DOMAIN, + FAN_DOMAIN, + LIGHT_DOMAIN, + LOCK_DOMAIN, + NUMBER_DOMAIN, + SCENE_DOMAIN, + SELECT_DOMAIN, + SENSOR_DOMAIN, + SIREN_DOMAIN, + SWITCH_DOMAIN, + ], ) - assert forward_entry_setup.mock_calls[1][1] == ( - config_entry, - BINARY_SENSOR_DOMAIN, - ) - assert forward_entry_setup.mock_calls[2][1] == (config_entry, BUTTON_DOMAIN) - assert forward_entry_setup.mock_calls[3][1] == (config_entry, CLIMATE_DOMAIN) - assert forward_entry_setup.mock_calls[4][1] == (config_entry, COVER_DOMAIN) - assert forward_entry_setup.mock_calls[5][1] == (config_entry, FAN_DOMAIN) - assert forward_entry_setup.mock_calls[6][1] == (config_entry, LIGHT_DOMAIN) - assert forward_entry_setup.mock_calls[7][1] == (config_entry, LOCK_DOMAIN) - assert forward_entry_setup.mock_calls[8][1] == (config_entry, NUMBER_DOMAIN) - assert forward_entry_setup.mock_calls[9][1] == (config_entry, SCENE_DOMAIN) - assert forward_entry_setup.mock_calls[10][1] == (config_entry, SELECT_DOMAIN) - assert forward_entry_setup.mock_calls[11][1] == (config_entry, SENSOR_DOMAIN) - assert forward_entry_setup.mock_calls[12][1] == (config_entry, SIREN_DOMAIN) - assert forward_entry_setup.mock_calls[13][1] == (config_entry, SWITCH_DOMAIN) gateway_entry = device_registry.async_get_device( identifiers={(DECONZ_DOMAIN, gateway.bridgeid)} diff --git a/tests/components/heos/test_init.py b/tests/components/heos/test_init.py index 4aca77fd90f2..fd453c70ebf4 100644 --- a/tests/components/heos/test_init.py +++ b/tests/components/heos/test_init.py @@ -80,7 +80,9 @@ async def test_async_setup_entry_loads_platforms( ) -> None: """Test load connects to heos, retrieves players, and loads platforms.""" config_entry.add_to_hass(hass) - with patch.object(hass.config_entries, "async_forward_entry_setup") as forward_mock: + with patch.object( + hass.config_entries, "async_forward_entry_setups" + ) as forward_mock: assert await async_setup_entry(hass, config_entry) # Assert platforms loaded await hass.async_block_till_done() @@ -107,7 +109,9 @@ async def test_async_setup_entry_not_signed_in_loads_platforms( config_entry.add_to_hass(hass) controller.is_signed_in = False controller.signed_in_username = None - with patch.object(hass.config_entries, "async_forward_entry_setup") as forward_mock: + with patch.object( + hass.config_entries, "async_forward_entry_setups" + ) as forward_mock: assert await async_setup_entry(hass, config_entry) # Assert platforms loaded await hass.async_block_till_done() diff --git a/tests/components/hue/test_bridge.py b/tests/components/hue/test_bridge.py index 946240116d86..ce1b3f34674e 100644 --- a/tests/components/hue/test_bridge.py +++ b/tests/components/hue/test_bridge.py @@ -30,7 +30,7 @@ async def test_bridge_setup_v1(hass: HomeAssistant, mock_api_v1) -> None: ) with patch.object(bridge, "HueBridgeV1", return_value=mock_api_v1), patch.object( - hass.config_entries, "async_forward_entry_setup" + hass.config_entries, "async_forward_entry_setups" ) as mock_forward: hue_bridge = bridge.HueBridge(hass, config_entry) assert await hue_bridge.async_initialize_bridge() is True @@ -38,8 +38,8 @@ async def test_bridge_setup_v1(hass: HomeAssistant, mock_api_v1) -> None: assert hue_bridge.api is mock_api_v1 assert isinstance(hue_bridge.api, HueBridgeV1) assert hue_bridge.api_version == 1 - assert len(mock_forward.mock_calls) == 3 - forward_entries = {c[1][1] for c in mock_forward.mock_calls} + assert len(mock_forward.mock_calls) == 1 + forward_entries = set(mock_forward.mock_calls[0][1][1]) assert forward_entries == {"light", "binary_sensor", "sensor"} @@ -51,7 +51,7 @@ async def test_bridge_setup_v2(hass: HomeAssistant, mock_api_v2) -> None: ) with patch.object(bridge, "HueBridgeV2", return_value=mock_api_v2), patch.object( - hass.config_entries, "async_forward_entry_setup" + hass.config_entries, "async_forward_entry_setups" ) as mock_forward: hue_bridge = bridge.HueBridge(hass, config_entry) assert await hue_bridge.async_initialize_bridge() is True @@ -59,8 +59,8 @@ async def test_bridge_setup_v2(hass: HomeAssistant, mock_api_v2) -> None: assert hue_bridge.api is mock_api_v2 assert isinstance(hue_bridge.api, HueBridgeV2) assert hue_bridge.api_version == 2 - assert len(mock_forward.mock_calls) == 6 - forward_entries = {c[1][1] for c in mock_forward.mock_calls} + assert len(mock_forward.mock_calls) == 1 + forward_entries = set(mock_forward.mock_calls[0][1][1]) assert forward_entries == { "light", "binary_sensor", @@ -115,7 +115,7 @@ async def test_reset_unloads_entry_if_setup(hass: HomeAssistant, mock_api_v1) -> ) with patch.object(bridge, "HueBridgeV1", return_value=mock_api_v1), patch.object( - hass.config_entries, "async_forward_entry_setup" + hass.config_entries, "async_forward_entry_setups" ) as mock_forward: hue_bridge = bridge.HueBridge(hass, config_entry) assert await hue_bridge.async_initialize_bridge() is True @@ -123,7 +123,7 @@ async def test_reset_unloads_entry_if_setup(hass: HomeAssistant, mock_api_v1) -> await asyncio.sleep(0) assert len(hass.services.async_services()) == 0 - assert len(mock_forward.mock_calls) == 3 + assert len(mock_forward.mock_calls) == 1 with patch.object( hass.config_entries, "async_forward_entry_unload", return_value=True diff --git a/tests/components/hue/test_services.py b/tests/components/hue/test_services.py index 5b91b84a33a2..0e80bb2ea084 100644 --- a/tests/components/hue/test_services.py +++ b/tests/components/hue/test_services.py @@ -63,7 +63,7 @@ async def test_hue_activate_scene(hass: HomeAssistant, mock_api_v1) -> None: mock_api_v1.mock_scene_responses.append(SCENE_RESPONSE) with patch.object(bridge, "HueBridgeV1", return_value=mock_api_v1), patch.object( - hass.config_entries, "async_forward_entry_setup" + hass.config_entries, "async_forward_entry_setups" ): hue_bridge = bridge.HueBridge(hass, config_entry) assert await hue_bridge.async_initialize_bridge() is True @@ -100,7 +100,7 @@ async def test_hue_activate_scene_transition(hass: HomeAssistant, mock_api_v1) - mock_api_v1.mock_scene_responses.append(SCENE_RESPONSE) with patch.object(bridge, "HueBridgeV1", return_value=mock_api_v1), patch.object( - hass.config_entries, "async_forward_entry_setup" + hass.config_entries, "async_forward_entry_setups" ): hue_bridge = bridge.HueBridge(hass, config_entry) assert await hue_bridge.async_initialize_bridge() is True @@ -139,7 +139,7 @@ async def test_hue_activate_scene_group_not_found( mock_api_v1.mock_scene_responses.append(SCENE_RESPONSE) with patch.object(bridge, "HueBridgeV1", return_value=mock_api_v1), patch.object( - hass.config_entries, "async_forward_entry_setup" + hass.config_entries, "async_forward_entry_setups" ): hue_bridge = bridge.HueBridge(hass, config_entry) assert await hue_bridge.async_initialize_bridge() is True @@ -173,7 +173,7 @@ async def test_hue_activate_scene_scene_not_found( mock_api_v1.mock_scene_responses.append({}) with patch.object(bridge, "HueBridgeV1", return_value=mock_api_v1), patch.object( - hass.config_entries, "async_forward_entry_setup" + hass.config_entries, "async_forward_entry_setups" ): hue_bridge = bridge.HueBridge(hass, config_entry) assert await hue_bridge.async_initialize_bridge() is True diff --git a/tests/components/minio/test_minio.py b/tests/components/minio/test_minio.py index 746085fafc27..74db0a2fcf98 100644 --- a/tests/components/minio/test_minio.py +++ b/tests/components/minio/test_minio.py @@ -76,8 +76,6 @@ async def test_minio_services( await hass.async_start() await hass.async_block_till_done() - assert "Setup of domain minio took" in caplog.text - # Call services await hass.services.async_call( DOMAIN, @@ -141,8 +139,6 @@ async def test_minio_listen( await hass.async_start() await hass.async_block_till_done() - assert "Setup of domain minio took" in caplog.text - while not events: await asyncio.sleep(0) diff --git a/tests/components/owntracks/test_config_flow.py b/tests/components/owntracks/test_config_flow.py index 038609271f4d..d8e700c37544 100644 --- a/tests/components/owntracks/test_config_flow.py +++ b/tests/components/owntracks/test_config_flow.py @@ -126,7 +126,7 @@ async def test_unload(hass: HomeAssistant) -> None: ) with patch( - "homeassistant.config_entries.ConfigEntries.async_forward_entry_setup" + "homeassistant.config_entries.ConfigEntries.async_forward_entry_setups" ) as mock_forward: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data={} @@ -135,8 +135,7 @@ async def test_unload(hass: HomeAssistant) -> None: assert len(mock_forward.mock_calls) == 1 entry = result["result"] - assert mock_forward.mock_calls[0][1][0] is entry - assert mock_forward.mock_calls[0][1][1] == "device_tracker" + mock_forward.assert_called_once_with(entry, ["device_tracker"]) assert entry.data["webhook_id"] in hass.data["webhook"] with patch( @@ -146,8 +145,7 @@ async def test_unload(hass: HomeAssistant) -> None: assert await hass.config_entries.async_unload(entry.entry_id) assert len(mock_unload.mock_calls) == 1 - assert mock_forward.mock_calls[0][1][0] is entry - assert mock_forward.mock_calls[0][1][1] == "device_tracker" + mock_forward.assert_called_once_with(entry, ["device_tracker"]) assert entry.data["webhook_id"] not in hass.data["webhook"] diff --git a/tests/components/smartthings/test_init.py b/tests/components/smartthings/test_init.py index f960ba15e859..dcce19a68d17 100644 --- a/tests/components/smartthings/test_init.py +++ b/tests/components/smartthings/test_init.py @@ -179,11 +179,13 @@ async def test_scenes_unauthorized_loads_platforms( ] smartthings_mock.subscriptions.return_value = subscriptions - with patch.object(hass.config_entries, "async_forward_entry_setup") as forward_mock: + with patch.object( + hass.config_entries, "async_forward_entry_setups" + ) as forward_mock: assert await hass.config_entries.async_setup(config_entry.entry_id) # Assert platforms loaded await hass.async_block_till_done() - assert forward_mock.call_count == len(PLATFORMS) + forward_mock.assert_called_once_with(config_entry, PLATFORMS) async def test_config_entry_loads_platforms( @@ -211,11 +213,13 @@ async def test_config_entry_loads_platforms( ] smartthings_mock.subscriptions.return_value = subscriptions - with patch.object(hass.config_entries, "async_forward_entry_setup") as forward_mock: + with patch.object( + hass.config_entries, "async_forward_entry_setups" + ) as forward_mock: assert await hass.config_entries.async_setup(config_entry.entry_id) # Assert platforms loaded await hass.async_block_till_done() - assert forward_mock.call_count == len(PLATFORMS) + forward_mock.assert_called_once_with(config_entry, PLATFORMS) async def test_config_entry_loads_unconnected_cloud( @@ -243,10 +247,12 @@ async def test_config_entry_loads_unconnected_cloud( subscription_factory(capability) for capability in device.capabilities ] smartthings_mock.subscriptions.return_value = subscriptions - with patch.object(hass.config_entries, "async_forward_entry_setup") as forward_mock: + with patch.object( + hass.config_entries, "async_forward_entry_setups" + ) as forward_mock: assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert forward_mock.call_count == len(PLATFORMS) + forward_mock.assert_called_once_with(config_entry, PLATFORMS) async def test_unload_entry(hass: HomeAssistant, config_entry) -> None: diff --git a/tests/components/tts/test_legacy.py b/tests/components/tts/test_legacy.py index 6fe6b0b790ae..59194f50d938 100644 --- a/tests/components/tts/test_legacy.py +++ b/tests/components/tts/test_legacy.py @@ -148,6 +148,7 @@ async def test_service_without_cache_config( with assert_setup_component(1, DOMAIN): assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() await hass.services.async_call( DOMAIN, diff --git a/tests/components/unifi/test_hub.py b/tests/components/unifi/test_hub.py index 6dce21097715..48aca0b407e7 100644 --- a/tests/components/unifi/test_hub.py +++ b/tests/components/unifi/test_hub.py @@ -24,11 +24,11 @@ from homeassistant.components.unifi.const import ( DEFAULT_TRACK_DEVICES, DEFAULT_TRACK_WIRED_CLIENTS, DOMAIN as UNIFI_DOMAIN, - PLATFORMS, UNIFI_WIRELESS_CLIENTS, ) from homeassistant.components.unifi.errors import AuthenticationRequired, CannotConnect from homeassistant.components.unifi.hub import get_unifi_api +from homeassistant.components.update import DOMAIN as UPDATE_DOMAIN from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, @@ -248,7 +248,7 @@ async def test_hub_setup( ) -> None: """Successful setup.""" with patch( - "homeassistant.config_entries.ConfigEntries.async_forward_entry_setup", + "homeassistant.config_entries.ConfigEntries.async_forward_entry_setups", return_value=True, ) as forward_entry_setup: config_entry = await setup_unifi_integration( @@ -257,12 +257,18 @@ async def test_hub_setup( hub = hass.data[UNIFI_DOMAIN][config_entry.entry_id] entry = hub.config.entry - assert len(forward_entry_setup.mock_calls) == len(PLATFORMS) - assert forward_entry_setup.mock_calls[0][1] == (entry, BUTTON_DOMAIN) - assert forward_entry_setup.mock_calls[1][1] == (entry, TRACKER_DOMAIN) - assert forward_entry_setup.mock_calls[2][1] == (entry, IMAGE_DOMAIN) - assert forward_entry_setup.mock_calls[3][1] == (entry, SENSOR_DOMAIN) - assert forward_entry_setup.mock_calls[4][1] == (entry, SWITCH_DOMAIN) + assert len(forward_entry_setup.mock_calls) == 1 + assert forward_entry_setup.mock_calls[0][1] == ( + entry, + [ + BUTTON_DOMAIN, + TRACKER_DOMAIN, + IMAGE_DOMAIN, + SENSOR_DOMAIN, + SWITCH_DOMAIN, + UPDATE_DOMAIN, + ], + ) assert hub.config.host == ENTRY_CONFIG[CONF_HOST] assert hub.is_admin == (SITE[0]["role"] == "admin") diff --git a/tests/components/vesync/test_init.py b/tests/components/vesync/test_init.py index 005beab45032..fedd7f70a93d 100644 --- a/tests/components/vesync/test_init.py +++ b/tests/components/vesync/test_init.py @@ -30,15 +30,12 @@ async def test_async_setup_entry__not_login( with patch.object( hass.config_entries, "async_forward_entry_setups" - ) as setups_mock, patch.object( - hass.config_entries, "async_forward_entry_setup" - ) as setup_mock, patch( + ) as setups_mock, patch( "homeassistant.components.vesync.async_process_devices" ) as process_mock: assert not await async_setup_entry(hass, config_entry) await hass.async_block_till_done() assert setups_mock.call_count == 0 - assert setup_mock.call_count == 0 assert process_mock.call_count == 0 assert manager.login.call_count == 1 @@ -50,18 +47,13 @@ async def test_async_setup_entry__no_devices( hass: HomeAssistant, config_entry: ConfigEntry, manager: VeSync ) -> None: """Test setup connects to vesync and creates empty config when no devices.""" - with patch.object( - hass.config_entries, "async_forward_entry_setups" - ) as setups_mock, patch.object( - hass.config_entries, "async_forward_entry_setup" - ) as setup_mock: + with patch.object(hass.config_entries, "async_forward_entry_setups") as setups_mock: assert await async_setup_entry(hass, config_entry) # Assert platforms loaded await hass.async_block_till_done() assert setups_mock.call_count == 1 assert setups_mock.call_args.args[0] == config_entry assert setups_mock.call_args.args[1] == [] - assert setup_mock.call_count == 0 assert manager.login.call_count == 1 assert hass.data[DOMAIN][VS_MANAGER] == manager @@ -81,18 +73,13 @@ async def test_async_setup_entry__loads_fans( "fans": fans, } - with patch.object( - hass.config_entries, "async_forward_entry_setups" - ) as setups_mock, patch.object( - hass.config_entries, "async_forward_entry_setup" - ) as setup_mock: + with patch.object(hass.config_entries, "async_forward_entry_setups") as setups_mock: assert await async_setup_entry(hass, config_entry) # Assert platforms loaded await hass.async_block_till_done() assert setups_mock.call_count == 1 assert setups_mock.call_args.args[0] == config_entry assert setups_mock.call_args.args[1] == [Platform.FAN, Platform.SENSOR] - assert setup_mock.call_count == 0 assert manager.login.call_count == 1 assert hass.data[DOMAIN][VS_MANAGER] == manager assert not hass.data[DOMAIN][VS_SWITCHES] diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 544396c237a5..da100d8cfb08 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -859,7 +859,7 @@ async def test_forward_entry_sets_up_component(hass: HomeAssistant) -> None: entry = MockConfigEntry(domain="original") mock_original_setup_entry = AsyncMock(return_value=True) - mock_integration( + integration = mock_integration( hass, MockModule("original", async_setup_entry=mock_original_setup_entry) ) @@ -868,7 +868,10 @@ async def test_forward_entry_sets_up_component(hass: HomeAssistant) -> None: hass, MockModule("forwarded", async_setup_entry=mock_forwarded_setup_entry) ) - await hass.config_entries.async_forward_entry_setup(entry, "forwarded") + with patch.object(integration, "async_get_platform") as mock_async_get_platform: + await hass.config_entries.async_forward_entry_setup(entry, "forwarded") + + mock_async_get_platform.assert_called_once_with("forwarded") assert len(mock_original_setup_entry.mock_calls) == 0 assert len(mock_forwarded_setup_entry.mock_calls) == 1 diff --git a/tests/test_setup.py b/tests/test_setup.py index 28366a50a823..b48e7252e65b 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -4,6 +4,7 @@ import asyncio import threading from unittest.mock import ANY, AsyncMock, Mock, patch +from freezegun.api import FrozenDateTimeFactory import pytest import voluptuous as vol @@ -16,6 +17,10 @@ from homeassistant.helpers.config_validation import ( PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ) +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from .common import ( MockConfigEntry, @@ -739,7 +744,9 @@ async def test_async_start_setup_running(hass: HomeAssistant) -> None: assert not setup_started -async def test_async_start_setup_config_entry(hass: HomeAssistant) -> None: +async def test_async_start_setup_config_entry( + hass: HomeAssistant, freezer: FrozenDateTimeFactory +) -> None: """Test setup started keeps track of setup times with a config entry.""" hass.set_state(CoreState.not_running) setup_started: dict[tuple[str, str | None], float] @@ -778,6 +785,7 @@ async def test_async_start_setup_config_entry(hass: HomeAssistant) -> None: phase=setup.SetupPhases.CONFIG_ENTRY_PLATFORM_SETUP, ): assert isinstance(setup_started[("august", "entry_id")], float) + # Platforms outside of CONFIG_ENTRY_SETUP should be tracked # This simulates a late platform forward assert setup_time["august"] == { @@ -788,6 +796,38 @@ async def test_async_start_setup_config_entry(hass: HomeAssistant) -> None: }, } + shorter_time = setup_time["august"]["entry_id"][ + setup.SetupPhases.CONFIG_ENTRY_PLATFORM_SETUP + ] + # Setup another platform, but make it take longer + with setup.async_start_setup( + hass, + integration="august", + group="entry_id", + phase=setup.SetupPhases.CONFIG_ENTRY_PLATFORM_SETUP, + ): + freezer.tick(10) + assert isinstance(setup_started[("august", "entry_id")], float) + + longer_time = setup_time["august"]["entry_id"][ + setup.SetupPhases.CONFIG_ENTRY_PLATFORM_SETUP + ] + assert longer_time > shorter_time + # Setup another platform, but make it take shorter + with setup.async_start_setup( + hass, + integration="august", + group="entry_id", + phase=setup.SetupPhases.CONFIG_ENTRY_PLATFORM_SETUP, + ): + assert isinstance(setup_started[("august", "entry_id")], float) + + # Ensure we keep the longest time + assert ( + setup_time["august"]["entry_id"][setup.SetupPhases.CONFIG_ENTRY_PLATFORM_SETUP] + == longer_time + ) + with setup.async_start_setup( hass, integration="august", @@ -815,6 +855,106 @@ async def test_async_start_setup_config_entry(hass: HomeAssistant) -> None: } +async def test_async_start_setup_config_entry_late_platform( + hass: HomeAssistant, freezer: FrozenDateTimeFactory +) -> None: + """Test setup started tracks config entry time with a late platform load.""" + hass.set_state(CoreState.not_running) + setup_started: dict[tuple[str, str | None], float] + setup_started = hass.data.setdefault(setup.DATA_SETUP_STARTED, {}) + setup_time = setup._setup_times(hass) + + with setup.async_start_setup( + hass, integration="august", phase=setup.SetupPhases.SETUP + ): + freezer.tick(10) + assert isinstance(setup_started[("august", None)], float) + + with setup.async_start_setup( + hass, + integration="august", + group="entry_id", + phase=setup.SetupPhases.CONFIG_ENTRY_SETUP, + ): + assert isinstance(setup_started[("august", "entry_id")], float) + + @callback + def async_late_platform_load(): + with setup.async_pause_setup(hass, setup.SetupPhases.WAIT_IMPORT_PLATFORMS): + freezer.tick(100) + with setup.async_start_setup( + hass, + integration="august", + group="entry_id", + phase=setup.SetupPhases.CONFIG_ENTRY_PLATFORM_SETUP, + ): + freezer.tick(20) + assert isinstance(setup_started[("august", "entry_id")], float) + + disconnect = async_dispatcher_connect( + hass, "late_platform_load_test", async_late_platform_load + ) + + # Dispatch a late platform load + async_dispatcher_send(hass, "late_platform_load_test") + disconnect() + + # CONFIG_ENTRY_PLATFORM_SETUP is late dispatched, so it should be tracked + # but any waiting time should not be because it's blocking the setup + assert setup_time["august"] == { + None: {setup.SetupPhases.SETUP: 10.0}, + "entry_id": { + setup.SetupPhases.CONFIG_ENTRY_PLATFORM_SETUP: 20.0, + setup.SetupPhases.CONFIG_ENTRY_SETUP: 0.0, + }, + } + + +async def test_async_start_setup_config_entry_platform_wait( + hass: HomeAssistant, freezer: FrozenDateTimeFactory +) -> None: + """Test setup started tracks wait time when a platform loads inside of config entry setup.""" + hass.set_state(CoreState.not_running) + setup_started: dict[tuple[str, str | None], float] + setup_started = hass.data.setdefault(setup.DATA_SETUP_STARTED, {}) + setup_time = setup._setup_times(hass) + + with setup.async_start_setup( + hass, integration="august", phase=setup.SetupPhases.SETUP + ): + freezer.tick(10) + assert isinstance(setup_started[("august", None)], float) + + with setup.async_start_setup( + hass, + integration="august", + group="entry_id", + phase=setup.SetupPhases.CONFIG_ENTRY_SETUP, + ): + assert isinstance(setup_started[("august", "entry_id")], float) + + with setup.async_pause_setup(hass, setup.SetupPhases.WAIT_IMPORT_PLATFORMS): + freezer.tick(100) + with setup.async_start_setup( + hass, + integration="august", + group="entry_id", + phase=setup.SetupPhases.CONFIG_ENTRY_PLATFORM_SETUP, + ): + freezer.tick(20) + assert isinstance(setup_started[("august", "entry_id")], float) + + # CONFIG_ENTRY_PLATFORM_SETUP is run inside of CONFIG_ENTRY_SETUP, so it should not + # be tracked, but any wait time should still be tracked because its blocking the setup + assert setup_time["august"] == { + None: {setup.SetupPhases.SETUP: 10.0}, + "entry_id": { + setup.SetupPhases.WAIT_IMPORT_PLATFORMS: -100.0, + setup.SetupPhases.CONFIG_ENTRY_SETUP: 120.0, + }, + } + + async def test_async_start_setup_top_level_yaml(hass: HomeAssistant) -> None: """Test setup started context manager keeps track of setup times with modern yaml.""" hass.set_state(CoreState.not_running)