Protect loop set default executor (#37438)

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
Paulus Schoutsen 2020-07-06 15:58:53 -07:00 committed by GitHub
parent f8651d9faa
commit f49ce5d1b4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 387 additions and 244 deletions

View File

@ -1,6 +1,5 @@
"""Start Home Assistant."""
import argparse
import asyncio
import os
import platform
import subprocess
@ -8,32 +7,9 @@ import sys
import threading
from typing import List
import yarl
from homeassistant.const import REQUIRED_PYTHON_VER, RESTART_EXIT_CODE, __version__
def set_loop() -> None:
"""Attempt to use different loop."""
# pylint: disable=import-outside-toplevel
from asyncio.events import BaseDefaultEventLoopPolicy
if sys.platform == "win32":
if hasattr(asyncio, "WindowsProactorEventLoopPolicy"):
# pylint: disable=no-member
policy = asyncio.WindowsProactorEventLoopPolicy()
else:
class ProactorPolicy(BaseDefaultEventLoopPolicy):
"""Event loop policy to create proactor loops."""
_loop_factory = asyncio.ProactorEventLoop
policy = ProactorPolicy()
asyncio.set_event_loop_policy(policy)
def validate_python() -> None:
"""Validate that the right Python version is running."""
if sys.version_info[:3] < REQUIRED_PYTHON_VER:
@ -240,39 +216,6 @@ def cmdline() -> List[str]:
return [arg for arg in sys.argv if arg != "--daemon"]
async def setup_and_run_hass(config_dir: str, args: argparse.Namespace) -> int:
"""Set up Home Assistant and run."""
# pylint: disable=import-outside-toplevel
from homeassistant import bootstrap
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 hass is None:
return 1
if args.open_ui:
import webbrowser # pylint: disable=import-outside-toplevel
if hass.config.api is not None:
scheme = "https" if hass.config.api.use_ssl else "http"
url = str(
yarl.URL.build(
scheme=scheme, host="127.0.0.1", port=hass.config.api.port
)
)
hass.add_job(webbrowser.open, url)
return await hass.async_run()
def try_to_restart() -> None:
"""Attempt to clean up state and start a new Home Assistant instance."""
# Things should be mostly shut down already at this point, now just try
@ -319,8 +262,6 @@ def main() -> int:
"""Start Home Assistant."""
validate_python()
set_loop()
# Run a simple daemon runner process on Windows to handle restarts
if os.name == "nt" and "--runner" not in sys.argv:
nt_args = cmdline() + ["--runner"]
@ -353,7 +294,22 @@ def main() -> int:
if args.pid_file:
write_pid(args.pid_file)
exit_code = asyncio.run(setup_and_run_hass(config_dir, args), debug=args.debug)
# pylint: disable=import-outside-toplevel
from homeassistant import runner
runtime_conf = runner.RuntimeConfig(
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,
debug=args.debug,
open_ui=args.open_ui,
)
exit_code = runner.run(runtime_conf)
if exit_code == RESTART_EXIT_CODE and not args.runner:
try_to_restart()

View File

@ -7,10 +7,11 @@ import logging.handlers
import os
import sys
from time import monotonic
from typing import Any, Dict, Optional, Set
from typing import TYPE_CHECKING, Any, Dict, Optional, Set
from async_timeout import timeout
import voluptuous as vol
import yarl
from homeassistant import config as conf_util, config_entries, core, loader
from homeassistant.components import http
@ -31,6 +32,9 @@ from homeassistant.util.logging import async_activate_log_queue_handler
from homeassistant.util.package import async_get_user_site, is_virtual_env
from homeassistant.util.yaml import clear_secret_cache
if TYPE_CHECKING:
from .runner import RuntimeConfig
_LOGGER = logging.getLogger(__name__)
ERROR_LOG_FILENAME = "home-assistant.log"
@ -66,23 +70,22 @@ 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,
runtime_config: "RuntimeConfig",
) -> Optional[core.HomeAssistant]:
"""Set up Home Assistant."""
hass = core.HomeAssistant()
hass.config.config_dir = config_dir
hass.config.config_dir = runtime_config.config_dir
async_enable_logging(hass, verbose, log_rotate_days, log_file, log_no_color)
async_enable_logging(
hass,
runtime_config.verbose,
runtime_config.log_rotate_days,
runtime_config.log_file,
runtime_config.log_no_color,
)
hass.config.skip_pip = skip_pip
if skip_pip:
hass.config.skip_pip = runtime_config.skip_pip
if runtime_config.skip_pip:
_LOGGER.warning(
"Skipping pip installation of required modules. This may cause issues"
)
@ -91,10 +94,11 @@ async def async_setup_hass(
_LOGGER.error("Error getting configuration path")
return None
_LOGGER.info("Config directory: %s", config_dir)
_LOGGER.info("Config directory: %s", runtime_config.config_dir)
config_dict = None
basic_setup_success = False
safe_mode = runtime_config.safe_mode
if not safe_mode:
await hass.async_add_executor_job(conf_util.process_ha_config_upgrade, hass)
@ -107,7 +111,7 @@ async def async_setup_hass(
)
else:
if not is_virtual_env():
await async_mount_local_lib_path(config_dir)
await async_mount_local_lib_path(runtime_config.config_dir)
basic_setup_success = (
await async_from_config_dict(config_dict, hass) is not None
@ -153,9 +157,32 @@ async def async_setup_hass(
{"safe_mode": {}, "http": http_conf}, hass,
)
if runtime_config.open_ui:
hass.add_job(open_hass_ui, hass)
return hass
def open_hass_ui(hass: core.HomeAssistant) -> None:
"""Open the UI."""
import webbrowser # pylint: disable=import-outside-toplevel
if hass.config.api is None or "frontend" not in hass.config.components:
_LOGGER.warning("Cannot launch the UI because frontend not loaded")
return
scheme = "https" if hass.config.api.use_ssl else "http"
url = str(
yarl.URL.build(scheme=scheme, host="127.0.0.1", port=hass.config.api.port)
)
if not webbrowser.open(url):
_LOGGER.warning(
"Unable to open the Home Assistant UI in a browser. Open it yourself at %s",
url,
)
async def async_from_config_dict(
config: ConfigType, hass: core.HomeAssistant
) -> Optional[core.HomeAssistant]:

View File

@ -7,8 +7,8 @@ import voluptuous as vol
from homeassistant.config_entries import CONN_CLASS_LOCAL_POLL, ConfigFlow
from homeassistant.const import CONF_HOST, CONF_PORT
from homeassistant.helpers import ConfigType
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.typing import ConfigType
from .const import ( # pylint:disable=unused-import
CONF_DEVICE_IDENT,

View File

@ -8,7 +8,7 @@ import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.helpers import ConfigType
from homeassistant.helpers.typing import ConfigType
from .const import DOMAIN # pylint: disable=unused-import
from .utils import load_plum

View File

@ -5,7 +5,6 @@ Home Assistant is a Home Automation framework for observing the state
of entities and react to changes.
"""
import asyncio
from concurrent.futures import ThreadPoolExecutor
import datetime
import enum
import functools
@ -145,19 +144,6 @@ def is_callback(func: Callable[..., Any]) -> bool:
return getattr(func, "_hass_callback", False) is True
@callback
def async_loop_exception_handler(_: Any, context: Dict) -> None:
"""Handle all exception inside the core loop."""
kwargs = {}
exception = context.get("exception")
if exception:
kwargs["exc_info"] = (type(exception), exception, exception.__traceback__)
_LOGGER.error(
"Error doing job: %s", context["message"], **kwargs # type: ignore
)
class CoreState(enum.Enum):
"""Represent the current state of Home Assistant."""
@ -179,18 +165,9 @@ class HomeAssistant:
http: "HomeAssistantHTTP" = None # type: ignore
config_entries: "ConfigEntries" = None # type: ignore
def __init__(self, loop: Optional[asyncio.events.AbstractEventLoop] = None) -> None:
def __init__(self) -> None:
"""Initialize new Home Assistant object."""
self.loop: asyncio.events.AbstractEventLoop = (loop or asyncio.get_event_loop())
executor_opts: Dict[str, Any] = {
"max_workers": None,
"thread_name_prefix": "SyncWorker",
}
self.executor = ThreadPoolExecutor(**executor_opts)
self.loop.set_default_executor(self.executor)
self.loop.set_exception_handler(async_loop_exception_handler)
self.loop = asyncio.get_running_loop()
self._pending_tasks: list = []
self._track_task = True
self.bus = EventBus(self)
@ -461,7 +438,9 @@ class HomeAssistant:
self.state = CoreState.not_running
self.bus.async_fire(EVENT_HOMEASSISTANT_CLOSE)
await self.async_block_till_done()
self.executor.shutdown()
# Python 3.9+ and backported in runner.py
await self.loop.shutdown_default_executor() # type: ignore
self.exit_code = exit_code

View File

@ -1,13 +1,14 @@
"""Helper methods for components within Home Assistant."""
import re
from typing import Any, Iterable, Sequence, Tuple
from typing import TYPE_CHECKING, Any, Iterable, Sequence, Tuple
from homeassistant.const import CONF_PLATFORM
from .typing import ConfigType
if TYPE_CHECKING:
from .typing import ConfigType
def config_per_platform(config: ConfigType, domain: str) -> Iterable[Tuple[Any, Any]]:
def config_per_platform(config: "ConfigType", domain: str) -> Iterable[Tuple[Any, Any]]:
"""Break a component config into different platforms.
For example, will find 'switch', 'switch 2', 'switch 3', .. etc
@ -31,7 +32,7 @@ def config_per_platform(config: ConfigType, domain: str) -> Iterable[Tuple[Any,
yield platform, item
def extract_domain_configs(config: ConfigType, domain: str) -> Sequence[str]:
def extract_domain_configs(config: "ConfigType", domain: str) -> Sequence[str]:
"""Extract keys from config for given domain name.
Async friendly.

View File

@ -13,7 +13,7 @@ import async_timeout
from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE, __version__
from homeassistant.core import Event, callback
from homeassistant.helpers.frame import MissingIntegrationFrame, get_integration_frame
from homeassistant.helpers.frame import warn_use
from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.loader import bind_hass
from homeassistant.util import ssl as ssl_util
@ -71,34 +71,9 @@ def async_create_clientsession(
connector=connector, headers={USER_AGENT: SERVER_SOFTWARE}, **kwargs,
)
async def patched_close() -> None:
"""Mock close to avoid integrations closing our session."""
try:
found_frame, integration, path = get_integration_frame()
except MissingIntegrationFrame:
# Did not source from an integration? Hard error.
raise RuntimeError(
"Detected closing of the Home Assistant aiohttp session in the Home Assistant core. "
"Please report this issue."
)
index = found_frame.filename.index(path)
if path == "custom_components/":
extra = " to the custom component author"
else:
extra = ""
_LOGGER.warning(
"Detected integration that closes the Home Assistant aiohttp session. "
"Please report issue%s for %s using this method at %s, line %s: %s",
extra,
integration,
found_frame.filename[index:],
found_frame.lineno,
found_frame.line.strip(),
)
clientsession.close = patched_close # type: ignore
clientsession.close = warn_use( # type: ignore
clientsession.close, "closes the Home Assistant aiohttp session"
)
if auto_cleanup:
_async_register_clientsession_shutdown(hass, clientsession)

View File

@ -1,9 +1,16 @@
"""Provide frame helper for finding the current frame context."""
import asyncio
import functools
import logging
from traceback import FrameSummary, extract_stack
from typing import Tuple
from typing import Any, Callable, Tuple, TypeVar, cast
from homeassistant.exceptions import HomeAssistantError
_LOGGER = logging.getLogger(__name__)
CALLABLE_T = TypeVar("CALLABLE_T", bound=Callable) # pylint: disable=invalid-name
def get_integration_frame() -> Tuple[FrameSummary, str, str]:
"""Return the frame, integration and integration path of the current stack frame."""
@ -34,3 +41,49 @@ def get_integration_frame() -> Tuple[FrameSummary, str, str]:
class MissingIntegrationFrame(HomeAssistantError):
"""Raised when no integration is found in the frame."""
def report(what: str) -> None:
"""Report incorrect usage.
Async friendly.
"""
try:
found_frame, integration, path = get_integration_frame()
except MissingIntegrationFrame:
# Did not source from an integration? Hard error.
raise RuntimeError(f"Detected code that {what}. Please report this issue.")
index = found_frame.filename.index(path)
if path == "custom_components/":
extra = " to the custom component author"
else:
extra = ""
_LOGGER.warning(
"Detected integration that %s. "
"Please report issue%s for %s using this method at %s, line %s: %s",
what,
extra,
integration,
found_frame.filename[index:],
found_frame.lineno,
found_frame.line.strip(),
)
def warn_use(func: CALLABLE_T, what: str) -> CALLABLE_T:
"""Mock a function to warn when it was about to be used."""
if asyncio.iscoroutinefunction(func):
@functools.wraps(func)
async def report_use(*args: Any, **kwargs: Any) -> None:
report(what)
else:
@functools.wraps(func)
def report_use(*args: Any, **kwargs: Any) -> None:
report(what)
return cast(CALLABLE_T, report_use)

119
homeassistant/runner.py Normal file
View File

@ -0,0 +1,119 @@
"""Run Home Assistant."""
import asyncio
from concurrent.futures import ThreadPoolExecutor
import dataclasses
import logging
import sys
import threading
from typing import Any, Dict, Optional
from homeassistant import bootstrap
from homeassistant.core import callback
from homeassistant.helpers.frame import warn_use
@dataclasses.dataclass
class RuntimeConfig:
"""Class to hold the information for running Home Assistant."""
config_dir: str
skip_pip: bool = False
safe_mode: bool = False
verbose: bool = False
log_rotate_days: Optional[int] = None
log_file: Optional[str] = None
log_no_color: bool = False
debug: bool = False
open_ui: bool = False
# In Python 3.8+ proactor policy is the default on Windows
if sys.platform == "win32" and sys.version_info[:2] < (3, 8):
PolicyBase = asyncio.WindowsProactorEventLoopPolicy
else:
PolicyBase = asyncio.DefaultEventLoopPolicy # pylint: disable=invalid-name
class HassEventLoopPolicy(PolicyBase):
"""Event loop policy for Home Assistant."""
def __init__(self, debug: bool) -> None:
"""Init the event loop policy."""
super().__init__()
self.debug = debug
@property
def loop_name(self) -> str:
"""Return name of the loop."""
return self._loop_factory.__name__
def new_event_loop(self):
"""Get the event loop."""
loop = super().new_event_loop()
loop.set_exception_handler(_async_loop_exception_handler)
if self.debug:
loop.set_debug(True)
executor = ThreadPoolExecutor(thread_name_prefix="SyncWorker")
loop.set_default_executor(executor)
loop.set_default_executor = warn_use( # type: ignore
loop.set_default_executor, "sets default executor on the event loop"
)
# Python 3.9+
if hasattr(loop, "shutdown_default_executor"):
return loop
# Copied from Python 3.9 source
def _do_shutdown(future):
try:
executor.shutdown(wait=True)
loop.call_soon_threadsafe(future.set_result, None)
except Exception as ex: # pylint: disable=broad-except
loop.call_soon_threadsafe(future.set_exception, ex)
async def shutdown_default_executor():
"""Schedule the shutdown of the default executor."""
future = loop.create_future()
thread = threading.Thread(target=_do_shutdown, args=(future,))
thread.start()
try:
await future
finally:
thread.join()
loop.shutdown_default_executor = shutdown_default_executor
return loop
@callback
def _async_loop_exception_handler(self, _: Any, context: Dict) -> None:
"""Handle all exception inside the core loop."""
kwargs = {}
exception = context.get("exception")
if exception:
kwargs["exc_info"] = (type(exception), exception, exception.__traceback__)
logging.getLogger(__package__).error(
"Error doing job: %s", context["message"], **kwargs # type: ignore
)
async def setup_and_run_hass(runtime_config: RuntimeConfig,) -> int:
"""Set up Home Assistant and run."""
hass = await bootstrap.async_setup_hass(runtime_config)
if hass is None:
return 1
return await hass.async_run()
def run(runtime_config: RuntimeConfig) -> int:
"""Run Home Assistant."""
asyncio.set_event_loop_policy(HassEventLoopPolicy(runtime_config.debug))
return asyncio.run(setup_and_run_hass(runtime_config))

View File

@ -7,6 +7,7 @@ import os
import sys
from typing import List, Optional, Sequence, Text
from homeassistant import runner
from homeassistant.bootstrap import async_mount_local_lib_path
from homeassistant.config import get_default_config_dir
from homeassistant.requirements import pip_kwargs
@ -59,6 +60,8 @@ def run(args: List) -> int:
print("Aborting script, could not install dependency", req)
return 1
asyncio.set_event_loop_policy(runner.HassEventLoopPolicy(False))
return script.run(args[1:]) # type: ignore

View File

@ -4,6 +4,7 @@ import asyncio
import logging
import os
from homeassistant import runner
from homeassistant.auth import auth_manager_from_config
from homeassistant.auth.providers import homeassistant as hass_auth
from homeassistant.config import get_default_config_dir
@ -43,24 +44,24 @@ def run(args):
parser_change_pw.add_argument("new_password", type=str)
parser_change_pw.set_defaults(func=change_password)
args = parser.parse_args(args)
loop = asyncio.get_event_loop()
hass = HomeAssistant(loop=loop)
loop.run_until_complete(run_command(hass, args))
# Triggers save on used storage helpers with delay (core auth)
logging.getLogger("homeassistant.core").setLevel(logging.WARNING)
loop.run_until_complete(hass.async_stop())
asyncio.set_event_loop_policy(runner.HassEventLoopPolicy(False))
asyncio.run(run_command(parser.parse_args(args)))
async def run_command(hass, args):
async def run_command(args):
"""Run the command."""
hass = HomeAssistant()
hass.config.config_dir = os.path.join(os.getcwd(), args.config)
hass.auth = await auth_manager_from_config(hass, [{"type": "homeassistant"}], [])
provider = hass.auth.auth_providers[0]
await provider.async_initialize()
await args.func(hass, provider, args)
# Triggers save on used storage helpers with delay (core auth)
logging.getLogger("homeassistant.core").setLevel(logging.WARNING)
await hass.async_stop()
async def list_users(hass, provider, args):
"""List the users."""

View File

@ -36,18 +36,19 @@ def run(args):
args = parser.parse_args()
bench = BENCHMARKS[args.name]
print("Using event loop:", asyncio.get_event_loop_policy().__module__)
print("Using event loop:", asyncio.get_event_loop_policy().loop_name)
with suppress(KeyboardInterrupt):
while True:
loop = asyncio.new_event_loop()
hass = core.HomeAssistant(loop)
hass.async_stop_track_tasks()
runtime = loop.run_until_complete(bench(hass))
print(f"Benchmark {bench.__name__} done in {runtime}s")
loop.run_until_complete(hass.async_stop())
loop.close()
asyncio.run(run_benchmark(bench))
async def run_benchmark(bench):
"""Run a benchmark."""
hass = core.HomeAssistant()
runtime = await bench(hass)
print(f"Benchmark {bench.__name__} done in {runtime}s")
await hass.async_stop()
def benchmark(func: CALLABLE_T) -> CALLABLE_T:

View File

@ -1,5 +1,6 @@
"""Script to check the configuration file."""
import argparse
import asyncio
from collections import OrderedDict
from collections.abc import Mapping, Sequence
from glob import glob
@ -199,12 +200,7 @@ def check(config_dir, secrets=False):
yaml_loader.yaml.SafeLoader.add_constructor("!secret", yaml_loader.secret_yaml)
try:
hass = core.HomeAssistant()
hass.config.config_dir = config_dir
res["components"] = hass.loop.run_until_complete(
async_check_ha_config_file(hass)
)
res["components"] = asyncio.run(async_check_config(config_dir))
res["secret_cache"] = OrderedDict(yaml_loader.__SECRET_CACHE)
for err in res["components"].errors:
domain = err.domain or ERROR_STR
@ -213,7 +209,6 @@ def check(config_dir, secrets=False):
res["except"].setdefault(domain, []).append(err.config)
except Exception as err: # pylint: disable=broad-except
_LOGGER.exception("BURB")
print(color("red", "Fatal error while loading config:"), str(err))
res["except"].setdefault(ERROR_STR, []).append(str(err))
finally:
@ -230,6 +225,15 @@ def check(config_dir, secrets=False):
return res
async def async_check_config(config_dir):
"""Check the HA config."""
hass = core.HomeAssistant()
hass.config.config_dir = config_dir
components = await async_check_ha_config_file(hass)
await hass.async_stop(force=True)
return components
def line_info(obj, **kwargs):
"""Display line config source."""
if hasattr(obj, "__config_file__"):

View File

@ -1,5 +1,6 @@
"""Script to ensure a configuration file exists."""
import argparse
import asyncio
import os
import homeassistant.config as config_util
@ -31,15 +32,15 @@ def run(args):
print("Creating directory", config_dir)
os.makedirs(config_dir)
hass = HomeAssistant()
hass.config.config_dir = config_dir
config_path = hass.loop.run_until_complete(async_run(hass))
config_path = asyncio.run(async_run(config_dir))
print("Configuration file:", config_path)
return 0
async def async_run(hass):
async def async_run(config_dir):
"""Make sure config exists."""
hass = HomeAssistant()
hass.config.config_dir = config_dir
path = await config_util.async_ensure_config_exists(hass)
await hass.async_stop(force=True)
return path

View File

@ -149,7 +149,7 @@ def get_test_home_assistant():
# pylint: disable=protected-access
async def async_test_home_assistant(loop):
"""Return a Home Assistant object pointing at test config dir."""
hass = ha.HomeAssistant(loop)
hass = ha.HomeAssistant()
store = auth_store.AuthStore(hass)
hass.auth = auth.AuthManager(hass, store, {}, {})
ensure_auth_manager_loaded(hass.auth)

View File

@ -8,8 +8,8 @@ from homeassistant.core import callback as ha_callback
from tests.async_mock import patch
@pytest.fixture(scope="session")
def hk_driver():
@pytest.fixture
def hk_driver(loop):
"""Return a custom AccessoryDriver instance for HomeKit accessory init."""
with patch("pyhap.accessory_driver.Zeroconf"), patch(
"pyhap.accessory_driver.AccessoryEncoder"
@ -18,7 +18,7 @@ def hk_driver():
), patch(
"pyhap.accessory_driver.AccessoryDriver.persist"
):
yield AccessoryDriver(pincode=b"123-45-678", address="127.0.0.1")
yield AccessoryDriver(pincode=b"123-45-678", address="127.0.0.1", loop=loop)
@pytest.fixture

View File

@ -68,6 +68,11 @@ from tests.components.homekit.common import patch_debounce
IP_ADDRESS = "127.0.0.1"
@pytest.fixture(autouse=True)
def always_patch_driver(hk_driver):
"""Load the hk_driver fixture."""
@pytest.fixture(name="device_reg")
def device_reg_fixture(hass):
"""Return an empty, loaded, registry."""

View File

@ -215,7 +215,7 @@ def test_density_to_air_quality():
assert density_to_air_quality(300) == 5
async def test_show_setup_msg(hass):
async def test_show_setup_msg(hass, hk_driver):
"""Test show setup message as persistence notification."""
pincode = b"123-45-678"

View File

@ -1,11 +1,12 @@
"""Set up some common test helper things."""
import asyncio
import functools
import logging
import pytest
import requests_mock as _requests_mock
from homeassistant import core as ha, loader, util
from homeassistant import core as ha, loader, runner, util
from homeassistant.auth.const import GROUP_ID_ADMIN, GROUP_ID_READ_ONLY
from homeassistant.auth.providers import homeassistant, legacy_api_password
from homeassistant.components import mqtt
@ -40,6 +41,10 @@ from tests.test_util.aiohttp import mock_aiohttp_client # noqa: E402, isort:ski
logging.basicConfig(level=logging.DEBUG)
logging.getLogger("sqlalchemy.engine").setLevel(logging.INFO)
asyncio.set_event_loop_policy(runner.HassEventLoopPolicy(False))
# Disable fixtures overriding our beautiful policy
asyncio.set_event_loop_policy = lambda policy: None
def pytest_configure(config):
"""Register marker for tests that log exceptions."""

View File

@ -7,17 +7,15 @@ from unittest.mock import Mock
import pytest
from homeassistant import bootstrap, core
from homeassistant import bootstrap, core, runner
import homeassistant.config as config_util
from homeassistant.exceptions import HomeAssistantError
import homeassistant.util.dt as dt_util
from tests.async_mock import patch
from tests.common import (
MockConfigEntry,
MockModule,
MockPlatform,
flush_store,
get_test_config_dir,
mock_coro,
mock_entity_platform,
@ -351,6 +349,7 @@ async def test_setup_hass(
mock_ensure_config_exists,
mock_process_ha_config_upgrade,
caplog,
loop,
):
"""Test it works."""
verbose = Mock()
@ -365,13 +364,15 @@ async def test_setup_hass(
"homeassistant.components.http.start_http_server_and_save_config"
):
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,
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,
safe_mode=False,
),
)
assert "Waiting on integrations to complete setup" not in caplog.text
@ -399,6 +400,7 @@ async def test_setup_hass_takes_longer_than_log_slow_startup(
mock_ensure_config_exists,
mock_process_ha_config_upgrade,
caplog,
loop,
):
"""Test it works."""
verbose = Mock()
@ -420,13 +422,15 @@ async def test_setup_hass_takes_longer_than_log_slow_startup(
"homeassistant.components.http.start_http_server_and_save_config"
):
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,
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,
safe_mode=False,
),
)
assert "Waiting on integrations to complete setup" in caplog.text
@ -438,19 +442,22 @@ async def test_setup_hass_invalid_yaml(
mock_mount_local_lib_path,
mock_ensure_config_exists,
mock_process_ha_config_upgrade,
loop,
):
"""Test it works."""
with patch(
"homeassistant.config.async_hass_config_yaml", side_effect=HomeAssistantError
), patch("homeassistant.components.http.start_http_server_and_save_config"):
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,
runner.RuntimeConfig(
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
@ -463,49 +470,52 @@ async def test_setup_hass_config_dir_nonexistent(
mock_mount_local_lib_path,
mock_ensure_config_exists,
mock_process_ha_config_upgrade,
loop,
):
"""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,
runner.RuntimeConfig(
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,
loop,
):
"""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, patch(
"homeassistant.components.http.start_http_server_and_save_config"
), patch(
"homeassistant.config_entries.ConfigEntries.async_domains",
return_value=["browser"],
):
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,
runner.RuntimeConfig(
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
@ -522,6 +532,7 @@ async def test_setup_hass_invalid_core_config(
mock_mount_local_lib_path,
mock_ensure_config_exists,
mock_process_ha_config_upgrade,
loop,
):
"""Test it works."""
with patch(
@ -529,13 +540,15 @@ async def test_setup_hass_invalid_core_config(
return_value={"homeassistant": {"non-existing": 1}},
), patch("homeassistant.components.http.start_http_server_and_save_config"):
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,
runner.RuntimeConfig(
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
@ -547,6 +560,7 @@ async def test_setup_safe_mode_if_no_frontend(
mock_mount_local_lib_path,
mock_ensure_config_exists,
mock_process_ha_config_upgrade,
loop,
):
"""Test we setup safe mode if frontend didn't load."""
verbose = Mock()
@ -566,13 +580,15 @@ async def test_setup_safe_mode_if_no_frontend(
},
), patch("homeassistant.components.http.start_http_server_and_save_config"):
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,
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,
safe_mode=False,
),
)
assert "safe_mode" in hass.config.components

View File

@ -1104,14 +1104,13 @@ def test_timer_out_of_sync(mock_monotonic, loop):
assert abs(target - 14.2) < 0.001
@asyncio.coroutine
def test_hass_start_starts_the_timer(loop):
async def test_hass_start_starts_the_timer(loop):
"""Test when hass starts, it starts the timer."""
hass = ha.HomeAssistant(loop=loop)
hass = ha.HomeAssistant()
try:
with patch("homeassistant.core._async_create_timer") as mock_timer:
yield from hass.async_start()
await hass.async_start()
assert hass.state == ha.CoreState.running
assert not hass._track_task
@ -1119,21 +1118,20 @@ def test_hass_start_starts_the_timer(loop):
assert mock_timer.mock_calls[0][1][0] is hass
finally:
yield from hass.async_stop()
await hass.async_stop()
assert hass.state == ha.CoreState.not_running
@asyncio.coroutine
def test_start_taking_too_long(loop, caplog):
async def test_start_taking_too_long(loop, caplog):
"""Test when async_start takes too long."""
hass = ha.HomeAssistant(loop=loop)
hass = ha.HomeAssistant()
caplog.set_level(logging.WARNING)
try:
with patch(
"homeassistant.core.timeout", side_effect=asyncio.TimeoutError
), patch("homeassistant.core._async_create_timer") as mock_timer:
yield from hass.async_start()
await hass.async_start()
assert hass.state == ha.CoreState.running
assert len(mock_timer.mock_calls) == 1
@ -1141,14 +1139,13 @@ def test_start_taking_too_long(loop, caplog):
assert "Something is blocking Home Assistant" in caplog.text
finally:
yield from hass.async_stop()
await hass.async_stop()
assert hass.state == ha.CoreState.not_running
@asyncio.coroutine
def test_track_task_functions(loop):
async def test_track_task_functions(loop):
"""Test function to start/stop track task and initial state."""
hass = ha.HomeAssistant(loop=loop)
hass = ha.HomeAssistant()
try:
assert hass._track_task
@ -1158,7 +1155,7 @@ def test_track_task_functions(loop):
hass.async_track_tasks()
assert hass._track_task
finally:
yield from hass.async_stop()
await hass.async_stop()
async def test_service_executed_with_subservices(hass):