From 5fdc60e067318b8fb605eaeb9858f4e1cc21237b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 14 Jan 2020 13:03:02 -0800 Subject: [PATCH] Add Safe Mode (#30723) * Store last working HTTP settings * Add safe mode * Fix tests * Add cloud to safe mode * Update logging text * Fix camera tests leaving files behind * Make emulated_hue tests not leave files behind * Make logbook tests not leave files behind * Make tts tests not leave files behind * Make image_processing tests not leave files behind * Make manual_mqtt tests not leave files behind --- CODEOWNERS | 1 + homeassistant/__main__.py | 60 ++--- homeassistant/bootstrap.py | 132 +++++------ homeassistant/components/frontend/__init__.py | 9 +- homeassistant/components/http/__init__.py | 19 +- .../components/safe_mode/__init__.py | 15 ++ .../components/safe_mode/manifest.json | 12 + homeassistant/config.py | 63 ++--- homeassistant/helpers/check_config.py | 8 +- homeassistant/scripts/ensure_config.py | 7 +- homeassistant/setup.py | 2 +- tests/components/automation/test_init.py | 23 +- tests/components/camera/test_init.py | 132 ++++------- tests/components/emulated_hue/test_upnp.py | 9 +- tests/components/frontend/test_init.py | 4 +- tests/components/group/test_init.py | 5 +- tests/components/homeassistant/test_scene.py | 4 +- tests/components/http/test_init.py | 13 + .../components/image_processing/test_init.py | 2 - tests/components/input_boolean/test_init.py | 15 +- tests/components/input_datetime/test_init.py | 19 +- tests/components/input_number/test_init.py | 19 +- tests/components/input_select/test_init.py | 19 +- tests/components/input_text/test_init.py | 19 +- tests/components/logbook/test_init.py | 3 +- .../manual_mqtt/test_alarm_control_panel.py | 6 - tests/components/person/test_init.py | 2 +- tests/components/safe_mode/__init__.py | 1 + tests/components/safe_mode/test_init.py | 9 + tests/components/script/test_init.py | 9 +- tests/components/timer/test_init.py | 19 +- tests/components/tts/test_init.py | 4 + tests/scripts/test_check_config.py | 2 +- tests/test_bootstrap.py | 222 ++++++++++++++---- tests/test_config.py | 22 +- 35 files changed, 480 insertions(+), 430 deletions(-) create mode 100644 homeassistant/components/safe_mode/__init__.py create mode 100644 homeassistant/components/safe_mode/manifest.json create mode 100644 tests/components/safe_mode/__init__.py create mode 100644 tests/components/safe_mode/test_init.py diff --git a/CODEOWNERS b/CODEOWNERS index 7ef5986a4da..9d2f4eb390b 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -278,6 +278,7 @@ homeassistant/components/rfxtrx/* @danielhiversen homeassistant/components/ring/* @balloob homeassistant/components/rmvtransport/* @cgtobi homeassistant/components/roomba/* @pschmitt +homeassistant/components/safe_mode/* @home-assistant/core homeassistant/components/saj/* @fredericvl homeassistant/components/samsungtv/* @escoand homeassistant/components/scene/* @home-assistant/core diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index 5ebdc71680e..d1d59482e6d 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -6,13 +6,10 @@ import platform import subprocess import sys import threading -from typing import TYPE_CHECKING, Any, Dict, List +from typing import List from homeassistant.const import REQUIRED_PYTHON_VER, RESTART_EXIT_CODE, __version__ -if TYPE_CHECKING: - from homeassistant import core - def set_loop() -> None: """Attempt to use different loop.""" @@ -78,19 +75,6 @@ def ensure_config_path(config_dir: str) -> None: sys.exit(1) -async def ensure_config_file(hass: "core.HomeAssistant", config_dir: str) -> str: - """Ensure configuration file exists.""" - import homeassistant.config as config_util - - config_path = await config_util.async_ensure_config_exists(hass, config_dir) - - if config_path is None: - print("Error getting configuration path") - sys.exit(1) - - return config_path - - def get_arguments() -> argparse.Namespace: """Get parsed passed in arguments.""" import homeassistant.config as config_util @@ -107,7 +91,7 @@ def get_arguments() -> argparse.Namespace: help="Directory that contains the Home Assistant configuration", ) parser.add_argument( - "--demo-mode", action="store_true", help="Start Home Assistant in demo mode" + "--safe-mode", action="store_true", help="Start Home Assistant in safe mode" ) parser.add_argument( "--debug", action="store_true", help="Start Home Assistant in debug mode" @@ -253,34 +237,20 @@ def cmdline() -> List[str]: async def setup_and_run_hass(config_dir: str, args: argparse.Namespace) -> int: """Set up Home Assistant and run.""" - from homeassistant import bootstrap, core + from homeassistant import bootstrap - hass = core.HomeAssistant() + hass = await bootstrap.async_setup_hass( + config_dir=config_dir, + verbose=args.verbose, + log_rotate_days=args.log_rotate_days, + log_file=args.log_file, + log_no_color=args.log_no_color, + skip_pip=args.skip_pip, + safe_mode=args.safe_mode, + ) - if args.demo_mode: - config: Dict[str, Any] = {"frontend": {}, "demo": {}} - bootstrap.async_from_config_dict( - config, - hass, - config_dir=config_dir, - verbose=args.verbose, - skip_pip=args.skip_pip, - log_rotate_days=args.log_rotate_days, - log_file=args.log_file, - log_no_color=args.log_no_color, - ) - else: - config_file = await ensure_config_file(hass, config_dir) - print("Config directory:", config_dir) - await bootstrap.async_from_config_file( - config_file, - hass, - verbose=args.verbose, - skip_pip=args.skip_pip, - log_rotate_days=args.log_rotate_days, - log_file=args.log_file, - log_no_color=args.log_no_color, - ) + if hass is None: + return 1 if args.open_ui and hass.config.api is not None: import webbrowser @@ -358,7 +328,7 @@ def main() -> int: return scripts.run(args.script) - config_dir = os.path.join(os.getcwd(), args.config) + config_dir = os.path.abspath(os.path.join(os.getcwd(), args.config)) ensure_config_path(config_dir) # Daemon functions diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 7ceedba5bd5..3d8523bf9ac 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -1,6 +1,5 @@ """Provide methods to bootstrap a Home Assistant instance.""" import asyncio -from collections import OrderedDict import logging import logging.handlers import os @@ -11,6 +10,7 @@ from typing import Any, Dict, Optional, Set import voluptuous as vol from homeassistant import config as conf_util, config_entries, core, loader +from homeassistant.components import http from homeassistant.const import ( EVENT_HOMEASSISTANT_CLOSE, REQUIRED_NEXT_PYTHON_DATE, @@ -42,16 +42,68 @@ STAGE_1_INTEGRATIONS = { } +async def async_setup_hass( + *, + config_dir: str, + verbose: bool, + log_rotate_days: int, + log_file: str, + log_no_color: bool, + skip_pip: bool, + safe_mode: bool, +) -> Optional[core.HomeAssistant]: + """Set up Home Assistant.""" + hass = core.HomeAssistant() + hass.config.config_dir = config_dir + + async_enable_logging(hass, verbose, log_rotate_days, log_file, log_no_color) + + hass.config.skip_pip = skip_pip + if skip_pip: + _LOGGER.warning( + "Skipping pip installation of required modules. This may cause issues" + ) + + if not await conf_util.async_ensure_config_exists(hass): + _LOGGER.error("Error getting configuration path") + return None + + _LOGGER.info("Config directory: %s", config_dir) + + config_dict = None + + if not safe_mode: + await hass.async_add_executor_job(conf_util.process_ha_config_upgrade, hass) + + try: + config_dict = await conf_util.async_hass_config_yaml(hass) + except HomeAssistantError as err: + _LOGGER.error( + "Failed to parse configuration.yaml: %s. Falling back to safe mode", + err, + ) + else: + if not is_virtual_env(): + await async_mount_local_lib_path(config_dir) + + await async_from_config_dict(config_dict, hass) + finally: + clear_secret_cache() + + if safe_mode or config_dict is None: + _LOGGER.info("Starting in safe mode") + + http_conf = (await http.async_get_last_config(hass)) or {} + + await async_from_config_dict( + {"safe_mode": {}, "http": http_conf}, hass, + ) + + return hass + + async def async_from_config_dict( - config: Dict[str, Any], - hass: core.HomeAssistant, - config_dir: Optional[str] = None, - enable_log: bool = True, - verbose: bool = False, - skip_pip: bool = False, - log_rotate_days: Any = None, - log_file: Any = None, - log_no_color: bool = False, + config: Dict[str, Any], hass: core.HomeAssistant ) -> Optional[core.HomeAssistant]: """Try to configure Home Assistant from a configuration dictionary. @@ -60,15 +112,6 @@ async def async_from_config_dict( """ start = time() - if enable_log: - async_enable_logging(hass, verbose, log_rotate_days, log_file, log_no_color) - - hass.config.skip_pip = skip_pip - if skip_pip: - _LOGGER.warning( - "Skipping pip installation of required modules. This may cause issues" - ) - core_config = config.get(core.DOMAIN, {}) try: @@ -83,14 +126,6 @@ async def async_from_config_dict( ) return None - # Make a copy because we are mutating it. - config = OrderedDict(config) - - # Merge packages - await conf_util.merge_packages_config( - hass, config, core_config.get(conf_util.CONF_PACKAGES, {}) - ) - hass.config_entries = config_entries.ConfigEntries(hass, config) await hass.config_entries.async_initialize() @@ -116,46 +151,6 @@ async def async_from_config_dict( return hass -async def async_from_config_file( - config_path: str, - hass: core.HomeAssistant, - verbose: bool = False, - skip_pip: bool = True, - log_rotate_days: Any = None, - log_file: Any = None, - log_no_color: bool = False, -) -> Optional[core.HomeAssistant]: - """Read the configuration file and try to start all the functionality. - - Will add functionality to 'hass' parameter. - This method is a coroutine. - """ - # Set config dir to directory holding config file - config_dir = os.path.abspath(os.path.dirname(config_path)) - hass.config.config_dir = config_dir - - if not is_virtual_env(): - await async_mount_local_lib_path(config_dir) - - async_enable_logging(hass, verbose, log_rotate_days, log_file, log_no_color) - - await hass.async_add_executor_job(conf_util.process_ha_config_upgrade, hass) - - try: - config_dict = await hass.async_add_executor_job( - conf_util.load_yaml_config_file, config_path - ) - except HomeAssistantError as err: - _LOGGER.error("Error loading %s: %s", config_path, err) - return None - finally: - clear_secret_cache() - - return await async_from_config_dict( - config_dict, hass, enable_log=False, skip_pip=skip_pip - ) - - @core.callback def async_enable_logging( hass: core.HomeAssistant, @@ -269,7 +264,8 @@ def _get_domains(hass: core.HomeAssistant, config: Dict[str, Any]) -> Set[str]: domains = set(key.split(" ")[0] for key in config.keys() if key != core.DOMAIN) # Add config entry domains - domains.update(hass.config_entries.async_domains()) + if "safe_mode" not in config: + domains.update(hass.config_entries.async_domains()) # Make sure the Hass.io component is loaded if "HASSIO" in os.environ: diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index efb1c34653b..8039b9947e7 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -13,7 +13,7 @@ from yarl import URL from homeassistant.components import websocket_api from homeassistant.components.http.view import HomeAssistantView -from homeassistant.config import find_config_file, load_yaml_config_file +from homeassistant.config import async_hass_config_yaml from homeassistant.const import CONF_NAME, EVENT_THEMES_UPDATED from homeassistant.core import callback import homeassistant.helpers.config_validation as cv @@ -362,11 +362,10 @@ def _async_setup_themes(hass, themes): else: _LOGGER.warning("Theme %s is not defined.", name) - @callback - def reload_themes(_): + async def reload_themes(_): """Reload themes.""" - path = find_config_file(hass.config.config_dir) - new_themes = load_yaml_config_file(path)[DOMAIN].get(CONF_THEMES, {}) + config = await async_hass_config_yaml(hass) + new_themes = config[DOMAIN].get(CONF_THEMES, {}) hass.data[DATA_THEMES] = new_themes if hass.data[DATA_DEFAULT_THEME] not in new_themes: hass.data[DATA_DEFAULT_THEME] = DEFAULT_THEME diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 0d93461f90f..58cfb4b9cc1 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -3,7 +3,7 @@ from ipaddress import ip_network import logging import os import ssl -from typing import Optional +from typing import Optional, cast from aiohttp import web from aiohttp.web_exceptions import HTTPMovedPermanently @@ -14,7 +14,10 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, SERVER_PORT, ) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import storage import homeassistant.helpers.config_validation as cv +from homeassistant.loader import bind_hass import homeassistant.util as hass_util from homeassistant.util import ssl as ssl_util @@ -56,6 +59,9 @@ NO_LOGIN_ATTEMPT_THRESHOLD = -1 MAX_CLIENT_SIZE: int = 1024 ** 2 * 16 +STORAGE_KEY = DOMAIN +STORAGE_VERSION = 1 + HTTP_SCHEMA = vol.Schema( { @@ -85,6 +91,13 @@ HTTP_SCHEMA = vol.Schema( CONFIG_SCHEMA = vol.Schema({DOMAIN: HTTP_SCHEMA}, extra=vol.ALLOW_EXTRA) +@bind_hass +async def async_get_last_config(hass: HomeAssistant) -> Optional[dict]: + """Return the last known working config.""" + store = storage.Store(hass, STORAGE_VERSION, STORAGE_KEY) + return cast(Optional[dict], await store.async_load()) + + class ApiConfig: """Configuration settings for API server.""" @@ -151,6 +164,10 @@ async def async_setup(hass, config): hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_server) await server.start() + # If we are set up successful, we store the HTTP settings for safe mode. + store = storage.Store(hass, STORAGE_VERSION, STORAGE_KEY) + await store.async_save(conf) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_server) hass.http = server diff --git a/homeassistant/components/safe_mode/__init__.py b/homeassistant/components/safe_mode/__init__.py new file mode 100644 index 00000000000..aef6834303b --- /dev/null +++ b/homeassistant/components/safe_mode/__init__.py @@ -0,0 +1,15 @@ +"""The Safe Mode integration.""" +from homeassistant.components import persistent_notification +from homeassistant.core import HomeAssistant + +DOMAIN = "safe_mode" + + +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the Safe Mode component.""" + persistent_notification.async_create( + hass, + "Home Assistant is running in safe mode. Check [the error log](/developer-tools/logs) to see what went wrong.", + "Safe Mode", + ) + return True diff --git a/homeassistant/components/safe_mode/manifest.json b/homeassistant/components/safe_mode/manifest.json new file mode 100644 index 00000000000..372ec51de37 --- /dev/null +++ b/homeassistant/components/safe_mode/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "safe_mode", + "name": "Safe Mode", + "config_flow": false, + "documentation": "https://www.home-assistant.io/integrations/safe_mode", + "requirements": [], + "ssdp": [], + "zeroconf": [], + "homekit": {}, + "dependencies": ["frontend", "config", "persistent_notification", "cloud"], + "codeowners": ["@home-assistant/core"] +} diff --git a/homeassistant/config.py b/homeassistant/config.py index 6777c1ef5a5..f5870d683a0 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -226,35 +226,34 @@ def get_default_config_dir() -> str: return os.path.join(data_dir, CONFIG_DIR_NAME) # type: ignore -async def async_ensure_config_exists( - hass: HomeAssistant, config_dir: str -) -> Optional[str]: +async def async_ensure_config_exists(hass: HomeAssistant) -> bool: """Ensure a configuration file exists in given configuration directory. Creating a default one if needed. - Return path to the configuration file. + Return boolean if configuration dir is ready to go. """ - config_path = find_config_file(config_dir) + config_path = hass.config.path(YAML_CONFIG_FILE) - if config_path is None: - print("Unable to find configuration. Creating default one in", config_dir) - config_path = await async_create_default_config(hass, config_dir) + if os.path.isfile(config_path): + return True - return config_path + print( + "Unable to find configuration. Creating default one in", hass.config.config_dir + ) + return await async_create_default_config(hass) -async def async_create_default_config( - hass: HomeAssistant, config_dir: str -) -> Optional[str]: +async def async_create_default_config(hass: HomeAssistant) -> bool: """Create a default configuration file in given configuration directory. - Return path to new config file if success, None if failed. - This method needs to run in an executor. + Return if creation was successful. """ - return await hass.async_add_executor_job(_write_default_config, config_dir) + return await hass.async_add_executor_job( + _write_default_config, hass.config.config_dir + ) -def _write_default_config(config_dir: str) -> Optional[str]: +def _write_default_config(config_dir: str) -> bool: """Write the default config.""" config_path = os.path.join(config_dir, YAML_CONFIG_FILE) secret_path = os.path.join(config_dir, SECRET_YAML) @@ -288,11 +287,11 @@ def _write_default_config(config_dir: str) -> Optional[str]: with open(scene_yaml_path, "wt"): pass - return config_path + return True except OSError: print("Unable to create default configuration file", config_path) - return None + return False async def async_hass_config_yaml(hass: HomeAssistant) -> Dict: @@ -300,35 +299,16 @@ async def async_hass_config_yaml(hass: HomeAssistant) -> Dict: This function allow a component inside the asyncio loop to reload its configuration by itself. Include package merge. - - This method is a coroutine. """ - - def _load_hass_yaml_config() -> Dict: - path = find_config_file(hass.config.config_dir) - if path is None: - raise HomeAssistantError( - f"Config file not found in: {hass.config.config_dir}" - ) - config = load_yaml_config_file(path) - return config - # Not using async_add_executor_job because this is an internal method. - config = await hass.loop.run_in_executor(None, _load_hass_yaml_config) + config = await hass.loop.run_in_executor( + None, load_yaml_config_file, hass.config.path(YAML_CONFIG_FILE) + ) core_config = config.get(CONF_CORE, {}) await merge_packages_config(hass, config, core_config.get(CONF_PACKAGES, {})) return config -def find_config_file(config_dir: Optional[str]) -> Optional[str]: - """Look in given directory for supported configuration files.""" - if config_dir is None: - return None - config_path = os.path.join(config_dir, YAML_CONFIG_FILE) - - return config_path if os.path.isfile(config_path) else None - - def load_yaml_config_file(config_path: str) -> Dict[Any, Any]: """Parse a YAML configuration file. @@ -382,8 +362,7 @@ def process_ha_config_upgrade(hass: HomeAssistant) -> None: if version_obj < LooseVersion("0.92"): # 0.92 moved google/tts.py to google_translate/tts.py - config_path = find_config_file(hass.config.config_dir) - assert config_path is not None + config_path = hass.config.path(YAML_CONFIG_FILE) with open(config_path, "rt", encoding="utf-8") as config_file: config_raw = config_file.read() diff --git a/homeassistant/helpers/check_config.py b/homeassistant/helpers/check_config.py index 6ac1326545a..0beeb4da4e8 100644 --- a/homeassistant/helpers/check_config.py +++ b/homeassistant/helpers/check_config.py @@ -1,5 +1,6 @@ """Helper to check the configuration file.""" from collections import OrderedDict +import os from typing import List, NamedTuple, Optional import attr @@ -10,10 +11,10 @@ from homeassistant.config import ( CONF_CORE, CONF_PACKAGES, CORE_CONFIG_SCHEMA, + YAML_CONFIG_FILE, _format_config_error, config_per_platform, extract_domain_configs, - find_config_file, load_yaml_config_file, merge_packages_config, ) @@ -62,7 +63,6 @@ async def async_check_ha_config_file(hass: HomeAssistant) -> HomeAssistantConfig This method is a coroutine. """ - config_dir = hass.config.config_dir result = HomeAssistantConfig() def _pack_error( @@ -79,9 +79,9 @@ async def async_check_ha_config_file(hass: HomeAssistant) -> HomeAssistantConfig result.add_error(_format_config_error(ex, domain, config), domain, config) # Load configuration.yaml + config_path = hass.config.path(YAML_CONFIG_FILE) try: - config_path = await hass.async_add_executor_job(find_config_file, config_dir) - if not config_path: + if not await hass.async_add_executor_job(os.path.isfile, config_path): return result.add_error("File configuration.yaml not found.") config = await hass.async_add_executor_job(load_yaml_config_file, config_path) except FileNotFoundError: diff --git a/homeassistant/scripts/ensure_config.py b/homeassistant/scripts/ensure_config.py index 0b5d1104997..10026127511 100644 --- a/homeassistant/scripts/ensure_config.py +++ b/homeassistant/scripts/ensure_config.py @@ -32,13 +32,14 @@ def run(args): os.makedirs(config_dir) hass = HomeAssistant() - config_path = hass.loop.run_until_complete(async_run(hass, config_dir)) + hass.config.config_dir = config_dir + config_path = hass.loop.run_until_complete(async_run(hass)) print("Configuration file:", config_path) return 0 -async def async_run(hass, config_dir): +async def async_run(hass): """Make sure config exists.""" - path = await config_util.async_ensure_config_exists(hass, config_dir) + path = await config_util.async_ensure_config_exists(hass) await hass.async_stop(force=True) return path diff --git a/homeassistant/setup.py b/homeassistant/setup.py index f97e5ae2363..2b96bb3ea9d 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -55,7 +55,7 @@ async def _async_process_dependencies( """Ensure all dependencies are set up.""" blacklisted = [dep for dep in dependencies if dep in loader.DEPENDENCY_BLACKLIST] - if blacklisted and name != "default_config": + if blacklisted and name not in ("default_config", "safe_mode"): _LOGGER.error( "Unable to set up dependencies of %s: " "found blacklisted dependencies: %s", diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index b0196fdfe60..49707ac66b0 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -496,14 +496,13 @@ async def test_reload_config_service(hass, calls, hass_admin_user, hass_read_onl } }, ): - with patch("homeassistant.config.find_config_file", return_value=""): - with pytest.raises(Unauthorized): - await common.async_reload(hass, Context(user_id=hass_read_only_user.id)) - await hass.async_block_till_done() - await common.async_reload(hass, Context(user_id=hass_admin_user.id)) - await hass.async_block_till_done() - # De-flake ?! + with pytest.raises(Unauthorized): + await common.async_reload(hass, Context(user_id=hass_read_only_user.id)) await hass.async_block_till_done() + await common.async_reload(hass, Context(user_id=hass_admin_user.id)) + await hass.async_block_till_done() + # De-flake ?! + await hass.async_block_till_done() assert hass.states.get("automation.hello") is None assert hass.states.get("automation.bye") is not None @@ -551,9 +550,8 @@ async def test_reload_config_when_invalid_config(hass, calls): autospec=True, return_value={automation.DOMAIN: "not valid"}, ): - with patch("homeassistant.config.find_config_file", return_value=""): - await common.async_reload(hass) - await hass.async_block_till_done() + await common.async_reload(hass) + await hass.async_block_till_done() assert hass.states.get("automation.hello") is None @@ -590,9 +588,8 @@ async def test_reload_config_handles_load_fails(hass, calls): "homeassistant.config.load_yaml_config_file", side_effect=HomeAssistantError("bla"), ): - with patch("homeassistant.config.find_config_file", return_value=""): - await common.async_reload(hass) - await hass.async_block_till_done() + await common.async_reload(hass) + await hass.async_block_till_done() assert hass.states.get("automation.hello") is not None diff --git a/tests/components/camera/test_init.py b/tests/components/camera/test_init.py index de48a1d48f3..7f5b2bd20b9 100644 --- a/tests/components/camera/test_init.py +++ b/tests/components/camera/test_init.py @@ -6,24 +6,15 @@ from unittest.mock import PropertyMock, mock_open, patch import pytest -from homeassistant.components import camera, http +from homeassistant.components import camera from homeassistant.components.camera.const import DOMAIN, PREF_PRELOAD_STREAM from homeassistant.components.camera.prefs import CameraEntityPreferences from homeassistant.components.websocket_api.const import TYPE_RESULT -from homeassistant.const import ( - ATTR_ENTITY_ID, - ATTR_ENTITY_PICTURE, - EVENT_HOMEASSISTANT_START, -) +from homeassistant.const import ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_START from homeassistant.exceptions import HomeAssistantError -from homeassistant.setup import async_setup_component, setup_component +from homeassistant.setup import async_setup_component -from tests.common import ( - assert_setup_component, - get_test_home_assistant, - get_test_instance_port, - mock_coro, -) +from tests.common import mock_coro from tests.components.camera import common @@ -55,96 +46,53 @@ def setup_camera_prefs(hass): return common.mock_camera_prefs(hass, "camera.demo_camera") -class TestSetupCamera: - """Test class for setup camera.""" - - def setup_method(self): - """Set up things to be run when tests are started.""" - self.hass = get_test_home_assistant() - - def teardown_method(self): - """Stop everything that was started.""" - self.hass.stop() - - def test_setup_component(self): - """Set up demo platform on camera component.""" - config = {camera.DOMAIN: {"platform": "demo"}} - - with assert_setup_component(1, camera.DOMAIN): - setup_component(self.hass, camera.DOMAIN, config) +@pytest.fixture +async def image_mock_url(hass): + """Fixture for get_image tests.""" + await async_setup_component( + hass, camera.DOMAIN, {camera.DOMAIN: {"platform": "demo"}} + ) -class TestGetImage: - """Test class for camera.""" +async def test_get_image_from_camera(hass, image_mock_url): + """Grab an image from camera entity.""" - def setup_method(self): - """Set up things to be run when tests are started.""" - self.hass = get_test_home_assistant() - - setup_component( - self.hass, - http.DOMAIN, - {http.DOMAIN: {http.CONF_SERVER_PORT: get_test_instance_port()}}, - ) - - config = {camera.DOMAIN: {"platform": "demo"}} - - setup_component(self.hass, camera.DOMAIN, config) - - state = self.hass.states.get("camera.demo_camera") - self.url = "{0}{1}".format( - self.hass.config.api.base_url, state.attributes.get(ATTR_ENTITY_PICTURE) - ) - - def teardown_method(self): - """Stop everything that was started.""" - self.hass.stop() - - @patch( + with patch( "homeassistant.components.demo.camera.DemoCamera.camera_image", autospec=True, return_value=b"Test", - ) - def test_get_image_from_camera(self, mock_camera): - """Grab an image from camera entity.""" - self.hass.start() + ) as mock_camera: + image = await camera.async_get_image(hass, "camera.demo_camera") - image = asyncio.run_coroutine_threadsafe( - camera.async_get_image(self.hass, "camera.demo_camera"), self.hass.loop - ).result() + assert mock_camera.called + assert image.content == b"Test" - assert mock_camera.called - assert image.content == b"Test" - def test_get_image_without_exists_camera(self): - """Try to get image without exists camera.""" - with patch( - "homeassistant.helpers.entity_component.EntityComponent.get_entity", - return_value=None, - ), pytest.raises(HomeAssistantError): - asyncio.run_coroutine_threadsafe( - camera.async_get_image(self.hass, "camera.demo_camera"), self.hass.loop - ).result() +async def test_get_image_without_exists_camera(hass, image_mock_url): + """Try to get image without exists camera.""" + with patch( + "homeassistant.helpers.entity_component.EntityComponent.get_entity", + return_value=None, + ), pytest.raises(HomeAssistantError): + await camera.async_get_image(hass, "camera.demo_camera") - def test_get_image_with_timeout(self): - """Try to get image with timeout.""" - with patch( - "homeassistant.components.camera.Camera.async_camera_image", - side_effect=asyncio.TimeoutError, - ), pytest.raises(HomeAssistantError): - asyncio.run_coroutine_threadsafe( - camera.async_get_image(self.hass, "camera.demo_camera"), self.hass.loop - ).result() - def test_get_image_fails(self): - """Try to get image with timeout.""" - with patch( - "homeassistant.components.camera.Camera.async_camera_image", - return_value=mock_coro(None), - ), pytest.raises(HomeAssistantError): - asyncio.run_coroutine_threadsafe( - camera.async_get_image(self.hass, "camera.demo_camera"), self.hass.loop - ).result() +async def test_get_image_with_timeout(hass, image_mock_url): + """Try to get image with timeout.""" + with patch( + "homeassistant.components.camera.Camera.async_camera_image", + side_effect=asyncio.TimeoutError, + ), pytest.raises(HomeAssistantError): + await camera.async_get_image(hass, "camera.demo_camera") + + +async def test_get_image_fails(hass, image_mock_url): + """Try to get image with timeout.""" + with patch( + "homeassistant.components.camera.Camera.async_camera_image", + return_value=mock_coro(None), + ), pytest.raises(HomeAssistantError): + await camera.async_get_image(hass, "camera.demo_camera") async def test_snapshot_service(hass, mock_camera): diff --git a/tests/components/emulated_hue/test_upnp.py b/tests/components/emulated_hue/test_upnp.py index 5897b80659a..ea002275153 100644 --- a/tests/components/emulated_hue/test_upnp.py +++ b/tests/components/emulated_hue/test_upnp.py @@ -4,10 +4,11 @@ import unittest from unittest.mock import patch from aiohttp.hdrs import CONTENT_TYPE +import defusedxml.ElementTree as ET import requests from homeassistant import const, setup -from homeassistant.components import emulated_hue, http +from homeassistant.components import emulated_hue from tests.common import get_test_home_assistant, get_test_instance_port @@ -28,10 +29,6 @@ class TestEmulatedHue(unittest.TestCase): """Set up the class.""" cls.hass = hass = get_test_home_assistant() - setup.setup_component( - hass, http.DOMAIN, {http.DOMAIN: {http.CONF_SERVER_PORT: HTTP_SERVER_PORT}} - ) - with patch("homeassistant.components.emulated_hue.UPNPResponderThread"): setup.setup_component( hass, @@ -52,8 +49,6 @@ class TestEmulatedHue(unittest.TestCase): def test_description_xml(self): """Test the description.""" - import defusedxml.ElementTree as ET - result = requests.get(BRIDGE_URL_BASE.format("/description.xml"), timeout=5) assert result.status_code == 200 diff --git a/tests/components/frontend/test_init.py b/tests/components/frontend/test_init.py index bd22730e82f..f9f25192211 100644 --- a/tests/components/frontend/test_init.py +++ b/tests/components/frontend/test_init.py @@ -1,7 +1,7 @@ """The tests for Home Assistant frontend.""" import re -from unittest.mock import patch +from asynctest import patch import pytest from homeassistant.components.frontend import ( @@ -173,7 +173,7 @@ async def test_themes_reload_themes(hass, hass_ws_client): client = await hass_ws_client(hass) with patch( - "homeassistant.components.frontend.load_yaml_config_file", + "homeassistant.components.frontend.async_hass_config_yaml", return_value={DOMAIN: {CONF_THEMES: {"sad": {"primary-color": "blue"}}}}, ): await hass.services.async_call( diff --git a/tests/components/group/test_init.py b/tests/components/group/test_init.py index ee52a551cb8..febe261c9e4 100644 --- a/tests/components/group/test_init.py +++ b/tests/components/group/test_init.py @@ -429,9 +429,8 @@ class TestComponentsGroup(unittest.TestCase): } }, ): - with patch("homeassistant.config.find_config_file", return_value=""): - common.reload(self.hass) - self.hass.block_till_done() + common.reload(self.hass) + self.hass.block_till_done() assert sorted(self.hass.states.entity_ids()) == [ "group.all_tests", diff --git a/tests/components/homeassistant/test_scene.py b/tests/components/homeassistant/test_scene.py index 5df6bd6ad52..c423f66c7b8 100644 --- a/tests/components/homeassistant/test_scene.py +++ b/tests/components/homeassistant/test_scene.py @@ -18,7 +18,7 @@ async def test_reload_config_service(hass): "homeassistant.config.load_yaml_config_file", autospec=True, return_value={"scene": {"name": "Hallo", "entities": {"light.kitchen": "on"}}}, - ), patch("homeassistant.config.find_config_file", return_value=""): + ): await hass.services.async_call("scene", "reload", blocking=True) await hass.async_block_till_done() @@ -28,7 +28,7 @@ async def test_reload_config_service(hass): "homeassistant.config.load_yaml_config_file", autospec=True, return_value={"scene": {"name": "Bye", "entities": {"light.kitchen": "on"}}}, - ), patch("homeassistant.config.find_config_file", return_value=""): + ): await hass.services.async_call("scene", "reload", blocking=True) await hass.async_block_till_done() diff --git a/tests/components/http/test_init.py b/tests/components/http/test_init.py index 212ae7499ab..43a39302f4f 100644 --- a/tests/components/http/test_init.py +++ b/tests/components/http/test_init.py @@ -240,3 +240,16 @@ async def test_cors_defaults(hass): assert len(mock_setup.mock_calls) == 1 assert mock_setup.mock_calls[0][1][1] == ["https://cast.home-assistant.io"] + + +async def test_storing_config(hass, aiohttp_client, aiohttp_unused_port): + """Test that we store last working config.""" + config = {http.CONF_SERVER_PORT: aiohttp_unused_port()} + + await async_setup_component(hass, http.DOMAIN, {http.DOMAIN: config}) + + await hass.async_start() + + assert await hass.components.http.async_get_last_config() == http.HTTP_SCHEMA( + config + ) diff --git a/tests/components/image_processing/test_init.py b/tests/components/image_processing/test_init.py index 3503fcfb9a2..39cbb8d583e 100644 --- a/tests/components/image_processing/test_init.py +++ b/tests/components/image_processing/test_init.py @@ -77,8 +77,6 @@ class TestImageProcessing: ) def test_get_image_from_camera(self, mock_camera): """Grab an image from camera entity.""" - self.hass.start() - common.scan(self.hass, entity_id="image_processing.test") self.hass.block_till_done() diff --git a/tests/components/input_boolean/test_init.py b/tests/components/input_boolean/test_init.py index 1ab7e9a3d13..c9f894656ea 100644 --- a/tests/components/input_boolean/test_init.py +++ b/tests/components/input_boolean/test_init.py @@ -240,14 +240,13 @@ async def test_reload(hass, hass_admin_user): } }, ): - with patch("homeassistant.config.find_config_file", return_value=""): - await hass.services.async_call( - DOMAIN, - SERVICE_RELOAD, - blocking=True, - context=Context(user_id=hass_admin_user.id), - ) - await hass.async_block_till_done() + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + blocking=True, + context=Context(user_id=hass_admin_user.id), + ) + await hass.async_block_till_done() assert count_start + 2 == len(hass.states.async_entity_ids()) diff --git a/tests/components/input_datetime/test_init.py b/tests/components/input_datetime/test_init.py index 6908c4fc5f1..fdf6e9296be 100644 --- a/tests/components/input_datetime/test_init.py +++ b/tests/components/input_datetime/test_init.py @@ -345,21 +345,20 @@ async def test_reload(hass, hass_admin_user, hass_read_only_user): } }, ): - with patch("homeassistant.config.find_config_file", return_value=""): - with pytest.raises(Unauthorized): - await hass.services.async_call( - DOMAIN, - SERVICE_RELOAD, - blocking=True, - context=Context(user_id=hass_read_only_user.id), - ) + with pytest.raises(Unauthorized): await hass.services.async_call( DOMAIN, SERVICE_RELOAD, blocking=True, - context=Context(user_id=hass_admin_user.id), + context=Context(user_id=hass_read_only_user.id), ) - await hass.async_block_till_done() + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + blocking=True, + context=Context(user_id=hass_admin_user.id), + ) + await hass.async_block_till_done() assert count_start + 2 == len(hass.states.async_entity_ids()) diff --git a/tests/components/input_number/test_init.py b/tests/components/input_number/test_init.py index f9763168354..4005268c5ba 100644 --- a/tests/components/input_number/test_init.py +++ b/tests/components/input_number/test_init.py @@ -338,21 +338,20 @@ async def test_reload(hass, hass_admin_user, hass_read_only_user): } }, ): - with patch("homeassistant.config.find_config_file", return_value=""): - with pytest.raises(Unauthorized): - await hass.services.async_call( - DOMAIN, - SERVICE_RELOAD, - blocking=True, - context=Context(user_id=hass_read_only_user.id), - ) + with pytest.raises(Unauthorized): await hass.services.async_call( DOMAIN, SERVICE_RELOAD, blocking=True, - context=Context(user_id=hass_admin_user.id), + context=Context(user_id=hass_read_only_user.id), ) - await hass.async_block_till_done() + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + blocking=True, + context=Context(user_id=hass_admin_user.id), + ) + await hass.async_block_till_done() assert count_start + 2 == len(hass.states.async_entity_ids()) diff --git a/tests/components/input_select/test_init.py b/tests/components/input_select/test_init.py index 13669ea507f..a3856277704 100644 --- a/tests/components/input_select/test_init.py +++ b/tests/components/input_select/test_init.py @@ -415,21 +415,20 @@ async def test_reload(hass, hass_admin_user, hass_read_only_user): } }, ): - with patch("homeassistant.config.find_config_file", return_value=""): - with pytest.raises(Unauthorized): - await hass.services.async_call( - DOMAIN, - SERVICE_RELOAD, - blocking=True, - context=Context(user_id=hass_read_only_user.id), - ) + with pytest.raises(Unauthorized): await hass.services.async_call( DOMAIN, SERVICE_RELOAD, blocking=True, - context=Context(user_id=hass_admin_user.id), + context=Context(user_id=hass_read_only_user.id), ) - await hass.async_block_till_done() + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + blocking=True, + context=Context(user_id=hass_admin_user.id), + ) + await hass.async_block_till_done() assert count_start + 2 == len(hass.states.async_entity_ids()) diff --git a/tests/components/input_text/test_init.py b/tests/components/input_text/test_init.py index d6478a5472f..41f94b51732 100644 --- a/tests/components/input_text/test_init.py +++ b/tests/components/input_text/test_init.py @@ -288,21 +288,20 @@ async def test_reload(hass, hass_admin_user, hass_read_only_user): } }, ): - with patch("homeassistant.config.find_config_file", return_value=""): - with pytest.raises(Unauthorized): - await hass.services.async_call( - DOMAIN, - SERVICE_RELOAD, - blocking=True, - context=Context(user_id=hass_read_only_user.id), - ) + with pytest.raises(Unauthorized): await hass.services.async_call( DOMAIN, SERVICE_RELOAD, blocking=True, - context=Context(user_id=hass_admin_user.id), + context=Context(user_id=hass_read_only_user.id), ) - await hass.async_block_till_done() + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + blocking=True, + context=Context(user_id=hass_admin_user.id), + ) + await hass.async_block_till_done() assert count_start + 2 == len(hass.states.async_entity_ids()) diff --git a/tests/components/logbook/test_init.py b/tests/components/logbook/test_init.py index 1b48f301529..70e769a54f2 100644 --- a/tests/components/logbook/test_init.py +++ b/tests/components/logbook/test_init.py @@ -48,7 +48,6 @@ class TestComponentLogbook(unittest.TestCase): self.hass = get_test_home_assistant() init_recorder_component(self.hass) # Force an in memory DB assert setup_component(self.hass, logbook.DOMAIN, self.EMPTY_CONFIG) - self.hass.start() def tearDown(self): """Stop everything that was started.""" @@ -90,7 +89,7 @@ class TestComponentLogbook(unittest.TestCase): dt_util.utcnow() + timedelta(hours=1), ) ) - assert len(events) == 2 + assert len(events) == 1 assert 1 == len(calls) last_call = calls[-1] diff --git a/tests/components/manual_mqtt/test_alarm_control_panel.py b/tests/components/manual_mqtt/test_alarm_control_panel.py index 91e97685588..9567391e273 100644 --- a/tests/components/manual_mqtt/test_alarm_control_panel.py +++ b/tests/components/manual_mqtt/test_alarm_control_panel.py @@ -269,9 +269,6 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): entity_id = "alarm_control_panel.test" - self.hass.start() - self.hass.block_till_done() - assert STATE_ALARM_DISARMED == self.hass.states.get(entity_id).state common.alarm_arm_home(self.hass, "abc") @@ -1471,9 +1468,6 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): entity_id = "alarm_control_panel.test" - self.hass.start() - self.hass.block_till_done() - assert STATE_ALARM_DISARMED == self.hass.states.get(entity_id).state common.alarm_arm_home(self.hass, "def") diff --git a/tests/components/person/test_init.py b/tests/components/person/test_init.py index 699fb58a539..e5a414d95ad 100644 --- a/tests/components/person/test_init.py +++ b/tests/components/person/test_init.py @@ -753,7 +753,7 @@ async def test_reload(hass, hass_admin_user): {"name": "Person 3", "id": "id-3"}, ] }, - ), patch("homeassistant.config.find_config_file", return_value=""): + ): await hass.services.async_call( DOMAIN, SERVICE_RELOAD, diff --git a/tests/components/safe_mode/__init__.py b/tests/components/safe_mode/__init__.py new file mode 100644 index 00000000000..3732fef17cb --- /dev/null +++ b/tests/components/safe_mode/__init__.py @@ -0,0 +1 @@ +"""Tests for the Safe Mode integration.""" diff --git a/tests/components/safe_mode/test_init.py b/tests/components/safe_mode/test_init.py new file mode 100644 index 00000000000..a069ce90b17 --- /dev/null +++ b/tests/components/safe_mode/test_init.py @@ -0,0 +1,9 @@ +"""Tests for safe mode integration.""" +from homeassistant.setup import async_setup_component + + +async def test_works(hass): + """Test safe mode works.""" + assert await async_setup_component(hass, "safe_mode", {}) + await hass.async_block_till_done() + assert len(hass.states.async_entity_ids()) == 1 diff --git a/tests/components/script/test_init.py b/tests/components/script/test_init.py index e008984f47c..cb66c26b6a3 100644 --- a/tests/components/script/test_init.py +++ b/tests/components/script/test_init.py @@ -229,9 +229,8 @@ class TestScriptComponent(unittest.TestCase): "script": {"test2": {"sequence": [{"delay": {"seconds": 5}}]}} }, ): - with patch("homeassistant.config.find_config_file", return_value=""): - reload(self.hass) - self.hass.block_till_done() + reload(self.hass) + self.hass.block_till_done() assert self.hass.states.get(ENTITY_ID) is None assert not self.hass.services.has_service(script.DOMAIN, "test") @@ -262,7 +261,6 @@ async def test_service_descriptions(hass): assert not descriptions[DOMAIN]["test"]["fields"] # Test 2: has "fields" but no "description" - await hass.services.async_call(DOMAIN, SERVICE_RELOAD, blocking=True) with patch( "homeassistant.config.load_yaml_config_file", return_value={ @@ -279,8 +277,7 @@ async def test_service_descriptions(hass): } }, ): - with patch("homeassistant.config.find_config_file", return_value=""): - await hass.services.async_call(DOMAIN, SERVICE_RELOAD, blocking=True) + await hass.services.async_call(DOMAIN, SERVICE_RELOAD, blocking=True) descriptions = await async_get_all_descriptions(hass) diff --git a/tests/components/timer/test_init.py b/tests/components/timer/test_init.py index 56675c9d893..bfb1f8fdd30 100644 --- a/tests/components/timer/test_init.py +++ b/tests/components/timer/test_init.py @@ -259,21 +259,20 @@ async def test_config_reload(hass, hass_admin_user, hass_read_only_user): } }, ): - with patch("homeassistant.config.find_config_file", return_value=""): - with pytest.raises(Unauthorized): - await hass.services.async_call( - DOMAIN, - SERVICE_RELOAD, - blocking=True, - context=Context(user_id=hass_read_only_user.id), - ) + with pytest.raises(Unauthorized): await hass.services.async_call( DOMAIN, SERVICE_RELOAD, blocking=True, - context=Context(user_id=hass_admin_user.id), + context=Context(user_id=hass_read_only_user.id), ) - await hass.async_block_till_done() + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + blocking=True, + context=Context(user_id=hass_admin_user.id), + ) + await hass.async_block_till_done() assert count_start + 2 == len(hass.states.async_entity_ids()) diff --git a/tests/components/tts/test_init.py b/tests/components/tts/test_init.py index f8dc11069d8..6aafe29901d 100644 --- a/tests/components/tts/test_init.py +++ b/tests/components/tts/test_init.py @@ -24,6 +24,7 @@ from tests.common import ( get_test_home_assistant, get_test_instance_port, mock_service, + mock_storage, ) @@ -45,6 +46,8 @@ class TestTTS: self.hass = get_test_home_assistant() self.demo_provider = DemoProvider("en") self.default_tts_cache = self.hass.config.path(tts.DEFAULT_CACHE_DIR) + self.mock_storage = mock_storage() + self.mock_storage.__enter__() setup_component( self.hass, @@ -55,6 +58,7 @@ class TestTTS: def teardown_method(self): """Stop everything that was started.""" self.hass.stop() + self.mock_storage.__exit__(None, None, None) if os.path.isdir(self.default_tts_cache): shutil.rmtree(self.default_tts_cache) diff --git a/tests/scripts/test_check_config.py b/tests/scripts/test_check_config.py index ea7ae03b5db..737c3b56ecf 100644 --- a/tests/scripts/test_check_config.py +++ b/tests/scripts/test_check_config.py @@ -120,7 +120,7 @@ def test_secrets(isfile_patch, loop): @patch("os.path.isfile", return_value=True) def test_package_invalid(isfile_patch, loop): - """Test a valid platform setup.""" + """Test an invalid package.""" files = { YAML_CONFIG_FILE: BASE_CONFIG + (" packages:\n p1:\n" ' group: ["a"]') } diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index e64672a1e88..48c5360d888 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -3,18 +3,23 @@ import asyncio import logging import os -from unittest.mock import Mock, patch +from unittest.mock import Mock + +from asynctest import patch +import pytest from homeassistant import bootstrap import homeassistant.config as config_util +from homeassistant.exceptions import HomeAssistantError import homeassistant.util.dt as dt_util from tests.common import ( + MockConfigEntry, MockModule, + flush_store, get_test_config_dir, mock_coro, mock_integration, - patch_yaml_files, ) ORIG_TIMEZONE = dt_util.DEFAULT_TIME_ZONE @@ -23,26 +28,6 @@ VERSION_PATH = os.path.join(get_test_config_dir(), config_util.VERSION_FILE) _LOGGER = logging.getLogger(__name__) -# prevent .HA_VERSION file from being written -@patch("homeassistant.bootstrap.conf_util.process_ha_config_upgrade", Mock()) -@patch( - "homeassistant.util.location.async_detect_location_info", - Mock(return_value=mock_coro(None)), -) -@patch("os.path.isfile", Mock(return_value=True)) -@patch("os.access", Mock(return_value=True)) -@patch("homeassistant.bootstrap.async_enable_logging", Mock(return_value=True)) -def test_from_config_file(hass): - """Test with configuration file.""" - components = set(["browser", "conversation", "script"]) - files = {"config.yaml": "".join(f"{comp}:\n" for comp in components)} - - with patch_yaml_files(files, True): - yield from bootstrap.async_from_config_file("config.yaml", hass) - - assert components == hass.config.components - - @patch("homeassistant.bootstrap.async_enable_logging", Mock()) @asyncio.coroutine def test_home_assistant_core_config_validation(hass): @@ -54,33 +39,6 @@ def test_home_assistant_core_config_validation(hass): assert result is None -async def test_async_from_config_file_not_mount_deps_folder(loop): - """Test that we not mount the deps folder inside async_from_config_file.""" - hass = Mock(async_add_executor_job=Mock(side_effect=lambda *args: mock_coro())) - - with patch("homeassistant.bootstrap.is_virtual_env", return_value=False), patch( - "homeassistant.bootstrap.async_enable_logging", return_value=mock_coro() - ), patch( - "homeassistant.bootstrap.async_mount_local_lib_path", return_value=mock_coro() - ) as mock_mount, patch( - "homeassistant.bootstrap.async_from_config_dict", return_value=mock_coro() - ): - - await bootstrap.async_from_config_file("mock-path", hass) - assert len(mock_mount.mock_calls) == 1 - - with patch("homeassistant.bootstrap.is_virtual_env", return_value=True), patch( - "homeassistant.bootstrap.async_enable_logging", return_value=mock_coro() - ), patch( - "homeassistant.bootstrap.async_mount_local_lib_path", return_value=mock_coro() - ) as mock_mount, patch( - "homeassistant.bootstrap.async_from_config_dict", return_value=mock_coro() - ): - - await bootstrap.async_from_config_file("mock-path", hass) - assert len(mock_mount.mock_calls) == 0 - - async def test_load_hassio(hass): """Test that we load Hass.io component.""" with patch.dict(os.environ, {}, clear=True): @@ -233,3 +191,169 @@ async def test_setup_after_deps_not_present(hass, caplog): 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(): + """Mock enable logging.""" + with patch( + "homeassistant.bootstrap.is_virtual_env", return_value=False + ) as is_virtual_env: + yield is_virtual_env + + +@pytest.fixture +def mock_enable_logging(): + """Mock enable logging.""" + with patch("homeassistant.bootstrap.async_enable_logging") as enable_logging: + yield enable_logging + + +@pytest.fixture +def mock_mount_local_lib_path(): + """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(): + """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(): + """Mock enable logging.""" + with patch( + "homeassistant.config.async_ensure_config_exists", return_value=True + ) as ensure_config_exists: + yield ensure_config_exists + + +async def test_setup_hass( + mock_enable_logging, + mock_is_virtual_env, + mock_mount_local_lib_path, + mock_ensure_config_exists, + mock_process_ha_config_upgrade, +): + """Test it works.""" + verbose = Mock() + log_rotate_days = Mock() + log_file = Mock() + log_no_color = Mock() + + with patch( + "homeassistant.config.async_hass_config_yaml", return_value={"browser": {}} + ): + hass = await bootstrap.async_setup_hass( + 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, + safe_mode=False, + ) + + assert "browser" 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 + + +async def test_setup_hass_invalid_yaml( + mock_enable_logging, + mock_is_virtual_env, + mock_mount_local_lib_path, + mock_ensure_config_exists, + mock_process_ha_config_upgrade, +): + """Test it works.""" + with patch( + "homeassistant.config.async_hass_config_yaml", side_effect=HomeAssistantError + ): + hass = await bootstrap.async_setup_hass( + config_dir=get_test_config_dir(), + verbose=False, + log_rotate_days=10, + log_file="", + log_no_color=False, + skip_pip=True, + safe_mode=False, + ) + + assert "safe_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_is_virtual_env, + mock_mount_local_lib_path, + mock_ensure_config_exists, + mock_process_ha_config_upgrade, +): + """Test it works.""" + mock_ensure_config_exists.return_value = False + + assert ( + await bootstrap.async_setup_hass( + config_dir=get_test_config_dir(), + verbose=False, + log_rotate_days=10, + log_file="", + log_no_color=False, + skip_pip=True, + safe_mode=False, + ) + is None + ) + + +async def test_setup_hass_safe_mode( + hass, + mock_enable_logging, + mock_is_virtual_env, + mock_mount_local_lib_path, + mock_ensure_config_exists, + mock_process_ha_config_upgrade, +): + """Test it works.""" + # Add a config entry to storage. + MockConfigEntry(domain="browser").add_to_hass(hass) + hass.config_entries._async_schedule_save() + await flush_store(hass.config_entries._store) + + with patch("homeassistant.components.browser.setup") as browser_setup: + hass = await bootstrap.async_setup_hass( + config_dir=get_test_config_dir(), + verbose=False, + log_rotate_days=10, + log_file="", + log_no_color=False, + skip_pip=True, + safe_mode=True, + ) + + assert "safe_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 diff --git a/tests/test_config.py b/tests/test_config.py index b4f7a916d37..8b6d8addb30 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -82,7 +82,7 @@ def teardown(): async def test_create_default_config(hass): """Test creation of default config.""" - await config_util.async_create_default_config(hass, CONFIG_DIR) + await config_util.async_create_default_config(hass) assert os.path.isfile(YAML_PATH) assert os.path.isfile(SECRET_PATH) @@ -91,20 +91,13 @@ async def test_create_default_config(hass): assert os.path.isfile(AUTOMATIONS_PATH) -def test_find_config_file_yaml(): - """Test if it finds a YAML config file.""" - create_file(YAML_PATH) - - assert YAML_PATH == config_util.find_config_file(CONFIG_DIR) - - async def test_ensure_config_exists_creates_config(hass): """Test that calling ensure_config_exists. If not creates a new config file. """ with mock.patch("builtins.print") as mock_print: - await config_util.async_ensure_config_exists(hass, CONFIG_DIR) + await config_util.async_ensure_config_exists(hass) assert os.path.isfile(YAML_PATH) assert mock_print.called @@ -113,7 +106,7 @@ async def test_ensure_config_exists_creates_config(hass): async def test_ensure_config_exists_uses_existing_config(hass): """Test that calling ensure_config_exists uses existing config.""" create_file(YAML_PATH) - await config_util.async_ensure_config_exists(hass, CONFIG_DIR) + await config_util.async_ensure_config_exists(hass) with open(YAML_PATH) as f: content = f.read() @@ -172,13 +165,9 @@ async def test_create_default_config_returns_none_if_write_error(hass): Non existing folder returns None. """ + hass.config.config_dir = os.path.join(CONFIG_DIR, "non_existing_dir/") with mock.patch("builtins.print") as mock_print: - assert ( - await config_util.async_create_default_config( - hass, os.path.join(CONFIG_DIR, "non_existing_dir/") - ) - is None - ) + assert await config_util.async_create_default_config(hass) is False assert mock_print.called @@ -331,7 +320,6 @@ def test_config_upgrade_same_version(hass): assert opened_file.write.call_count == 0 -@mock.patch("homeassistant.config.find_config_file", mock.Mock()) def test_config_upgrade_no_file(hass): """Test update of version on upgrade, with no version file.""" mock_open = mock.mock_open()