"""Test the bootstrapping.""" import asyncio from collections.abc import Generator, Iterable import contextlib import glob import os import sys from typing import Any from unittest.mock import AsyncMock, Mock, patch import pytest from homeassistant import bootstrap, loader, runner import homeassistant.config as config_util from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_DEBUG, SIGNAL_BOOTSTRAP_INTEGRATIONS from homeassistant.core import CoreState, HomeAssistant, async_get_hass, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.translation import async_translations_loaded from homeassistant.helpers.typing import ConfigType from homeassistant.loader import Integration from homeassistant.setup import BASE_PLATFORMS from .common import ( MockConfigEntry, MockModule, MockPlatform, get_test_config_dir, mock_config_flow, mock_integration, mock_platform, ) VERSION_PATH = os.path.join(get_test_config_dir(), config_util.VERSION_FILE) @pytest.fixture(autouse=True) def disable_installed_check() -> Generator[None, None, None]: """Disable package installed check.""" with patch("homeassistant.util.package.is_installed", return_value=True): yield @pytest.fixture(autouse=True) 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="module", autouse=True) 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 @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( {"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" ) 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) 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 @pytest.mark.parametrize("load_registries", [False]) async def test_config_does_not_turn_off_debug(hass: HomeAssistant) -> None: """Test that config does not turn off debug if its turned on by runtime config.""" # Mock that its turned on from RuntimeConfig hass.config.debug = True await bootstrap.async_from_config_dict({CONF_DEBUG: False}, hass) assert hass.config.debug is True @pytest.mark.parametrize("hass_config", [{"frontend": {}}]) async def test_asyncio_debug_on_turns_hass_debug_on( 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, ) -> None: """Test that asyncio debug turns on hass debug.""" asyncio.get_running_loop().set_debug(True) 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 hass.config.debug is True @pytest.mark.parametrize("load_registries", [False]) async def test_preload_translations(hass: HomeAssistant) -> None: """Test translations are preloaded for all frontend deps and base platforms.""" await bootstrap.async_from_config_dict({}, hass) await hass.async_block_till_done(wait_background_tasks=True) frontend = await loader.async_get_integration(hass, "frontend") assert async_translations_loaded(hass, set(frontend.all_dependencies)) assert async_translations_loaded(hass, BASE_PLATFORMS) async def test_core_failure_loads_recovery_mode( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test failing core setup aborts further setup.""" with patch( "homeassistant.components.homeassistant.async_setup", return_value=False, ): await bootstrap.async_from_config_dict({"group": {}}, hass) assert "core failed to initialize" in caplog.text # We aborted early, group not set up 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.""" await bootstrap._async_set_up_integrations( hass, {"group hello": {}, "homeassistant": {}} ) 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 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": {}} ) 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( 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_after_deps_manifests_are_loaded_even_if_not_setup( hass: HomeAssistant, ) -> None: """Ensure we preload manifests for after deps even if they are not setup. Its important that we preload the after dep manifests even if they are not setup since we will always have to check their requirements since any integration that lists an after dep may import it and we have to ensure requirements are up to date before the after dep can be imported. """ # This test relies on this assert "cloud" in bootstrap.STAGE_1_INTEGRATIONS order = [] def gen_domain_setup(domain): async def async_setup(hass, config): order.append(domain) return True return async_setup mock_integration( hass, MockModule( domain="normal_integration", async_setup=gen_domain_setup("normal_integration"), partial_manifest={"after_dependencies": ["an_after_dep"]}, ), ) mock_integration( hass, MockModule( domain="an_after_dep", async_setup=gen_domain_setup("an_after_dep"), partial_manifest={"after_dependencies": ["an_after_dep_of_after_dep"]}, ), ) mock_integration( hass, MockModule( domain="an_after_dep_of_after_dep", async_setup=gen_domain_setup("an_after_dep_of_after_dep"), partial_manifest={ "after_dependencies": ["an_after_dep_of_after_dep_of_after_dep"] }, ), ) mock_integration( hass, MockModule( domain="an_after_dep_of_after_dep_of_after_dep", async_setup=gen_domain_setup("an_after_dep_of_after_dep_of_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": {}} ) assert "normal_integration" in hass.config.components assert "cloud" in hass.config.components assert "an_after_dep" not in hass.config.components assert "an_after_dep_of_after_dep" not in hass.config.components assert "an_after_dep_of_after_dep_of_after_dep" not in hass.config.components assert order == ["cloud", "normal_integration"] assert loader.async_get_loaded_integration(hass, "an_after_dep") is not None assert ( loader.async_get_loaded_integration(hass, "an_after_dep_of_after_dep") is not None ) assert ( loader.async_get_loaded_integration( hass, "an_after_dep_of_after_dep_of_after_dep" ) is not None ) @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"), partial_manifest={ "after_dependencies": ["http"], }, ), ) 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 "http" 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 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 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 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 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 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 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 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, 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, debug=True, ), ) 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 # debug in RuntimeConfig should set it it in hass.config assert hass.config.debug is True 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, 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.2) return True with ( patch.object(bootstrap, "LOG_SLOW_STARTUP_INTERVAL", 0.1), 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( 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( 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( 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, 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, 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.""" # setup times only tracked when not running hass.set_state(CoreState.not_running) order = [] def gen_domain_setup(domain): async def async_setup(hass, config): order.append(domain) await asyncio.sleep(0.05) async def _background_task(): await asyncio.sleep(0.1) 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.025): 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.""" task: asyncio.Task | None = None def gen_domain_setup(domain): async def async_setup(hass, config): nonlocal task async def _not_marked_background_task(): await asyncio.sleep(2) task = hass.async_create_task(_not_marked_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": {}}) task.cancel() with contextlib.suppress(asyncio.CancelledError): await task assert "Setup timed out for bootstrap" in caplog.text assert "waiting on" in caplog.text assert "_not_marked_background_task" in caplog.text @pytest.mark.parametrize("load_registries", [False]) async def test_tasks_logged_that_block_stage_1( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test we log tasks that delay stage 1 startup.""" def gen_domain_setup(domain): async def async_setup(hass, config): async def _not_marked_background_task(): await asyncio.sleep(0.2) hass.async_create_task(_not_marked_background_task()) await asyncio.sleep(0.1) return True return async_setup mock_integration( hass, MockModule( domain="normal_integration", async_setup=gen_domain_setup("normal_integration"), partial_manifest={}, ), ) original_stage_1 = bootstrap.STAGE_1_INTEGRATIONS with ( patch.object(bootstrap, "STAGE_1_TIMEOUT", 0), patch.object(bootstrap, "COOLDOWN_TIME", 0), patch.object( bootstrap, "STAGE_1_INTEGRATIONS", [*original_stage_1, "normal_integration"] ), ): await bootstrap._async_set_up_integrations(hass, {"normal_integration": {}}) await hass.async_block_till_done() assert "Setup timed out for stage 1 waiting on" in caplog.text assert "waiting on" in caplog.text assert "_not_marked_background_task" in caplog.text @pytest.mark.parametrize("load_registries", [False]) async def test_tasks_logged_that_block_stage_2( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test we log tasks that delay stage 2 startup.""" def gen_domain_setup(domain): async def async_setup(hass, config): async def _not_marked_background_task(): await asyncio.sleep(0.2) hass.async_create_task(_not_marked_background_task()) await asyncio.sleep(0.1) 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, "STAGE_2_TIMEOUT", 0), patch.object(bootstrap, "COOLDOWN_TIME", 0), ): await bootstrap._async_set_up_integrations(hass, {"normal_integration": {}}) await hass.async_block_till_done() assert "Setup timed out for stage 2 waiting on" in caplog.text assert "waiting on" in caplog.text assert "_not_marked_background_task" 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.fixture(name="mock_mqtt_config_flow") def mock_mqtt_config_flow_fixture() -> Generator[None, None, None]: """Mock MQTT config flow.""" class MockConfigFlow: """Mock the MQTT config flow.""" VERSION = 1 MINOR_VERSION = 1 with mock_config_flow("mqtt", MockConfigFlow): yield @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, mock_mqtt_config_flow: None, ) -> None: """Test dependencies are set up correctly,.""" 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() mqtt_integration.platforms_exists = Mock(return_value=True) 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_pre_import_no_requirements(hass: HomeAssistant) -> None: """Test 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 @pytest.mark.timeout(20) 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 @pytest.mark.parametrize("load_registries", [False]) async def test_cancellation_does_not_leak_upward_from_async_setup( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, enable_custom_integrations: None, ) -> None: """Test setting up an integration that raises asyncio.CancelledError.""" await bootstrap.async_setup_multi_components( hass, {"test_package_raises_cancelled_error"}, {} ) await hass.async_block_till_done() assert ( "Error during setup of component test_package_raises_cancelled_error" in caplog.text ) @pytest.mark.parametrize("load_registries", [False]) async def test_cancellation_does_not_leak_upward_from_async_setup_entry( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, enable_custom_integrations: None, ) -> None: """Test setting up an integration that raises asyncio.CancelledError.""" entry = MockConfigEntry( domain="test_package_raises_cancelled_error_config_entry", data={} ) entry.add_to_hass(hass) await bootstrap.async_setup_multi_components( hass, {"test_package_raises_cancelled_error_config_entry"}, {} ) await hass.async_block_till_done() await bootstrap.async_setup_multi_components(hass, {"test_package"}, {}) await hass.async_block_till_done() assert ( "Error setting up entry Mock Title for test_package_raises_cancelled_error_config_entry" in caplog.text ) assert "test_package" in hass.config.components assert "test_package_raises_cancelled_error_config_entry" in hass.config.components @pytest.mark.parametrize("load_registries", [False]) async def test_setup_does_base_platforms_first(hass: HomeAssistant) -> None: """Test setup does base platforms first. Its important that base platforms are setup before other integrations in stage1/2 since they are the foundation for other integrations and almost every integration has to wait for them to be setup. """ 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="sensor", async_setup=gen_domain_setup("sensor")) ) mock_integration( hass, MockModule( domain="binary_sensor", async_setup=gen_domain_setup("binary_sensor") ), ) 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": {}, "sensor": {}, "logger": {}, "binary_sensor": {}, }, ) assert "binary_sensor" in hass.config.components assert "sensor" in hass.config.components assert "root" in hass.config.components assert "first_dep" in hass.config.components assert "second_dep" in hass.config.components assert order[0] == "logger" # base platforms (sensor/binary_sensor) should be setup before other integrations # but after logger integrations. The order of base platforms is not guaranteed, # only that they are setup before other integrations. assert set(order[1:3]) == {"sensor", "binary_sensor"} assert order[3:] == ["root", "first_dep", "second_dep"] def test_should_rollover_is_always_false(): """Test that shouldRollover always returns False.""" assert ( bootstrap._RotatingFileHandlerWithoutShouldRollOver( "any.log", delay=True ).shouldRollover(Mock()) is False )