2018-08-19 20:29:08 +00:00
|
|
|
"""Set up some common test helper things."""
|
2022-04-23 05:29:44 +00:00
|
|
|
from __future__ import annotations
|
|
|
|
|
2020-07-06 22:58:53 +00:00
|
|
|
import asyncio
|
2022-04-23 05:29:44 +00:00
|
|
|
from collections.abc import AsyncGenerator
|
2016-10-24 06:48:01 +00:00
|
|
|
import functools
|
|
|
|
import logging
|
2021-10-06 00:46:09 +00:00
|
|
|
import socket
|
2020-08-15 13:26:54 +00:00
|
|
|
import ssl
|
2020-07-09 14:15:14 +00:00
|
|
|
import threading
|
2021-12-08 20:28:26 +00:00
|
|
|
from unittest.mock import AsyncMock, MagicMock, Mock, patch
|
2016-10-24 06:48:01 +00:00
|
|
|
|
2020-08-15 13:26:54 +00:00
|
|
|
from aiohttp.test_utils import make_mocked_request
|
2021-11-02 17:11:39 +00:00
|
|
|
import freezegun
|
2020-11-27 07:55:34 +00:00
|
|
|
import multidict
|
2016-10-24 06:48:01 +00:00
|
|
|
import pytest
|
2021-10-06 00:46:09 +00:00
|
|
|
import pytest_socket
|
2016-10-24 06:48:01 +00:00
|
|
|
import requests_mock as _requests_mock
|
|
|
|
|
2020-07-06 22:58:53 +00:00
|
|
|
from homeassistant import core as ha, loader, runner, util
|
2018-12-02 15:32:53 +00:00
|
|
|
from homeassistant.auth.const import GROUP_ID_ADMIN, GROUP_ID_READ_ONLY
|
2021-01-28 11:06:20 +00:00
|
|
|
from homeassistant.auth.models import Credentials
|
2019-12-09 15:52:24 +00:00
|
|
|
from homeassistant.auth.providers import homeassistant, legacy_api_password
|
2021-05-20 11:05:15 +00:00
|
|
|
from homeassistant.components import mqtt, recorder
|
2020-01-03 20:37:11 +00:00
|
|
|
from homeassistant.components.websocket_api.auth import (
|
|
|
|
TYPE_AUTH,
|
|
|
|
TYPE_AUTH_OK,
|
|
|
|
TYPE_AUTH_REQUIRED,
|
|
|
|
)
|
|
|
|
from homeassistant.components.websocket_api.http import URL
|
2022-04-09 19:05:54 +00:00
|
|
|
from homeassistant.const import HASSIO_USER_NAME
|
2022-04-23 05:29:44 +00:00
|
|
|
from homeassistant.core import CoreState, HomeAssistant
|
2022-04-09 19:05:54 +00:00
|
|
|
from homeassistant.helpers import config_entry_oauth2_flow
|
2022-04-23 05:29:44 +00:00
|
|
|
from homeassistant.helpers.typing import ConfigType
|
2020-01-03 20:37:11 +00:00
|
|
|
from homeassistant.setup import async_setup_component
|
2022-03-20 09:25:15 +00:00
|
|
|
from homeassistant.util import dt as dt_util, location
|
2016-10-24 06:48:01 +00:00
|
|
|
|
2020-05-06 21:14:57 +00:00
|
|
|
from tests.ignore_uncaught_exceptions import IGNORE_UNCAUGHT_EXCEPTIONS
|
2020-04-30 20:29:50 +00:00
|
|
|
|
2019-12-09 18:04:38 +00:00
|
|
|
pytest.register_assert_rewrite("tests.common")
|
|
|
|
|
|
|
|
from tests.common import ( # noqa: E402, isort:skip
|
2019-12-09 15:52:24 +00:00
|
|
|
CLIENT_ID,
|
2019-07-31 19:25:30 +00:00
|
|
|
INSTANCES,
|
2022-02-10 20:09:57 +00:00
|
|
|
MockConfigEntry,
|
2019-12-09 15:52:24 +00:00
|
|
|
MockUser,
|
2022-04-23 05:29:44 +00:00
|
|
|
SetupRecorderInstanceT,
|
2020-06-22 21:59:50 +00:00
|
|
|
async_fire_mqtt_message,
|
2019-12-09 15:52:24 +00:00
|
|
|
async_test_home_assistant,
|
2021-05-20 11:05:15 +00:00
|
|
|
get_test_home_assistant,
|
|
|
|
init_recorder_component,
|
2019-07-31 19:25:30 +00:00
|
|
|
mock_storage as mock_storage,
|
|
|
|
)
|
2019-12-09 18:04:38 +00:00
|
|
|
from tests.test_util.aiohttp import mock_aiohttp_client # noqa: E402, isort:skip
|
2022-04-23 05:29:44 +00:00
|
|
|
from tests.components.recorder.common import ( # noqa: E402, isort:skip
|
|
|
|
async_recorder_block_till_done,
|
|
|
|
)
|
2017-03-07 09:11:41 +00:00
|
|
|
|
2022-04-26 16:08:00 +00:00
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
2018-07-13 13:31:20 +00:00
|
|
|
logging.basicConfig(level=logging.DEBUG)
|
2019-07-31 19:25:30 +00:00
|
|
|
logging.getLogger("sqlalchemy.engine").setLevel(logging.INFO)
|
2016-10-24 06:48:01 +00:00
|
|
|
|
2020-07-06 22:58:53 +00:00
|
|
|
asyncio.set_event_loop_policy(runner.HassEventLoopPolicy(False))
|
|
|
|
# Disable fixtures overriding our beautiful policy
|
|
|
|
asyncio.set_event_loop_policy = lambda policy: None
|
|
|
|
|
2016-10-24 06:48:01 +00:00
|
|
|
|
2020-05-06 21:14:57 +00:00
|
|
|
def pytest_configure(config):
|
|
|
|
"""Register marker for tests that log exceptions."""
|
|
|
|
config.addinivalue_line(
|
|
|
|
"markers", "no_fail_on_log_exception: mark test to not fail on logged exception"
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2021-10-06 00:46:09 +00:00
|
|
|
def pytest_runtest_setup():
|
2021-11-02 17:11:39 +00:00
|
|
|
"""Prepare pytest_socket and freezegun.
|
|
|
|
|
|
|
|
pytest_socket:
|
|
|
|
Throw if tests attempt to open sockets.
|
2021-10-06 00:46:09 +00:00
|
|
|
|
|
|
|
allow_unix_socket is set to True because it's needed by asyncio.
|
|
|
|
Important: socket_allow_hosts must be called before disable_socket, otherwise all
|
|
|
|
destinations will be allowed.
|
2021-11-02 17:11:39 +00:00
|
|
|
|
|
|
|
freezegun:
|
|
|
|
Modified to include https://github.com/spulec/freezegun/pull/424
|
2021-10-06 00:46:09 +00:00
|
|
|
"""
|
|
|
|
pytest_socket.socket_allow_hosts(["127.0.0.1"])
|
|
|
|
disable_socket(allow_unix_socket=True)
|
|
|
|
|
2021-11-02 17:11:39 +00:00
|
|
|
freezegun.api.datetime_to_fakedatetime = ha_datetime_to_fakedatetime
|
|
|
|
freezegun.api.FakeDatetime = HAFakeDatetime
|
|
|
|
|
2021-10-06 00:46:09 +00:00
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
def socket_disabled(pytestconfig):
|
|
|
|
"""Disable socket.socket for duration of this test function.
|
|
|
|
|
|
|
|
This incorporates changes from https://github.com/miketheman/pytest-socket/pull/76
|
|
|
|
and hardcodes allow_unix_socket to True because it's not passed on the command line.
|
|
|
|
"""
|
|
|
|
socket_was_enabled = socket.socket == pytest_socket._true_socket
|
|
|
|
disable_socket(allow_unix_socket=True)
|
|
|
|
yield
|
|
|
|
if socket_was_enabled:
|
|
|
|
pytest_socket.enable_socket()
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
def socket_enabled(pytestconfig):
|
|
|
|
"""Enable socket.socket for duration of this test function.
|
|
|
|
|
|
|
|
This incorporates changes from https://github.com/miketheman/pytest-socket/pull/76
|
|
|
|
and hardcodes allow_unix_socket to True because it's not passed on the command line.
|
|
|
|
"""
|
|
|
|
socket_was_disabled = socket.socket != pytest_socket._true_socket
|
|
|
|
pytest_socket.enable_socket()
|
|
|
|
yield
|
|
|
|
if socket_was_disabled:
|
|
|
|
disable_socket(allow_unix_socket=True)
|
|
|
|
|
|
|
|
|
|
|
|
def disable_socket(allow_unix_socket=False):
|
|
|
|
"""Disable socket.socket to disable the Internet. useful in testing.
|
|
|
|
|
|
|
|
This incorporates changes from https://github.com/miketheman/pytest-socket/pull/75
|
|
|
|
"""
|
|
|
|
|
|
|
|
class GuardedSocket(socket.socket):
|
|
|
|
"""socket guard to disable socket creation (from pytest-socket)."""
|
|
|
|
|
|
|
|
def __new__(cls, *args, **kwargs):
|
|
|
|
try:
|
|
|
|
if len(args) > 0:
|
|
|
|
is_unix_socket = args[0] == socket.AF_UNIX
|
|
|
|
else:
|
|
|
|
is_unix_socket = kwargs.get("family") == socket.AF_UNIX
|
|
|
|
except AttributeError:
|
|
|
|
# AF_UNIX not supported on Windows https://bugs.python.org/issue33408
|
|
|
|
is_unix_socket = False
|
|
|
|
if is_unix_socket and allow_unix_socket:
|
|
|
|
return super().__new__(cls, *args, **kwargs)
|
|
|
|
raise pytest_socket.SocketBlockedError()
|
|
|
|
|
|
|
|
socket.socket = GuardedSocket
|
|
|
|
|
|
|
|
|
2021-11-02 17:11:39 +00:00
|
|
|
def ha_datetime_to_fakedatetime(datetime):
|
|
|
|
"""Convert datetime to FakeDatetime.
|
|
|
|
|
|
|
|
Modified to include https://github.com/spulec/freezegun/pull/424.
|
|
|
|
"""
|
|
|
|
return freezegun.api.FakeDatetime(
|
|
|
|
datetime.year,
|
|
|
|
datetime.month,
|
|
|
|
datetime.day,
|
|
|
|
datetime.hour,
|
|
|
|
datetime.minute,
|
|
|
|
datetime.second,
|
|
|
|
datetime.microsecond,
|
|
|
|
datetime.tzinfo,
|
|
|
|
fold=datetime.fold,
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
class HAFakeDatetime(freezegun.api.FakeDatetime):
|
|
|
|
"""Modified to include https://github.com/spulec/freezegun/pull/424."""
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def now(cls, tz=None):
|
|
|
|
"""Return frozen now."""
|
|
|
|
now = cls._time_to_freeze() or freezegun.api.real_datetime.now()
|
|
|
|
if tz:
|
|
|
|
result = tz.fromutc(now.replace(tzinfo=tz))
|
|
|
|
else:
|
|
|
|
result = now
|
|
|
|
|
|
|
|
# Add the _tz_offset only if it's non-zero to preserve fold
|
|
|
|
if cls._tz_offset():
|
|
|
|
result += cls._tz_offset()
|
|
|
|
|
|
|
|
return ha_datetime_to_fakedatetime(result)
|
|
|
|
|
|
|
|
|
2018-05-12 21:44:53 +00:00
|
|
|
def check_real(func):
|
2016-10-24 06:48:01 +00:00
|
|
|
"""Force a function to require a keyword _test_real to be passed in."""
|
2019-07-31 19:25:30 +00:00
|
|
|
|
2016-10-24 06:48:01 +00:00
|
|
|
@functools.wraps(func)
|
2020-02-16 23:33:09 +00:00
|
|
|
async def guard_func(*args, **kwargs):
|
2019-07-31 19:25:30 +00:00
|
|
|
real = kwargs.pop("_test_real", None)
|
2016-10-24 06:48:01 +00:00
|
|
|
|
|
|
|
if not real:
|
2019-07-31 19:25:30 +00:00
|
|
|
raise Exception(
|
|
|
|
'Forgot to mock or pass "_test_real=True" to %s', func.__name__
|
|
|
|
)
|
2016-10-24 06:48:01 +00:00
|
|
|
|
2020-02-16 23:33:09 +00:00
|
|
|
return await func(*args, **kwargs)
|
2016-10-24 06:48:01 +00:00
|
|
|
|
|
|
|
return guard_func
|
|
|
|
|
2016-11-19 05:47:59 +00:00
|
|
|
|
2016-10-24 06:48:01 +00:00
|
|
|
# Guard a few functions that would make network connections
|
2019-07-31 19:25:30 +00:00
|
|
|
location.async_detect_location_info = check_real(location.async_detect_location_info)
|
|
|
|
util.get_local_ip = lambda: "127.0.0.1"
|
2016-10-24 06:48:01 +00:00
|
|
|
|
|
|
|
|
2017-02-26 22:05:18 +00:00
|
|
|
@pytest.fixture(autouse=True)
|
|
|
|
def verify_cleanup():
|
|
|
|
"""Verify that the test has cleaned up resources correctly."""
|
2020-07-09 14:15:14 +00:00
|
|
|
threads_before = frozenset(threading.enumerate())
|
|
|
|
|
2017-02-26 22:05:18 +00:00
|
|
|
yield
|
|
|
|
|
2017-05-17 22:19:40 +00:00
|
|
|
if len(INSTANCES) >= 2:
|
|
|
|
count = len(INSTANCES)
|
|
|
|
for inst in INSTANCES:
|
|
|
|
inst.stop()
|
2020-01-03 13:47:06 +00:00
|
|
|
pytest.exit(f"Detected non stopped instances ({count}), aborting test run")
|
2017-02-26 22:05:18 +00:00
|
|
|
|
2020-07-09 14:15:14 +00:00
|
|
|
threads = frozenset(threading.enumerate()) - threads_before
|
|
|
|
assert not threads
|
|
|
|
|
2017-02-26 22:05:18 +00:00
|
|
|
|
2021-01-29 18:57:14 +00:00
|
|
|
@pytest.fixture(autouse=True)
|
|
|
|
def bcrypt_cost():
|
|
|
|
"""Run with reduced rounds during tests, to speed up uses."""
|
|
|
|
import bcrypt
|
|
|
|
|
|
|
|
gensalt_orig = bcrypt.gensalt
|
|
|
|
|
|
|
|
def gensalt_mock(rounds=12, prefix=b"2b"):
|
|
|
|
return gensalt_orig(4, prefix)
|
|
|
|
|
|
|
|
bcrypt.gensalt = gensalt_mock
|
|
|
|
yield
|
|
|
|
bcrypt.gensalt = gensalt_orig
|
|
|
|
|
|
|
|
|
2016-10-24 06:48:01 +00:00
|
|
|
@pytest.fixture
|
2018-06-29 02:14:26 +00:00
|
|
|
def hass_storage():
|
|
|
|
"""Fixture to mock storage."""
|
|
|
|
with mock_storage() as stored_data:
|
|
|
|
yield stored_data
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
2021-02-11 16:36:19 +00:00
|
|
|
def load_registries():
|
|
|
|
"""Fixture to control the loading of registries when setting up the hass fixture.
|
|
|
|
|
|
|
|
To avoid loading the registries, tests can be marked with:
|
|
|
|
@pytest.mark.parametrize("load_registries", [False])
|
|
|
|
"""
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
def hass(loop, load_registries, hass_storage, request):
|
2020-01-05 12:09:17 +00:00
|
|
|
"""Fixture to provide a test instance of Home Assistant."""
|
2020-03-28 04:36:06 +00:00
|
|
|
|
2022-03-20 09:25:15 +00:00
|
|
|
orig_tz = dt_util.DEFAULT_TIME_ZONE
|
|
|
|
|
2020-03-28 04:36:06 +00:00
|
|
|
def exc_handle(loop, context):
|
|
|
|
"""Handle exceptions by rethrowing them, which will fail the test."""
|
2020-10-16 08:01:58 +00:00
|
|
|
# Most of these contexts will contain an exception, but not all.
|
|
|
|
# The docs note the key as "optional"
|
|
|
|
# See https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.loop.call_exception_handler
|
|
|
|
if "exception" in context:
|
|
|
|
exceptions.append(context["exception"])
|
|
|
|
else:
|
|
|
|
exceptions.append(
|
|
|
|
Exception(
|
|
|
|
"Received exception handler without exception, but with message: %s"
|
|
|
|
% context["message"]
|
|
|
|
)
|
|
|
|
)
|
2020-03-28 04:36:06 +00:00
|
|
|
orig_exception_handler(loop, context)
|
|
|
|
|
|
|
|
exceptions = []
|
2021-02-11 16:36:19 +00:00
|
|
|
hass = loop.run_until_complete(async_test_home_assistant(loop, load_registries))
|
2020-03-28 04:36:06 +00:00
|
|
|
orig_exception_handler = loop.get_exception_handler()
|
|
|
|
loop.set_exception_handler(exc_handle)
|
2016-10-24 06:48:01 +00:00
|
|
|
|
|
|
|
yield hass
|
|
|
|
|
2018-09-19 13:40:02 +00:00
|
|
|
loop.run_until_complete(hass.async_stop(force=True))
|
2022-03-20 09:25:15 +00:00
|
|
|
|
|
|
|
# Restore timezone, it is set when creating the hass object
|
|
|
|
dt_util.DEFAULT_TIME_ZONE = orig_tz
|
|
|
|
|
2020-03-28 04:36:06 +00:00
|
|
|
for ex in exceptions:
|
2020-05-06 21:14:57 +00:00
|
|
|
if (
|
|
|
|
request.module.__name__,
|
|
|
|
request.function.__name__,
|
|
|
|
) in IGNORE_UNCAUGHT_EXCEPTIONS:
|
|
|
|
continue
|
2020-03-28 04:36:06 +00:00
|
|
|
raise ex
|
2016-10-24 06:48:01 +00:00
|
|
|
|
|
|
|
|
2020-07-09 14:15:14 +00:00
|
|
|
@pytest.fixture
|
|
|
|
async def stop_hass():
|
|
|
|
"""Make sure all hass are stopped."""
|
|
|
|
orig_hass = ha.HomeAssistant
|
|
|
|
|
|
|
|
created = []
|
|
|
|
|
|
|
|
def mock_hass():
|
|
|
|
hass_inst = orig_hass()
|
|
|
|
created.append(hass_inst)
|
|
|
|
return hass_inst
|
|
|
|
|
|
|
|
with patch("homeassistant.core.HomeAssistant", mock_hass):
|
|
|
|
yield
|
|
|
|
|
|
|
|
for hass_inst in created:
|
|
|
|
if hass_inst.state == ha.CoreState.stopped:
|
|
|
|
continue
|
|
|
|
|
|
|
|
with patch.object(hass_inst.loop, "stop"):
|
|
|
|
await hass_inst.async_block_till_done()
|
|
|
|
await hass_inst.async_stop(force=True)
|
|
|
|
|
|
|
|
|
2016-10-24 06:48:01 +00:00
|
|
|
@pytest.fixture
|
|
|
|
def requests_mock():
|
|
|
|
"""Fixture to provide a requests mocker."""
|
|
|
|
with _requests_mock.mock() as m:
|
|
|
|
yield m
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
def aioclient_mock():
|
|
|
|
"""Fixture to mock aioclient calls."""
|
|
|
|
with mock_aiohttp_client() as mock_session:
|
|
|
|
yield mock_session
|
2017-02-07 17:13:24 +00:00
|
|
|
|
|
|
|
|
2018-03-06 19:53:02 +00:00
|
|
|
@pytest.fixture
|
|
|
|
def mock_device_tracker_conf():
|
|
|
|
"""Prevent device tracker from reading/writing data."""
|
|
|
|
devices = []
|
|
|
|
|
|
|
|
async def mock_update_config(path, id, entity):
|
|
|
|
devices.append(entity)
|
|
|
|
|
|
|
|
with patch(
|
2019-07-31 19:25:30 +00:00
|
|
|
"homeassistant.components.device_tracker.legacy"
|
|
|
|
".DeviceTracker.async_update_config",
|
|
|
|
side_effect=mock_update_config,
|
2018-03-06 19:53:02 +00:00
|
|
|
), patch(
|
2019-07-31 19:25:30 +00:00
|
|
|
"homeassistant.components.device_tracker.legacy.async_load_config",
|
2020-04-25 21:32:55 +00:00
|
|
|
side_effect=lambda *args: devices,
|
2018-04-10 01:21:26 +00:00
|
|
|
):
|
2018-03-06 19:53:02 +00:00
|
|
|
yield devices
|
2018-12-02 15:32:53 +00:00
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
2021-01-28 11:06:20 +00:00
|
|
|
async def hass_admin_credential(hass, local_auth):
|
|
|
|
"""Provide credentials for admin user."""
|
2021-01-29 16:58:25 +00:00
|
|
|
return Credentials(
|
|
|
|
id="mock-credential-id",
|
|
|
|
auth_provider_type="homeassistant",
|
|
|
|
auth_provider_id=None,
|
|
|
|
data={"username": "admin"},
|
|
|
|
is_new=False,
|
|
|
|
)
|
2021-01-28 11:06:20 +00:00
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
async def hass_access_token(hass, hass_admin_user, hass_admin_credential):
|
2018-12-02 15:32:53 +00:00
|
|
|
"""Return an access token to access Home Assistant."""
|
2021-01-28 11:06:20 +00:00
|
|
|
await hass.auth.async_link_user(hass_admin_user, hass_admin_credential)
|
|
|
|
|
|
|
|
refresh_token = await hass.auth.async_create_refresh_token(
|
|
|
|
hass_admin_user, CLIENT_ID, credential=hass_admin_credential
|
2019-07-31 19:25:30 +00:00
|
|
|
)
|
2018-12-14 09:19:27 +00:00
|
|
|
return hass.auth.async_create_access_token(refresh_token)
|
2018-12-02 15:32:53 +00:00
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
def hass_owner_user(hass, local_auth):
|
|
|
|
"""Return a Home Assistant admin user."""
|
|
|
|
return MockUser(is_owner=True).add_to_hass(hass)
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
def hass_admin_user(hass, local_auth):
|
|
|
|
"""Return a Home Assistant admin user."""
|
2019-07-31 19:25:30 +00:00
|
|
|
admin_group = hass.loop.run_until_complete(
|
|
|
|
hass.auth.async_get_group(GROUP_ID_ADMIN)
|
|
|
|
)
|
2018-12-02 15:32:53 +00:00
|
|
|
return MockUser(groups=[admin_group]).add_to_hass(hass)
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
def hass_read_only_user(hass, local_auth):
|
|
|
|
"""Return a Home Assistant read only user."""
|
2019-07-31 19:25:30 +00:00
|
|
|
read_only_group = hass.loop.run_until_complete(
|
|
|
|
hass.auth.async_get_group(GROUP_ID_READ_ONLY)
|
|
|
|
)
|
2018-12-02 15:32:53 +00:00
|
|
|
return MockUser(groups=[read_only_group]).add_to_hass(hass)
|
|
|
|
|
|
|
|
|
2018-12-14 09:19:27 +00:00
|
|
|
@pytest.fixture
|
2021-01-28 11:06:20 +00:00
|
|
|
def hass_read_only_access_token(hass, hass_read_only_user, local_auth):
|
2018-12-14 09:19:27 +00:00
|
|
|
"""Return a Home Assistant read only user."""
|
2021-01-28 11:06:20 +00:00
|
|
|
credential = Credentials(
|
|
|
|
id="mock-readonly-credential-id",
|
|
|
|
auth_provider_type="homeassistant",
|
|
|
|
auth_provider_id=None,
|
|
|
|
data={"username": "readonly"},
|
|
|
|
is_new=False,
|
|
|
|
)
|
|
|
|
hass_read_only_user.credentials.append(credential)
|
|
|
|
|
2018-12-14 09:19:27 +00:00
|
|
|
refresh_token = hass.loop.run_until_complete(
|
2021-01-28 11:06:20 +00:00
|
|
|
hass.auth.async_create_refresh_token(
|
|
|
|
hass_read_only_user, CLIENT_ID, credential=credential
|
|
|
|
)
|
2019-07-31 19:25:30 +00:00
|
|
|
)
|
2018-12-14 09:19:27 +00:00
|
|
|
return hass.auth.async_create_access_token(refresh_token)
|
|
|
|
|
|
|
|
|
2021-12-09 00:49:35 +00:00
|
|
|
@pytest.fixture
|
|
|
|
def hass_supervisor_user(hass, local_auth):
|
|
|
|
"""Return the Home Assistant Supervisor user."""
|
|
|
|
admin_group = hass.loop.run_until_complete(
|
|
|
|
hass.auth.async_get_group(GROUP_ID_ADMIN)
|
|
|
|
)
|
|
|
|
return MockUser(
|
|
|
|
name=HASSIO_USER_NAME, groups=[admin_group], system_generated=True
|
|
|
|
).add_to_hass(hass)
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
def hass_supervisor_access_token(hass, hass_supervisor_user, local_auth):
|
|
|
|
"""Return a Home Assistant Supervisor access token."""
|
|
|
|
refresh_token = hass.loop.run_until_complete(
|
|
|
|
hass.auth.async_create_refresh_token(hass_supervisor_user)
|
|
|
|
)
|
|
|
|
return hass.auth.async_create_access_token(refresh_token)
|
|
|
|
|
|
|
|
|
2018-12-02 15:32:53 +00:00
|
|
|
@pytest.fixture
|
|
|
|
def legacy_auth(hass):
|
|
|
|
"""Load legacy API password provider."""
|
|
|
|
prv = legacy_api_password.LegacyApiPasswordAuthProvider(
|
2019-07-31 19:25:30 +00:00
|
|
|
hass,
|
|
|
|
hass.auth._store,
|
|
|
|
{"type": "legacy_api_password", "api_password": "test-password"},
|
2018-12-02 15:32:53 +00:00
|
|
|
)
|
|
|
|
hass.auth._providers[(prv.type, prv.id)] = prv
|
2019-03-11 02:55:36 +00:00
|
|
|
return prv
|
2018-12-02 15:32:53 +00:00
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
def local_auth(hass):
|
|
|
|
"""Load local auth provider."""
|
|
|
|
prv = homeassistant.HassAuthProvider(
|
2019-07-31 19:25:30 +00:00
|
|
|
hass, hass.auth._store, {"type": "homeassistant"}
|
2018-12-02 15:32:53 +00:00
|
|
|
)
|
2021-01-28 11:06:20 +00:00
|
|
|
hass.loop.run_until_complete(prv.async_initialize())
|
2018-12-02 15:32:53 +00:00
|
|
|
hass.auth._providers[(prv.type, prv.id)] = prv
|
2019-03-11 02:55:36 +00:00
|
|
|
return prv
|
2018-12-02 15:32:53 +00:00
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
2021-10-06 00:46:09 +00:00
|
|
|
def hass_client(hass, aiohttp_client, hass_access_token, socket_enabled):
|
2018-12-02 15:32:53 +00:00
|
|
|
"""Return an authenticated HTTP client."""
|
2019-07-31 19:25:30 +00:00
|
|
|
|
2018-12-02 15:32:53 +00:00
|
|
|
async def auth_client():
|
|
|
|
"""Return an authenticated client."""
|
2019-07-31 19:25:30 +00:00
|
|
|
return await aiohttp_client(
|
2020-04-02 17:25:28 +00:00
|
|
|
hass.http.app, headers={"Authorization": f"Bearer {hass_access_token}"}
|
2019-07-31 19:25:30 +00:00
|
|
|
)
|
2018-12-02 15:32:53 +00:00
|
|
|
|
|
|
|
return auth_client
|
2020-01-03 20:37:11 +00:00
|
|
|
|
|
|
|
|
2021-09-02 11:09:16 +00:00
|
|
|
@pytest.fixture
|
2021-10-06 00:46:09 +00:00
|
|
|
def hass_client_no_auth(hass, aiohttp_client, socket_enabled):
|
2021-09-02 11:09:16 +00:00
|
|
|
"""Return an unauthenticated HTTP client."""
|
|
|
|
|
|
|
|
async def client():
|
|
|
|
"""Return an authenticated client."""
|
|
|
|
return await aiohttp_client(hass.http.app)
|
|
|
|
|
|
|
|
return client
|
|
|
|
|
|
|
|
|
2020-08-15 13:26:54 +00:00
|
|
|
@pytest.fixture
|
2020-11-27 07:55:34 +00:00
|
|
|
def current_request():
|
2020-08-15 13:26:54 +00:00
|
|
|
"""Mock current request."""
|
2020-11-27 07:55:34 +00:00
|
|
|
with patch("homeassistant.components.http.current_request") as mock_request_context:
|
2020-08-15 13:26:54 +00:00
|
|
|
mocked_request = make_mocked_request(
|
|
|
|
"GET",
|
|
|
|
"/some/request",
|
|
|
|
headers={"Host": "example.com"},
|
|
|
|
sslcontext=ssl.SSLContext(ssl.PROTOCOL_TLS),
|
|
|
|
)
|
2020-11-27 07:55:34 +00:00
|
|
|
mock_request_context.get.return_value = mocked_request
|
2020-08-15 13:26:54 +00:00
|
|
|
yield mock_request_context
|
|
|
|
|
|
|
|
|
2020-11-27 07:55:34 +00:00
|
|
|
@pytest.fixture
|
|
|
|
def current_request_with_host(current_request):
|
|
|
|
"""Mock current request with a host header."""
|
|
|
|
new_headers = multidict.CIMultiDict(current_request.get.return_value.headers)
|
|
|
|
new_headers[config_entry_oauth2_flow.HEADER_FRONTEND_BASE] = "https://example.com"
|
|
|
|
current_request.get.return_value = current_request.get.return_value.clone(
|
|
|
|
headers=new_headers
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2020-01-03 20:37:11 +00:00
|
|
|
@pytest.fixture
|
2021-10-06 00:46:09 +00:00
|
|
|
def hass_ws_client(aiohttp_client, hass_access_token, hass, socket_enabled):
|
2020-01-03 20:37:11 +00:00
|
|
|
"""Websocket client fixture connected to websocket server."""
|
|
|
|
|
2020-04-14 01:50:36 +00:00
|
|
|
async def create_client(hass=hass, access_token=hass_access_token):
|
2020-01-03 20:37:11 +00:00
|
|
|
"""Create a websocket client."""
|
|
|
|
assert await async_setup_component(hass, "websocket_api", {})
|
|
|
|
client = await aiohttp_client(hass.http.app)
|
2022-01-21 18:06:39 +00:00
|
|
|
websocket = await client.ws_connect(URL)
|
|
|
|
auth_resp = await websocket.receive_json()
|
|
|
|
assert auth_resp["type"] == TYPE_AUTH_REQUIRED
|
2020-01-03 20:37:11 +00:00
|
|
|
|
2022-01-21 18:06:39 +00:00
|
|
|
if access_token is None:
|
|
|
|
await websocket.send_json({"type": TYPE_AUTH, "access_token": "incorrect"})
|
|
|
|
else:
|
|
|
|
await websocket.send_json({"type": TYPE_AUTH, "access_token": access_token})
|
2020-01-03 20:37:11 +00:00
|
|
|
|
2022-01-21 18:06:39 +00:00
|
|
|
auth_ok = await websocket.receive_json()
|
|
|
|
assert auth_ok["type"] == TYPE_AUTH_OK
|
2020-01-03 20:37:11 +00:00
|
|
|
|
|
|
|
# wrap in client
|
|
|
|
websocket.client = client
|
|
|
|
return websocket
|
|
|
|
|
|
|
|
return create_client
|
2020-05-06 21:14:57 +00:00
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
|
|
def fail_on_log_exception(request, monkeypatch):
|
|
|
|
"""Fixture to fail if a callback wrapped by catch_log_exception or coroutine wrapped by async_create_catching_coro throws."""
|
|
|
|
if "no_fail_on_log_exception" in request.keywords:
|
|
|
|
return
|
|
|
|
|
|
|
|
def log_exception(format_err, *args):
|
|
|
|
raise
|
|
|
|
|
|
|
|
monkeypatch.setattr("homeassistant.util.logging.log_exception", log_exception)
|
2020-06-22 21:59:50 +00:00
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
def mqtt_config():
|
|
|
|
"""Fixture to allow overriding MQTT config."""
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
def mqtt_client_mock(hass):
|
|
|
|
"""Fixture to mock MQTT client."""
|
|
|
|
|
2020-08-21 15:00:13 +00:00
|
|
|
mid = 0
|
|
|
|
|
|
|
|
def get_mid():
|
|
|
|
nonlocal mid
|
|
|
|
mid += 1
|
|
|
|
return mid
|
|
|
|
|
|
|
|
class FakeInfo:
|
|
|
|
def __init__(self, mid):
|
|
|
|
self.mid = mid
|
|
|
|
self.rc = 0
|
2020-06-22 21:59:50 +00:00
|
|
|
|
|
|
|
with patch("paho.mqtt.client.Client") as mock_client:
|
2020-08-21 15:00:13 +00:00
|
|
|
|
|
|
|
@ha.callback
|
|
|
|
def _async_fire_mqtt_message(topic, payload, qos, retain):
|
|
|
|
async_fire_mqtt_message(hass, topic, payload, qos, retain)
|
|
|
|
mid = get_mid()
|
|
|
|
mock_client.on_publish(0, 0, mid)
|
|
|
|
return FakeInfo(mid)
|
|
|
|
|
|
|
|
def _subscribe(topic, qos=0):
|
2020-10-06 12:51:58 +00:00
|
|
|
mid = get_mid()
|
2020-08-21 15:00:13 +00:00
|
|
|
mock_client.on_subscribe(0, 0, mid)
|
|
|
|
return (0, mid)
|
|
|
|
|
|
|
|
def _unsubscribe(topic):
|
2020-10-06 12:51:58 +00:00
|
|
|
mid = get_mid()
|
2020-08-21 15:00:13 +00:00
|
|
|
mock_client.on_unsubscribe(0, 0, mid)
|
|
|
|
return (0, mid)
|
|
|
|
|
2020-06-22 21:59:50 +00:00
|
|
|
mock_client = mock_client.return_value
|
|
|
|
mock_client.connect.return_value = 0
|
2020-08-21 15:00:13 +00:00
|
|
|
mock_client.subscribe.side_effect = _subscribe
|
|
|
|
mock_client.unsubscribe.side_effect = _unsubscribe
|
2020-06-22 21:59:50 +00:00
|
|
|
mock_client.publish.side_effect = _async_fire_mqtt_message
|
|
|
|
yield mock_client
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
async def mqtt_mock(hass, mqtt_client_mock, mqtt_config):
|
|
|
|
"""Fixture to mock MQTT component."""
|
|
|
|
if mqtt_config is None:
|
2020-12-04 03:39:49 +00:00
|
|
|
mqtt_config = {mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_BIRTH_MESSAGE: {}}
|
2020-06-22 21:59:50 +00:00
|
|
|
|
|
|
|
await hass.async_block_till_done()
|
|
|
|
|
2022-02-10 20:09:57 +00:00
|
|
|
entry = MockConfigEntry(
|
|
|
|
data=mqtt_config,
|
|
|
|
domain=mqtt.DOMAIN,
|
|
|
|
title="Tasmota",
|
|
|
|
)
|
|
|
|
|
|
|
|
entry.add_to_hass(hass)
|
|
|
|
|
|
|
|
assert await hass.config_entries.async_setup(entry.entry_id)
|
|
|
|
await hass.async_block_till_done()
|
2020-10-08 06:52:23 +00:00
|
|
|
|
2020-06-23 00:49:01 +00:00
|
|
|
mqtt_component_mock = MagicMock(
|
|
|
|
return_value=hass.data["mqtt"],
|
2022-02-10 20:09:57 +00:00
|
|
|
spec_set=hass.data["mqtt"],
|
2020-06-23 00:49:01 +00:00
|
|
|
wraps=hass.data["mqtt"],
|
|
|
|
)
|
2022-02-18 08:28:49 +00:00
|
|
|
mqtt_component_mock.conf = hass.data["mqtt"].conf # For diagnostics
|
2020-06-22 21:59:50 +00:00
|
|
|
mqtt_component_mock._mqttc = mqtt_client_mock
|
2022-03-30 03:26:11 +00:00
|
|
|
# connected set to True to get a more realistics behavior when subscribing
|
|
|
|
hass.data["mqtt"].connected = True
|
2020-06-22 21:59:50 +00:00
|
|
|
|
|
|
|
hass.data["mqtt"] = mqtt_component_mock
|
|
|
|
component = hass.data["mqtt"]
|
|
|
|
component.reset_mock()
|
|
|
|
return component
|
2020-06-29 16:39:24 +00:00
|
|
|
|
|
|
|
|
2021-11-15 17:18:57 +00:00
|
|
|
@pytest.fixture(autouse=True)
|
2021-09-02 18:44:50 +00:00
|
|
|
def mock_get_source_ip():
|
|
|
|
"""Mock network util's async_get_source_ip."""
|
|
|
|
with patch(
|
|
|
|
"homeassistant.components.network.util.async_get_source_ip",
|
|
|
|
return_value="10.10.10.10",
|
|
|
|
):
|
|
|
|
yield
|
|
|
|
|
|
|
|
|
2020-09-13 23:06:19 +00:00
|
|
|
@pytest.fixture
|
|
|
|
def mock_zeroconf():
|
|
|
|
"""Mock zeroconf."""
|
2021-09-01 20:38:00 +00:00
|
|
|
with patch("homeassistant.components.zeroconf.HaZeroconf", autospec=True), patch(
|
|
|
|
"homeassistant.components.zeroconf.HaAsyncServiceBrowser", autospec=True
|
|
|
|
):
|
|
|
|
yield
|
2020-09-13 23:06:19 +00:00
|
|
|
|
|
|
|
|
2021-11-19 04:23:20 +00:00
|
|
|
@pytest.fixture
|
|
|
|
def mock_async_zeroconf(mock_zeroconf):
|
|
|
|
"""Mock AsyncZeroconf."""
|
|
|
|
with patch("homeassistant.components.zeroconf.HaAsyncZeroconf") as mock_aiozc:
|
|
|
|
zc = mock_aiozc.return_value
|
|
|
|
zc.async_unregister_service = AsyncMock()
|
|
|
|
zc.async_register_service = AsyncMock()
|
|
|
|
zc.async_update_service = AsyncMock()
|
|
|
|
zc.zeroconf.async_wait_for_start = AsyncMock()
|
|
|
|
zc.zeroconf.done = False
|
|
|
|
zc.async_close = AsyncMock()
|
|
|
|
zc.ha_async_close = AsyncMock()
|
|
|
|
yield zc
|
|
|
|
|
|
|
|
|
2020-11-27 11:53:16 +00:00
|
|
|
@pytest.fixture
|
|
|
|
def enable_custom_integrations(hass):
|
|
|
|
"""Enable custom integrations defined in the test dir."""
|
|
|
|
hass.data.pop(loader.DATA_CUSTOM_COMPONENTS)
|
2021-05-20 11:05:15 +00:00
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
def enable_statistics():
|
|
|
|
"""Fixture to control enabling of recorder's statistics compilation.
|
|
|
|
|
|
|
|
To enable statistics, tests can be marked with:
|
|
|
|
@pytest.mark.parametrize("enable_statistics", [True])
|
|
|
|
"""
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
2022-03-15 16:23:52 +00:00
|
|
|
def enable_nightly_purge():
|
|
|
|
"""Fixture to control enabling of recorder's nightly purge job.
|
|
|
|
|
2022-04-23 05:29:44 +00:00
|
|
|
To enable nightly purging, tests can be marked with:
|
2022-03-15 16:23:52 +00:00
|
|
|
@pytest.mark.parametrize("enable_nightly_purge", [True])
|
|
|
|
"""
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
2022-04-23 05:29:44 +00:00
|
|
|
@pytest.fixture
|
|
|
|
def recorder_config():
|
|
|
|
"""Fixture to override recorder config.
|
|
|
|
|
|
|
|
To override the config, tests can be marked with:
|
|
|
|
@pytest.mark.parametrize("recorder_config", [{...}])
|
|
|
|
"""
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
2022-03-15 16:23:52 +00:00
|
|
|
@pytest.fixture
|
|
|
|
def hass_recorder(enable_nightly_purge, enable_statistics, hass_storage):
|
2021-05-20 11:05:15 +00:00
|
|
|
"""Home Assistant fixture with in-memory recorder."""
|
2022-03-23 08:30:01 +00:00
|
|
|
original_tz = dt_util.DEFAULT_TIME_ZONE
|
|
|
|
|
2021-05-20 11:05:15 +00:00
|
|
|
hass = get_test_home_assistant()
|
2022-03-15 16:23:52 +00:00
|
|
|
nightly = recorder.Recorder.async_nightly_tasks if enable_nightly_purge else None
|
2022-04-23 05:29:44 +00:00
|
|
|
stats = recorder.Recorder.async_periodic_statistics if enable_statistics else None
|
2021-05-20 11:05:15 +00:00
|
|
|
with patch(
|
2022-03-15 16:23:52 +00:00
|
|
|
"homeassistant.components.recorder.Recorder.async_nightly_tasks",
|
|
|
|
side_effect=nightly,
|
|
|
|
autospec=True,
|
2022-04-23 05:29:44 +00:00
|
|
|
), patch(
|
|
|
|
"homeassistant.components.recorder.Recorder.async_periodic_statistics",
|
|
|
|
side_effect=stats,
|
|
|
|
autospec=True,
|
2021-05-20 11:05:15 +00:00
|
|
|
):
|
|
|
|
|
|
|
|
def setup_recorder(config=None):
|
|
|
|
"""Set up with params."""
|
|
|
|
init_recorder_component(hass, config)
|
|
|
|
hass.start()
|
|
|
|
hass.block_till_done()
|
|
|
|
hass.data[recorder.DATA_INSTANCE].block_till_done()
|
|
|
|
return hass
|
|
|
|
|
|
|
|
yield setup_recorder
|
|
|
|
hass.stop()
|
2021-12-08 20:28:26 +00:00
|
|
|
|
2022-03-23 08:30:01 +00:00
|
|
|
# Restore timezone, it is set when creating the hass object
|
|
|
|
dt_util.DEFAULT_TIME_ZONE = original_tz
|
|
|
|
|
2021-12-08 20:28:26 +00:00
|
|
|
|
2022-04-26 16:08:00 +00:00
|
|
|
async def _async_init_recorder_component(hass, add_config=None):
|
|
|
|
"""Initialize the recorder asynchronously."""
|
|
|
|
config = dict(add_config) if add_config else {}
|
|
|
|
if recorder.CONF_DB_URL not in config:
|
|
|
|
config[recorder.CONF_DB_URL] = "sqlite://" # In memory DB
|
|
|
|
if recorder.CONF_COMMIT_INTERVAL not in config:
|
|
|
|
config[recorder.CONF_COMMIT_INTERVAL] = 0
|
|
|
|
|
|
|
|
with patch(
|
|
|
|
"homeassistant.components.recorder.ALLOW_IN_MEMORY_DB",
|
|
|
|
True,
|
|
|
|
), patch("homeassistant.components.recorder.migration.migrate_schema"):
|
|
|
|
assert await async_setup_component(
|
|
|
|
hass, recorder.DOMAIN, {recorder.DOMAIN: config}
|
|
|
|
)
|
|
|
|
assert recorder.DOMAIN in hass.config.components
|
|
|
|
_LOGGER.info(
|
|
|
|
"Test recorder successfully started, database location: %s",
|
|
|
|
config[recorder.CONF_DB_URL],
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2022-03-21 22:49:18 +00:00
|
|
|
@pytest.fixture
|
2022-04-23 05:29:44 +00:00
|
|
|
async def async_setup_recorder_instance(
|
|
|
|
enable_nightly_purge, enable_statistics
|
|
|
|
) -> AsyncGenerator[SetupRecorderInstanceT, None]:
|
|
|
|
"""Yield callable to setup recorder instance."""
|
|
|
|
|
|
|
|
async def async_setup_recorder(
|
|
|
|
hass: HomeAssistant, config: ConfigType | None = None
|
|
|
|
) -> recorder.Recorder:
|
|
|
|
"""Setup and return recorder instance.""" # noqa: D401
|
|
|
|
nightly = (
|
|
|
|
recorder.Recorder.async_nightly_tasks if enable_nightly_purge else None
|
2022-03-21 22:49:18 +00:00
|
|
|
)
|
2022-04-23 05:29:44 +00:00
|
|
|
stats = (
|
|
|
|
recorder.Recorder.async_periodic_statistics if enable_statistics else None
|
|
|
|
)
|
|
|
|
with patch(
|
|
|
|
"homeassistant.components.recorder.Recorder.async_nightly_tasks",
|
|
|
|
side_effect=nightly,
|
|
|
|
autospec=True,
|
|
|
|
), patch(
|
|
|
|
"homeassistant.components.recorder.Recorder.async_periodic_statistics",
|
|
|
|
side_effect=stats,
|
|
|
|
autospec=True,
|
|
|
|
):
|
2022-04-26 16:08:00 +00:00
|
|
|
await _async_init_recorder_component(hass, config)
|
2022-04-23 05:29:44 +00:00
|
|
|
await hass.async_block_till_done()
|
|
|
|
instance = hass.data[recorder.DATA_INSTANCE]
|
|
|
|
# The recorder's worker is not started until Home Assistant is running
|
|
|
|
if hass.state == CoreState.running:
|
2022-04-25 10:04:47 +00:00
|
|
|
await async_recorder_block_till_done(hass)
|
2022-04-23 05:29:44 +00:00
|
|
|
return instance
|
|
|
|
|
|
|
|
return async_setup_recorder
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
async def recorder_mock(recorder_config, async_setup_recorder_instance, hass):
|
|
|
|
"""Fixture with in-memory recorder."""
|
|
|
|
await async_setup_recorder_instance(hass, recorder_config)
|
2022-03-21 22:49:18 +00:00
|
|
|
|
|
|
|
|
2021-12-08 20:28:26 +00:00
|
|
|
@pytest.fixture
|
|
|
|
def mock_integration_frame():
|
|
|
|
"""Mock as if we're calling code from inside an integration."""
|
|
|
|
correct_frame = Mock(
|
|
|
|
filename="/home/paulus/homeassistant/components/hue/light.py",
|
|
|
|
lineno="23",
|
|
|
|
line="self.light.is_on",
|
|
|
|
)
|
|
|
|
with patch(
|
|
|
|
"homeassistant.helpers.frame.extract_stack",
|
|
|
|
return_value=[
|
|
|
|
Mock(
|
|
|
|
filename="/home/paulus/homeassistant/core.py",
|
|
|
|
lineno="23",
|
|
|
|
line="do_something()",
|
|
|
|
),
|
|
|
|
correct_frame,
|
|
|
|
Mock(
|
|
|
|
filename="/home/paulus/aiohue/lights.py",
|
|
|
|
lineno="2",
|
|
|
|
line="something()",
|
|
|
|
),
|
|
|
|
],
|
|
|
|
):
|
|
|
|
yield correct_frame
|