ha-core/tests/test_bootstrap.py

1070 lines
33 KiB
Python
Raw Normal View History

2016-03-09 10:25:50 +01:00
"""Test the bootstrapping."""
import asyncio
from collections.abc import Generator, Iterable
import glob
import os
import sys
2023-03-16 11:08:47 +01:00
from typing import Any
from unittest.mock import AsyncMock, Mock, patch
import pytest
2015-08-11 17:20:13 +02:00
from homeassistant import bootstrap, loader, runner
import homeassistant.config as config_util
from homeassistant.config_entries import HANDLERS, ConfigEntry
from homeassistant.const import SIGNAL_BOOTSTRAP_INTEGRATIONS
from homeassistant.core import HomeAssistant, async_get_hass, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import Integration
2015-08-11 17:20:13 +02:00
from .common import (
MockConfigEntry,
MockModule,
MockPlatform,
2019-07-31 21:25:30 +02:00
get_test_config_dir,
mock_integration,
mock_platform,
2019-07-31 21:25:30 +02:00
)
2016-02-15 00:08:23 +01:00
VERSION_PATH = os.path.join(get_test_config_dir(), config_util.VERSION_FILE)
2015-08-11 17:20:13 +02:00
2016-03-30 07:50:38 +02:00
@pytest.fixture(autouse=True)
2023-03-16 11:08:47 +01:00
def apply_mock_storage(hass_storage: dict[str, Any]) -> None:
"""Apply the storage mock."""
@pytest.fixture(autouse=True)
async def apply_stop_hass(stop_hass: None) -> None:
"""Make sure all hass are stopped."""
@pytest.fixture(scope="session", autouse=True)
2023-03-16 11:08:47 +01:00
def mock_http_start_stop() -> Generator[None, None, None]:
"""Mock HTTP start and stop."""
with patch(
"homeassistant.components.http.start_http_server_and_save_config"
), patch("homeassistant.components.http.HomeAssistantHTTP.stop"):
yield
2019-07-31 21:25:30 +02:00
@patch("homeassistant.bootstrap.async_enable_logging", Mock())
async def test_home_assistant_core_config_validation(hass: HomeAssistant) -> None:
"""Test if we pass in wrong information for HA conf."""
# Extensive HA conf validation testing is done
result = await bootstrap.async_from_config_dict(
2019-07-31 21:25:30 +02:00
{"homeassistant": {"latitude": "some string"}}, hass
)
assert result is None
async def test_async_enable_logging(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test to ensure logging is migrated to the queue handlers."""
with patch("logging.getLogger"), patch(
"homeassistant.bootstrap.async_activate_log_queue_handler"
2021-09-03 18:53:47 +02:00
) as mock_async_activate_log_queue_handler, patch(
"homeassistant.bootstrap.logging.handlers.RotatingFileHandler.doRollover",
side_effect=OSError,
):
bootstrap.async_enable_logging(hass)
mock_async_activate_log_queue_handler.assert_called_once()
mock_async_activate_log_queue_handler.reset_mock()
bootstrap.async_enable_logging(
hass,
log_rotate_days=5,
log_file="test.log",
)
mock_async_activate_log_queue_handler.assert_called_once()
for f in glob.glob("test.log*"):
os.remove(f)
for f in glob.glob("testing_config/home-assistant.log*"):
os.remove(f)
2021-09-03 18:53:47 +02:00
assert "Error rolling over log file" in caplog.text
async def test_load_hassio(hass: HomeAssistant) -> None:
"""Test that we load the hassio integration when using Supervisor."""
with patch.dict(os.environ, {}, clear=True):
assert "hassio" not in bootstrap._get_domains(hass, {})
with patch.dict(os.environ, {"SUPERVISOR": "1"}):
assert "hassio" in bootstrap._get_domains(hass, {})
@pytest.mark.parametrize("load_registries", [False])
async def test_empty_setup(hass: HomeAssistant) -> None:
"""Test an empty set up loads the core."""
await bootstrap.async_from_config_dict({}, hass)
for domain in bootstrap.CORE_INTEGRATIONS:
assert domain in hass.config.components, domain
async def test_core_failure_loads_recovery_mode(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test failing core setup aborts further setup."""
2019-07-31 21:25:30 +02:00
with patch(
"homeassistant.components.homeassistant.async_setup",
return_value=False,
2019-07-31 21:25:30 +02:00
):
await bootstrap.async_from_config_dict({"group": {}}, hass)
2019-07-31 21:25:30 +02:00
assert "core failed to initialize" in caplog.text
# We aborted early, group not set up
2019-07-31 21:25:30 +02:00
assert "group" not in hass.config.components
@pytest.mark.parametrize("load_registries", [False])
async def test_setting_up_config(hass: HomeAssistant) -> None:
"""Test we set up domains in config."""
2019-07-31 21:25:30 +02:00
await bootstrap._async_set_up_integrations(
hass, {"group hello": {}, "homeassistant": {}}
)
2019-07-31 21:25:30 +02:00
assert "group" in hass.config.components
@pytest.mark.parametrize("load_registries", [False])
async def test_setup_after_deps_all_present(hass: HomeAssistant) -> None:
"""Test after_dependencies when all present."""
order = []
def gen_domain_setup(domain):
async def async_setup(hass, config):
order.append(domain)
return True
return async_setup
2019-07-31 21:25:30 +02:00
mock_integration(
hass, MockModule(domain="root", async_setup=gen_domain_setup("root"))
)
mock_integration(
hass,
MockModule(
domain="first_dep",
async_setup=gen_domain_setup("first_dep"),
partial_manifest={"after_dependencies": ["root"]},
),
)
mock_integration(
hass,
MockModule(
domain="second_dep",
async_setup=gen_domain_setup("second_dep"),
partial_manifest={"after_dependencies": ["first_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": {}}
)
2019-07-31 21:25:30 +02:00
assert "root" in hass.config.components
assert "first_dep" in hass.config.components
assert "second_dep" in hass.config.components
assert order == ["logger", "root", "first_dep", "second_dep"]
@pytest.mark.parametrize("load_registries", [False])
async def test_setup_after_deps_in_stage_1_ignored(hass: HomeAssistant) -> None:
"""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(
2020-08-27 13:56:20 +02:00
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"]
@pytest.mark.parametrize("load_registries", [False])
async def test_setup_frontend_before_recorder(hass: HomeAssistant) -> None:
"""Test frontend is setup before recorder."""
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="frontend",
async_setup=gen_domain_setup("frontend"),
partial_manifest={
"dependencies": ["http"],
"after_dependencies": ["an_after_dep"],
},
),
)
mock_integration(
hass,
MockModule(
domain="http",
async_setup=gen_domain_setup("http"),
),
)
mock_integration(
hass,
MockModule(
domain="recorder",
async_setup=gen_domain_setup("recorder"),
),
)
await bootstrap._async_set_up_integrations(
hass,
{
"frontend": {},
"http": {},
"recorder": {},
"normal_integration": {},
"an_after_dep": {},
},
)
assert "frontend" in hass.config.components
assert "normal_integration" in hass.config.components
assert "recorder" in hass.config.components
assert order == [
"http",
"frontend",
"recorder",
"an_after_dep",
"normal_integration",
]
@pytest.mark.parametrize("load_registries", [False])
async def test_setup_after_deps_via_platform(hass: HomeAssistant) -> None:
"""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_platform(hass, "platform_int.light", MockPlatform())
@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"]
@pytest.mark.parametrize("load_registries", [False])
async def test_setup_after_deps_not_trigger_load(hass: HomeAssistant) -> None:
"""Test after_dependencies does not trigger loading it."""
order = []
def gen_domain_setup(domain):
async def async_setup(hass, config):
order.append(domain)
return True
return async_setup
2019-07-31 21:25:30 +02:00
mock_integration(
hass, MockModule(domain="root", async_setup=gen_domain_setup("root"))
)
mock_integration(
hass,
MockModule(
domain="first_dep",
async_setup=gen_domain_setup("first_dep"),
partial_manifest={"after_dependencies": ["root"]},
),
)
mock_integration(
hass,
MockModule(
domain="second_dep",
async_setup=gen_domain_setup("second_dep"),
partial_manifest={"after_dependencies": ["first_dep"]},
),
)
await bootstrap._async_set_up_integrations(hass, {"root": {}, "second_dep": {}})
assert "root" in hass.config.components
assert "first_dep" not in hass.config.components
assert "second_dep" in hass.config.components
@pytest.mark.parametrize("load_registries", [False])
async def test_setup_after_deps_not_present(hass: HomeAssistant) -> None:
"""Test after_dependencies when referenced integration doesn't exist."""
order = []
def gen_domain_setup(domain):
async def async_setup(hass, config):
order.append(domain)
return True
return async_setup
2019-07-31 21:25:30 +02:00
mock_integration(
hass, MockModule(domain="root", async_setup=gen_domain_setup("root"))
)
mock_integration(
hass,
MockModule(
domain="second_dep",
async_setup=gen_domain_setup("second_dep"),
partial_manifest={"after_dependencies": ["first_dep"]},
),
)
await bootstrap._async_set_up_integrations(
hass, {"root": {}, "first_dep": {}, "second_dep": {}}
)
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"]
@pytest.fixture
2023-03-16 11:08:47 +01:00
def mock_is_virtual_env() -> Generator[Mock, None, None]:
"""Mock is_virtual_env."""
with patch(
"homeassistant.bootstrap.is_virtual_env", return_value=False
) as is_virtual_env:
yield is_virtual_env
@pytest.fixture
2023-03-16 11:08:47 +01:00
def mock_enable_logging() -> Generator[Mock, None, None]:
"""Mock enable logging."""
with patch("homeassistant.bootstrap.async_enable_logging") as enable_logging:
yield enable_logging
@pytest.fixture
2023-03-16 11:08:47 +01:00
def mock_mount_local_lib_path() -> Generator[AsyncMock, None, None]:
"""Mock enable logging."""
with patch(
"homeassistant.bootstrap.async_mount_local_lib_path"
) as mount_local_lib_path:
yield mount_local_lib_path
@pytest.fixture
2023-03-16 11:08:47 +01:00
def mock_process_ha_config_upgrade() -> Generator[Mock, None, None]:
"""Mock enable logging."""
with patch(
"homeassistant.config.process_ha_config_upgrade"
) as process_ha_config_upgrade:
yield process_ha_config_upgrade
@pytest.fixture
2023-03-16 11:08:47 +01:00
def mock_ensure_config_exists() -> Generator[AsyncMock, None, None]:
"""Mock enable logging."""
with patch(
"homeassistant.config.async_ensure_config_exists", return_value=True
) as ensure_config_exists:
yield ensure_config_exists
@pytest.mark.parametrize("hass_config", [{"browser": {}, "frontend": {}}])
async def test_setup_hass(
mock_hass_config: None,
2023-03-16 11:08:47 +01:00
mock_enable_logging: Mock,
mock_is_virtual_env: Mock,
mock_mount_local_lib_path: AsyncMock,
mock_ensure_config_exists: AsyncMock,
mock_process_ha_config_upgrade: Mock,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test it works."""
verbose = Mock()
log_rotate_days = Mock()
log_file = Mock()
log_no_color = Mock()
with patch.object(bootstrap, "LOG_SLOW_STARTUP_INTERVAL", 5000):
hass = await bootstrap.async_setup_hass(
runner.RuntimeConfig(
config_dir=get_test_config_dir(),
verbose=verbose,
log_rotate_days=log_rotate_days,
log_file=log_file,
log_no_color=log_no_color,
skip_pip=True,
recovery_mode=False,
),
)
assert "Waiting on integrations to complete setup" not in caplog.text
assert "browser" in hass.config.components
assert "recovery_mode" not in hass.config.components
assert len(mock_enable_logging.mock_calls) == 1
assert mock_enable_logging.mock_calls[0][1] == (
hass,
verbose,
log_rotate_days,
log_file,
log_no_color,
)
assert len(mock_mount_local_lib_path.mock_calls) == 1
assert len(mock_ensure_config_exists.mock_calls) == 1
assert len(mock_process_ha_config_upgrade.mock_calls) == 1
assert hass == async_get_hass()
@pytest.mark.parametrize("hass_config", [{"browser": {}, "frontend": {}}])
async def test_setup_hass_takes_longer_than_log_slow_startup(
mock_hass_config: None,
2023-03-16 11:08:47 +01:00
mock_enable_logging: Mock,
mock_is_virtual_env: Mock,
mock_mount_local_lib_path: AsyncMock,
mock_ensure_config_exists: AsyncMock,
mock_process_ha_config_upgrade: Mock,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test it works."""
verbose = Mock()
log_rotate_days = Mock()
log_file = Mock()
log_no_color = Mock()
async def _async_setup_that_blocks_startup(*args, **kwargs):
await asyncio.sleep(0.6)
return True
with patch.object(bootstrap, "LOG_SLOW_STARTUP_INTERVAL", 0.3), patch.object(
bootstrap, "SLOW_STARTUP_CHECK_INTERVAL", 0.05
), patch(
"homeassistant.components.frontend.async_setup",
side_effect=_async_setup_that_blocks_startup,
):
await bootstrap.async_setup_hass(
runner.RuntimeConfig(
config_dir=get_test_config_dir(),
verbose=verbose,
log_rotate_days=log_rotate_days,
log_file=log_file,
log_no_color=log_no_color,
skip_pip=True,
recovery_mode=False,
),
)
assert "Waiting on integrations to complete setup" in caplog.text
async def test_setup_hass_invalid_yaml(
2023-03-16 11:08:47 +01:00
mock_enable_logging: Mock,
mock_is_virtual_env: Mock,
mock_mount_local_lib_path: AsyncMock,
mock_ensure_config_exists: AsyncMock,
mock_process_ha_config_upgrade: Mock,
) -> None:
"""Test it works."""
with patch(
"homeassistant.config.async_hass_config_yaml", side_effect=HomeAssistantError
):
hass = await bootstrap.async_setup_hass(
runner.RuntimeConfig(
config_dir=get_test_config_dir(),
verbose=False,
log_rotate_days=10,
log_file="",
log_no_color=False,
skip_pip=True,
recovery_mode=False,
),
)
assert "recovery_mode" in hass.config.components
assert len(mock_mount_local_lib_path.mock_calls) == 0
async def test_setup_hass_config_dir_nonexistent(
2023-03-16 11:08:47 +01:00
mock_enable_logging: Mock,
mock_is_virtual_env: Mock,
mock_mount_local_lib_path: AsyncMock,
mock_ensure_config_exists: AsyncMock,
mock_process_ha_config_upgrade: Mock,
) -> None:
"""Test it works."""
mock_ensure_config_exists.return_value = False
assert (
await bootstrap.async_setup_hass(
runner.RuntimeConfig(
config_dir=get_test_config_dir(),
verbose=False,
log_rotate_days=10,
log_file="",
log_no_color=False,
skip_pip=True,
recovery_mode=False,
),
)
is None
)
async def test_setup_hass_recovery_mode(
2023-03-16 11:08:47 +01:00
mock_enable_logging: Mock,
mock_is_virtual_env: Mock,
mock_mount_local_lib_path: AsyncMock,
mock_ensure_config_exists: AsyncMock,
mock_process_ha_config_upgrade: Mock,
) -> None:
"""Test it works."""
with patch("homeassistant.components.browser.setup") as browser_setup, patch(
"homeassistant.config_entries.ConfigEntries.async_domains",
return_value=["browser"],
):
hass = await bootstrap.async_setup_hass(
runner.RuntimeConfig(
config_dir=get_test_config_dir(),
verbose=False,
log_rotate_days=10,
log_file="",
log_no_color=False,
skip_pip=True,
recovery_mode=True,
),
)
assert "recovery_mode" in hass.config.components
assert len(mock_mount_local_lib_path.mock_calls) == 0
# Validate we didn't try to set up config entry.
assert "browser" not in hass.config.components
assert len(browser_setup.mock_calls) == 0
async def test_setup_hass_safe_mode(
mock_hass_config: None,
mock_enable_logging: Mock,
mock_is_virtual_env: Mock,
mock_mount_local_lib_path: AsyncMock,
mock_ensure_config_exists: AsyncMock,
mock_process_ha_config_upgrade: Mock,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test it works."""
with patch("homeassistant.components.browser.setup"), patch(
"homeassistant.config_entries.ConfigEntries.async_domains",
return_value=["browser"],
):
hass = await bootstrap.async_setup_hass(
runner.RuntimeConfig(
config_dir=get_test_config_dir(),
verbose=False,
log_rotate_days=10,
log_file="",
log_no_color=False,
skip_pip=True,
recovery_mode=False,
safe_mode=True,
),
)
assert "recovery_mode" not in hass.config.components
assert "Starting in recovery mode" not in caplog.text
assert "Starting in safe mode" in caplog.text
async def test_setup_hass_recovery_mode_and_safe_mode(
mock_hass_config: None,
mock_enable_logging: Mock,
mock_is_virtual_env: Mock,
mock_mount_local_lib_path: AsyncMock,
mock_ensure_config_exists: AsyncMock,
mock_process_ha_config_upgrade: Mock,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test it works."""
with patch("homeassistant.components.browser.setup"), patch(
"homeassistant.config_entries.ConfigEntries.async_domains",
return_value=["browser"],
):
hass = await bootstrap.async_setup_hass(
runner.RuntimeConfig(
config_dir=get_test_config_dir(),
verbose=False,
log_rotate_days=10,
log_file="",
log_no_color=False,
skip_pip=True,
recovery_mode=True,
safe_mode=True,
),
)
assert "recovery_mode" in hass.config.components
assert "Starting in recovery mode" in caplog.text
assert "Starting in safe mode" not in caplog.text
@pytest.mark.parametrize("hass_config", [{"homeassistant": {"non-existing": 1}}])
async def test_setup_hass_invalid_core_config(
mock_hass_config: None,
2023-03-16 11:08:47 +01:00
mock_enable_logging: Mock,
mock_is_virtual_env: Mock,
mock_mount_local_lib_path: AsyncMock,
mock_ensure_config_exists: AsyncMock,
mock_process_ha_config_upgrade: Mock,
) -> None:
"""Test it works."""
with patch("homeassistant.bootstrap.async_notify_setup_error") as mock_notify:
hass = await bootstrap.async_setup_hass(
runner.RuntimeConfig(
config_dir=get_test_config_dir(),
verbose=False,
log_rotate_days=10,
log_file="",
log_no_color=False,
skip_pip=True,
recovery_mode=False,
),
)
assert len(mock_notify.mock_calls) == 1
assert "recovery_mode" in hass.config.components
@pytest.mark.parametrize(
"hass_config",
[
{
"homeassistant": {
"internal_url": "http://192.168.1.100:8123",
"external_url": "https://abcdef.ui.nabu.casa",
},
"map": {},
"person": {"invalid": True},
}
],
)
async def test_setup_recovery_mode_if_no_frontend(
mock_hass_config: None,
2023-03-16 11:08:47 +01:00
mock_enable_logging: Mock,
mock_is_virtual_env: Mock,
mock_mount_local_lib_path: AsyncMock,
mock_ensure_config_exists: AsyncMock,
mock_process_ha_config_upgrade: Mock,
) -> None:
"""Test we setup recovery mode if frontend didn't load."""
verbose = Mock()
log_rotate_days = Mock()
log_file = Mock()
log_no_color = Mock()
hass = await bootstrap.async_setup_hass(
runner.RuntimeConfig(
config_dir=get_test_config_dir(),
verbose=verbose,
log_rotate_days=log_rotate_days,
log_file=log_file,
log_no_color=log_no_color,
skip_pip=True,
recovery_mode=False,
),
)
assert "recovery_mode" in hass.config.components
assert hass.config.config_dir == get_test_config_dir()
assert hass.config.skip_pip
assert hass.config.internal_url == "http://192.168.1.100:8123"
assert hass.config.external_url == "https://abcdef.ui.nabu.casa"
@pytest.mark.parametrize("load_registries", [False])
@patch("homeassistant.bootstrap.DEFAULT_INTEGRATIONS", set())
async def test_empty_integrations_list_is_only_sent_at_the_end_of_bootstrap(
hass: HomeAssistant,
) -> None:
"""Test empty integrations list is only sent at the end of bootstrap."""
order = []
def gen_domain_setup(domain):
async def async_setup(hass, config):
order.append(domain)
await asyncio.sleep(0.1)
async def _background_task():
await asyncio.sleep(0.2)
await hass.async_create_task(_background_task())
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"),
),
)
integrations = []
@callback
def _bootstrap_integrations(data):
integrations.append(data)
async_dispatcher_connect(
hass, SIGNAL_BOOTSTRAP_INTEGRATIONS, _bootstrap_integrations
)
with patch.object(bootstrap, "SLOW_STARTUP_CHECK_INTERVAL", 0.05):
await bootstrap._async_set_up_integrations(
hass, {"normal_integration": {}, "an_after_dep": {}}
)
await hass.async_block_till_done()
assert integrations[0] != {}
assert "an_after_dep" in integrations[0]
assert integrations[-2] != {}
assert integrations[-1] == {}
assert "normal_integration" in hass.config.components
assert order == ["an_after_dep", "normal_integration"]
@pytest.mark.parametrize("load_registries", [False])
async def test_warning_logged_on_wrap_up_timeout(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test we log a warning on bootstrap timeout."""
def gen_domain_setup(domain):
async def async_setup(hass, config):
async def _background_task():
await asyncio.sleep(0.2)
hass.async_create_task(_background_task())
return True
return async_setup
mock_integration(
hass,
MockModule(
domain="normal_integration",
async_setup=gen_domain_setup("normal_integration"),
partial_manifest={},
),
)
with patch.object(bootstrap, "WRAP_UP_TIMEOUT", 0):
await bootstrap._async_set_up_integrations(hass, {"normal_integration": {}})
await hass.async_block_till_done()
assert "Setup timed out for bootstrap - moving forward" in caplog.text
@pytest.mark.parametrize("load_registries", [False])
async def test_bootstrap_is_cancellation_safe(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test cancellation during async_setup_component does not cancel bootstrap."""
with patch.object(
bootstrap, "async_setup_component", side_effect=asyncio.CancelledError
):
await bootstrap._async_set_up_integrations(hass, {"cancel_integration": {}})
await hass.async_block_till_done()
assert "Error setting up integration cancel_integration" in caplog.text
@pytest.mark.parametrize("load_registries", [False])
async def test_bootstrap_empty_integrations(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test setting up an empty integrations does not raise."""
await bootstrap.async_setup_multi_components(hass, set(), {})
await hass.async_block_till_done()
@pytest.mark.parametrize("integration", ["mqtt_eventstream", "mqtt_statestream"])
@pytest.mark.parametrize("load_registries", [False])
async def test_bootstrap_dependencies(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
integration: str,
) -> None:
"""Test dependencies are set up correctly,."""
# Prepare MQTT config entry
@HANDLERS.register("mqtt")
class MockConfigFlow:
"""Mock the MQTT config flow."""
VERSION = 1
MINOR_VERSION = 1
entry = MockConfigEntry(domain="mqtt", data={"broker": "test-broker"})
entry.add_to_hass(hass)
calls: list[str] = []
assertions: list[bool] = []
async def async_mqtt_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Assert the mqtt config entry was set up."""
calls.append("mqtt")
# assert the integration is not yet set up
assertions.append(hass.data["setup_done"][integration].done() is False)
assertions.append(
all(
dependency in hass.config.components
for dependency in integrations[integration]["dependencies"]
)
)
assertions.append(integration not in hass.config.components)
return True
async def async_integration_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Assert the mqtt config entry was set up."""
calls.append(integration)
# assert mqtt was already set up
assertions.append(
"mqtt" not in hass.data["setup_done"]
or hass.data["setup_done"]["mqtt"].done()
)
assertions.append("mqtt" in hass.config.components)
return True
mqtt_integration = mock_integration(
hass,
MockModule(
"mqtt",
async_setup_entry=async_mqtt_setup_entry,
dependencies=["file_upload", "http"],
),
)
# We patch the _import platform method to avoid loading the platform module
# to avoid depending on non core components in the tests.
mqtt_integration._import_platform = Mock()
integrations = {
"mqtt": {
"dependencies": {"file_upload", "http"},
"integration": mqtt_integration,
},
"mqtt_eventstream": {
"dependencies": {"mqtt"},
"integration": mock_integration(
hass,
MockModule(
"mqtt_eventstream",
async_setup=async_integration_setup,
dependencies=["mqtt"],
),
),
},
"mqtt_statestream": {
"dependencies": {"mqtt"},
"integration": mock_integration(
hass,
MockModule(
"mqtt_statestream",
async_setup=async_integration_setup,
dependencies=["mqtt"],
),
),
},
"file_upload": {
"dependencies": {"http"},
"integration": mock_integration(
hass,
MockModule(
"file_upload",
dependencies=["http"],
),
),
},
"http": {
"dependencies": set(),
"integration": mock_integration(
hass,
MockModule("http", dependencies=[]),
),
},
}
async def mock_async_get_integrations(
hass: HomeAssistant, domains: Iterable[str]
) -> dict[str, Integration | Exception]:
"""Mock integrations."""
return {domain: integrations[domain]["integration"] for domain in domains}
with patch(
"homeassistant.setup.loader.async_get_integrations",
side_effect=mock_async_get_integrations,
), patch(
"homeassistant.config.async_process_component_config",
return_value=config_util.IntegrationConfigInfo({}, []),
):
bootstrap.async_set_domains_to_be_loaded(hass, {integration})
await bootstrap.async_setup_multi_components(hass, {integration}, {})
await hass.async_block_till_done()
for assertion in assertions:
assert assertion
assert calls == ["mqtt", integration]
assert (
f"Dependency {integration} will wait for dependencies dict_keys(['mqtt'])"
in caplog.text
)
async def test_frontend_deps_pre_import_no_requirements(hass: HomeAssistant) -> None:
"""Test frontend dependencies are pre-imported and do not have any requirements."""
pre_imports = [
name.removesuffix("_pre_import")
for name in dir(bootstrap)
if name.endswith("_pre_import")
]
# Make sure future refactoring does not
# accidentally remove the pre-imports
# or change the naming convention without
# updating this test.
assert len(pre_imports) > 3
for pre_import in pre_imports:
integration = await loader.async_get_integration(hass, pre_import)
assert not integration.requirements
async def test_bootstrap_does_not_preload_stage_1_integrations() -> None:
"""Test that the bootstrap does not preload stage 1 integrations.
If this test fails it means that stage1 integrations are being
loaded too soon and will not get their requirements updated
before they are loaded at runtime.
"""
process = await asyncio.create_subprocess_exec(
sys.executable,
"-c",
"import homeassistant.bootstrap; import sys; print(sys.modules)",
stdout=asyncio.subprocess.PIPE,
)
stdout, _ = await process.communicate()
assert process.returncode == 0
decoded_stdout = stdout.decode()
# Ensure no stage1 integrations have been imported
# as a side effect of importing the pre-imports
for integration in bootstrap.STAGE_1_INTEGRATIONS:
assert f"homeassistant.components.{integration}" not in decoded_stdout