Fix setup timings when config entry platform loads are not awaited (#113959)

* Move setup time logging into the context manager

We were fetching the time twice but since the context
manager already has the timing, move it there

* remove log setup assertions from integration test

* tweak logging to give us better data for tracking issues

* redundant

* adjust

* preen

* fixes

* adjust

* make api change internal so nobody uses it

* coverage

* fix test

* fix more tests

* coverage

* more tests assuming internal calls

* fix more

* adjust

* adjust

* fix axis tests

* fix broadlink -- it does not call async_forward_entry_setup

* missed some

* remove useless patch

* rename, detect it both ways

* clear

* debug

* try to fix

* handle phase finishing out while paused

* where its set does not need to know its late as that is an implemenation detail of setup

* where its set does not need to know its late as that is an implemenation detail of setup

* tweak

* simplify

* reduce complexity

* revert order change as it makes review harder

* revert naming changes as it makes review harder

* improve comment

* improve debug

* late dispatch test

* test the other way as well

* Update setup.py

* Update setup.py

* Update setup.py

* simplify

* reduce
This commit is contained in:
J. Nick Koston 2024-03-23 09:26:38 -10:00 committed by GitHub
parent a4f52cc622
commit 4f18f0d902
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 305 additions and 116 deletions

View File

@ -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

View File

@ -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

View File

@ -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]

View File

@ -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(

View File

@ -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)}

View File

@ -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()

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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"]

View File

@ -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:

View File

@ -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,

View File

@ -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")

View File

@ -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]

View File

@ -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

View File

@ -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)