pytest-homeassistant-custom-component 0.13.163__py3-none-any.whl → 0.13.298__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pytest_homeassistant_custom_component/common.py +289 -46
- pytest_homeassistant_custom_component/components/__init__.py +351 -0
- pytest_homeassistant_custom_component/components/diagnostics/__init__.py +71 -0
- pytest_homeassistant_custom_component/components/recorder/common.py +143 -13
- pytest_homeassistant_custom_component/components/recorder/db_schema_0.py +1 -1
- pytest_homeassistant_custom_component/const.py +4 -3
- pytest_homeassistant_custom_component/patch_json.py +41 -0
- pytest_homeassistant_custom_component/patch_recorder.py +1 -1
- pytest_homeassistant_custom_component/patch_time.py +66 -0
- pytest_homeassistant_custom_component/plugins.py +429 -177
- pytest_homeassistant_custom_component/syrupy.py +188 -1
- pytest_homeassistant_custom_component/test_util/aiohttp.py +56 -20
- pytest_homeassistant_custom_component/typing.py +5 -0
- {pytest_homeassistant_custom_component-0.13.163.dist-info → pytest_homeassistant_custom_component-0.13.298.dist-info}/METADATA +54 -29
- pytest_homeassistant_custom_component-0.13.298.dist-info/RECORD +28 -0
- {pytest_homeassistant_custom_component-0.13.163.dist-info → pytest_homeassistant_custom_component-0.13.298.dist-info}/WHEEL +1 -1
- pytest_homeassistant_custom_component-0.13.163.dist-info/RECORD +0 -26
- {pytest_homeassistant_custom_component-0.13.163.dist-info → pytest_homeassistant_custom_component-0.13.298.dist-info}/entry_points.txt +0 -0
- {pytest_homeassistant_custom_component-0.13.163.dist-info → pytest_homeassistant_custom_component-0.13.298.dist-info/licenses}/LICENSE +0 -0
- {pytest_homeassistant_custom_component-0.13.163.dist-info → pytest_homeassistant_custom_component-0.13.298.dist-info/licenses}/LICENSE_HA_CORE.md +0 -0
- {pytest_homeassistant_custom_component-0.13.163.dist-info → pytest_homeassistant_custom_component-0.13.298.dist-info}/top_level.txt +0 -0
|
@@ -15,15 +15,18 @@ import gc
|
|
|
15
15
|
import itertools
|
|
16
16
|
import logging
|
|
17
17
|
import os
|
|
18
|
+
import pathlib
|
|
18
19
|
import reprlib
|
|
19
20
|
from shutil import rmtree
|
|
20
21
|
import sqlite3
|
|
21
22
|
import ssl
|
|
23
|
+
import sys
|
|
22
24
|
import threading
|
|
23
25
|
from typing import TYPE_CHECKING, Any, cast
|
|
24
26
|
from unittest.mock import AsyncMock, MagicMock, Mock, _patch, patch
|
|
25
27
|
|
|
26
28
|
from aiohttp import client
|
|
29
|
+
from aiohttp.resolver import AsyncResolver
|
|
27
30
|
from aiohttp.test_utils import (
|
|
28
31
|
BaseTestServer,
|
|
29
32
|
TestClient,
|
|
@@ -36,33 +39,47 @@ import bcrypt
|
|
|
36
39
|
import freezegun
|
|
37
40
|
import multidict
|
|
38
41
|
import pytest
|
|
42
|
+
import pytest_asyncio
|
|
39
43
|
import pytest_socket
|
|
40
44
|
import requests_mock
|
|
41
45
|
import respx
|
|
42
46
|
from syrupy.assertion import SnapshotAssertion
|
|
47
|
+
from syrupy.session import SnapshotSession
|
|
48
|
+
|
|
49
|
+
# Setup patching of JSON functions before any other Home Assistant imports
|
|
50
|
+
from . import patch_json # isort:skip
|
|
43
51
|
|
|
44
52
|
from homeassistant import block_async_io
|
|
45
53
|
from homeassistant.exceptions import ServiceNotFound
|
|
46
54
|
|
|
47
55
|
# Setup patching of recorder functions before any other Home Assistant imports
|
|
48
|
-
from . import patch_recorder #
|
|
56
|
+
from . import patch_recorder # isort:skip
|
|
49
57
|
|
|
50
58
|
# Setup patching of dt_util time functions before any other Home Assistant imports
|
|
51
|
-
from . import patch_time #
|
|
59
|
+
from . import patch_time # isort:skip
|
|
52
60
|
|
|
53
|
-
from homeassistant import core as ha, loader, runner
|
|
61
|
+
from homeassistant import components, core as ha, loader, runner
|
|
54
62
|
from homeassistant.auth.const import GROUP_ID_ADMIN, GROUP_ID_READ_ONLY
|
|
55
63
|
from homeassistant.auth.models import Credentials
|
|
56
64
|
from homeassistant.auth.providers import homeassistant
|
|
57
65
|
from homeassistant.components.device_tracker.legacy import Device
|
|
66
|
+
|
|
67
|
+
# pylint: disable-next=hass-component-root-import
|
|
58
68
|
from homeassistant.components.websocket_api.auth import (
|
|
59
69
|
TYPE_AUTH,
|
|
60
70
|
TYPE_AUTH_OK,
|
|
61
71
|
TYPE_AUTH_REQUIRED,
|
|
62
72
|
)
|
|
73
|
+
|
|
74
|
+
# pylint: disable-next=hass-component-root-import
|
|
63
75
|
from homeassistant.components.websocket_api.http import URL
|
|
64
76
|
from homeassistant.config import YAML_CONFIG_FILE
|
|
65
|
-
from homeassistant.config_entries import
|
|
77
|
+
from homeassistant.config_entries import (
|
|
78
|
+
ConfigEntries,
|
|
79
|
+
ConfigEntry,
|
|
80
|
+
ConfigEntryState,
|
|
81
|
+
ConfigSubentryData,
|
|
82
|
+
)
|
|
66
83
|
from homeassistant.const import BASE_PLATFORMS, HASSIO_USER_NAME
|
|
67
84
|
from homeassistant.core import (
|
|
68
85
|
Context,
|
|
@@ -79,26 +96,30 @@ from homeassistant.helpers import (
|
|
|
79
96
|
device_registry as dr,
|
|
80
97
|
entity_registry as er,
|
|
81
98
|
floor_registry as fr,
|
|
99
|
+
frame,
|
|
82
100
|
issue_registry as ir,
|
|
83
101
|
label_registry as lr,
|
|
84
102
|
recorder as recorder_helper,
|
|
103
|
+
translation as translation_helper,
|
|
85
104
|
)
|
|
86
105
|
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
|
106
|
+
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
|
|
87
107
|
from homeassistant.helpers.translation import _TranslationsCacheData
|
|
88
108
|
from homeassistant.helpers.typing import ConfigType
|
|
89
109
|
from homeassistant.setup import async_setup_component
|
|
90
|
-
from homeassistant.util import dt as dt_util, location
|
|
110
|
+
from homeassistant.util import dt as dt_util, location as location_util
|
|
91
111
|
from homeassistant.util.async_ import create_eager_task, get_scheduled_timer_handles
|
|
92
112
|
from homeassistant.util.json import json_loads
|
|
93
113
|
|
|
94
114
|
from .ignore_uncaught_exceptions import IGNORE_UNCAUGHT_EXCEPTIONS
|
|
95
|
-
from .syrupy import HomeAssistantSnapshotExtension
|
|
115
|
+
from .syrupy import HomeAssistantSnapshotExtension, override_syrupy_finish
|
|
96
116
|
from .typing import (
|
|
97
117
|
ClientSessionGenerator,
|
|
98
118
|
MockHAClientWebSocket,
|
|
99
119
|
MqttMockHAClient,
|
|
100
120
|
MqttMockHAClientGenerator,
|
|
101
121
|
MqttMockPahoClient,
|
|
122
|
+
RecorderInstanceContextManager,
|
|
102
123
|
RecorderInstanceGenerator,
|
|
103
124
|
WebSocketGenerator,
|
|
104
125
|
)
|
|
@@ -106,14 +127,17 @@ from .typing import (
|
|
|
106
127
|
if TYPE_CHECKING:
|
|
107
128
|
# Local import to avoid processing recorder and SQLite modules when running a
|
|
108
129
|
# testcase which does not use the recorder.
|
|
130
|
+
from homeassistant.auth.models import RefreshToken
|
|
109
131
|
from homeassistant.components import recorder
|
|
110
132
|
|
|
133
|
+
|
|
111
134
|
pytest.register_assert_rewrite(".common")
|
|
112
135
|
|
|
113
136
|
from .common import ( # noqa: E402, isort:skip
|
|
114
137
|
CLIENT_ID,
|
|
115
138
|
INSTANCES,
|
|
116
139
|
MockConfigEntry,
|
|
140
|
+
MockMqttReasonCode,
|
|
117
141
|
MockUser,
|
|
118
142
|
async_fire_mqtt_message,
|
|
119
143
|
async_test_home_assistant,
|
|
@@ -139,6 +163,7 @@ asyncio.set_event_loop_policy = lambda policy: None
|
|
|
139
163
|
def pytest_addoption(parser: pytest.Parser) -> None:
|
|
140
164
|
"""Register custom pytest options."""
|
|
141
165
|
parser.addoption("--dburl", action="store", default="sqlite://")
|
|
166
|
+
parser.addoption("--drop-existing-db", action="store_const", const=True)
|
|
142
167
|
|
|
143
168
|
|
|
144
169
|
def pytest_configure(config: pytest.Config) -> None:
|
|
@@ -149,6 +174,11 @@ def pytest_configure(config: pytest.Config) -> None:
|
|
|
149
174
|
if config.getoption("verbose") > 0:
|
|
150
175
|
logging.getLogger().setLevel(logging.DEBUG)
|
|
151
176
|
|
|
177
|
+
# Override default finish to detect unused snapshots despite xdist
|
|
178
|
+
# Temporary workaround until it is finalised inside syrupy
|
|
179
|
+
# See https://github.com/syrupy-project/syrupy/pull/901
|
|
180
|
+
SnapshotSession.finish = override_syrupy_finish
|
|
181
|
+
|
|
152
182
|
|
|
153
183
|
def pytest_runtest_setup() -> None:
|
|
154
184
|
"""Prepare pytest_socket and freezegun.
|
|
@@ -161,69 +191,33 @@ def pytest_runtest_setup() -> None:
|
|
|
161
191
|
destinations will be allowed.
|
|
162
192
|
|
|
163
193
|
freezegun:
|
|
164
|
-
Modified to include https://github.com/spulec/freezegun/pull/424
|
|
194
|
+
Modified to include https://github.com/spulec/freezegun/pull/424 and improve class str.
|
|
165
195
|
"""
|
|
166
196
|
pytest_socket.socket_allow_hosts(["127.0.0.1"])
|
|
167
197
|
pytest_socket.disable_socket(allow_unix_socket=True)
|
|
168
198
|
|
|
169
|
-
freezegun.api.
|
|
170
|
-
|
|
199
|
+
freezegun.api.FakeDate = patch_time.HAFakeDate # type: ignore[attr-defined]
|
|
200
|
+
|
|
201
|
+
freezegun.api.datetime_to_fakedatetime = patch_time.ha_datetime_to_fakedatetime # type: ignore[attr-defined]
|
|
202
|
+
freezegun.api.FakeDatetime = patch_time.HAFakeDatetime # type: ignore[attr-defined]
|
|
171
203
|
|
|
172
204
|
def adapt_datetime(val):
|
|
173
205
|
return val.isoformat(" ")
|
|
174
206
|
|
|
175
207
|
# Setup HAFakeDatetime converter for sqlite3
|
|
176
|
-
sqlite3.register_adapter(HAFakeDatetime, adapt_datetime)
|
|
208
|
+
sqlite3.register_adapter(patch_time.HAFakeDatetime, adapt_datetime)
|
|
177
209
|
|
|
178
210
|
# Setup HAFakeDatetime converter for pymysql
|
|
179
211
|
try:
|
|
180
|
-
#
|
|
181
|
-
import MySQLdb.converters as MySQLdb_converters
|
|
212
|
+
import MySQLdb.converters as MySQLdb_converters # noqa: PLC0415
|
|
182
213
|
except ImportError:
|
|
183
214
|
pass
|
|
184
215
|
else:
|
|
185
|
-
MySQLdb_converters.conversions[HAFakeDatetime] = (
|
|
216
|
+
MySQLdb_converters.conversions[patch_time.HAFakeDatetime] = (
|
|
186
217
|
MySQLdb_converters.DateTime2literal
|
|
187
218
|
)
|
|
188
219
|
|
|
189
220
|
|
|
190
|
-
def ha_datetime_to_fakedatetime(datetime) -> freezegun.api.FakeDatetime: # type: ignore[name-defined]
|
|
191
|
-
"""Convert datetime to FakeDatetime.
|
|
192
|
-
|
|
193
|
-
Modified to include https://github.com/spulec/freezegun/pull/424.
|
|
194
|
-
"""
|
|
195
|
-
return freezegun.api.FakeDatetime( # type: ignore[attr-defined]
|
|
196
|
-
datetime.year,
|
|
197
|
-
datetime.month,
|
|
198
|
-
datetime.day,
|
|
199
|
-
datetime.hour,
|
|
200
|
-
datetime.minute,
|
|
201
|
-
datetime.second,
|
|
202
|
-
datetime.microsecond,
|
|
203
|
-
datetime.tzinfo,
|
|
204
|
-
fold=datetime.fold,
|
|
205
|
-
)
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
class HAFakeDatetime(freezegun.api.FakeDatetime): # type: ignore[name-defined]
|
|
209
|
-
"""Modified to include https://github.com/spulec/freezegun/pull/424."""
|
|
210
|
-
|
|
211
|
-
@classmethod
|
|
212
|
-
def now(cls, tz=None):
|
|
213
|
-
"""Return frozen now."""
|
|
214
|
-
now = cls._time_to_freeze() or freezegun.api.real_datetime.now()
|
|
215
|
-
if tz:
|
|
216
|
-
result = tz.fromutc(now.replace(tzinfo=tz))
|
|
217
|
-
else:
|
|
218
|
-
result = now
|
|
219
|
-
|
|
220
|
-
# Add the _tz_offset only if it's non-zero to preserve fold
|
|
221
|
-
if cls._tz_offset():
|
|
222
|
-
result += cls._tz_offset()
|
|
223
|
-
|
|
224
|
-
return ha_datetime_to_fakedatetime(result)
|
|
225
|
-
|
|
226
|
-
|
|
227
221
|
def check_real[**_P, _R](func: Callable[_P, Coroutine[Any, Any, _R]]):
|
|
228
222
|
"""Force a function to require a keyword _test_real to be passed in."""
|
|
229
223
|
|
|
@@ -242,7 +236,9 @@ def check_real[**_P, _R](func: Callable[_P, Coroutine[Any, Any, _R]]):
|
|
|
242
236
|
|
|
243
237
|
|
|
244
238
|
# Guard a few functions that would make network connections
|
|
245
|
-
|
|
239
|
+
location_util.async_detect_location_info = check_real(
|
|
240
|
+
location_util.async_detect_location_info
|
|
241
|
+
)
|
|
246
242
|
|
|
247
243
|
|
|
248
244
|
@pytest.fixture(name="caplog")
|
|
@@ -263,6 +259,7 @@ def garbage_collection() -> None:
|
|
|
263
259
|
to run per test case if needed.
|
|
264
260
|
"""
|
|
265
261
|
gc.collect()
|
|
262
|
+
gc.freeze()
|
|
266
263
|
|
|
267
264
|
|
|
268
265
|
@pytest.fixture(autouse=True)
|
|
@@ -340,18 +337,18 @@ def long_repr_strings() -> Generator[None]:
|
|
|
340
337
|
|
|
341
338
|
|
|
342
339
|
@pytest.fixture(autouse=True)
|
|
343
|
-
def enable_event_loop_debug(
|
|
340
|
+
def enable_event_loop_debug() -> None:
|
|
344
341
|
"""Enable event loop debug mode."""
|
|
345
|
-
|
|
342
|
+
asyncio.get_event_loop().set_debug(True)
|
|
346
343
|
|
|
347
344
|
|
|
348
345
|
@pytest.fixture(autouse=True)
|
|
349
346
|
def verify_cleanup(
|
|
350
|
-
event_loop: asyncio.AbstractEventLoop,
|
|
351
347
|
expected_lingering_tasks: bool,
|
|
352
348
|
expected_lingering_timers: bool,
|
|
353
349
|
) -> Generator[None]:
|
|
354
350
|
"""Verify that the test has cleaned up resources correctly."""
|
|
351
|
+
event_loop = asyncio.get_event_loop()
|
|
355
352
|
threads_before = frozenset(threading.enumerate())
|
|
356
353
|
tasks_before = asyncio.all_tasks(event_loop)
|
|
357
354
|
yield
|
|
@@ -392,8 +389,10 @@ def verify_cleanup(
|
|
|
392
389
|
# Verify no threads where left behind.
|
|
393
390
|
threads = frozenset(threading.enumerate()) - threads_before
|
|
394
391
|
for thread in threads:
|
|
395
|
-
assert
|
|
396
|
-
|
|
392
|
+
assert (
|
|
393
|
+
isinstance(thread, threading._DummyThread)
|
|
394
|
+
or thread.name.startswith("waitpid-")
|
|
395
|
+
or "_run_safe_shutdown_loop" in thread.name
|
|
397
396
|
)
|
|
398
397
|
|
|
399
398
|
try:
|
|
@@ -405,20 +404,34 @@ def verify_cleanup(
|
|
|
405
404
|
|
|
406
405
|
try:
|
|
407
406
|
# Verify respx.mock has been cleaned up
|
|
408
|
-
assert not respx.mock.routes,
|
|
407
|
+
assert not respx.mock.routes, (
|
|
408
|
+
"respx.mock routes not cleaned up, maybe the test needs to be decorated with @respx.mock"
|
|
409
|
+
)
|
|
409
410
|
finally:
|
|
410
411
|
# Clear mock routes not break subsequent tests
|
|
411
412
|
respx.mock.clear()
|
|
412
413
|
|
|
413
414
|
|
|
414
415
|
@pytest.fixture(autouse=True)
|
|
415
|
-
def
|
|
416
|
-
"""Reset
|
|
416
|
+
def reset_globals() -> Generator[None]:
|
|
417
|
+
"""Reset global objects for every test case."""
|
|
417
418
|
yield
|
|
419
|
+
|
|
420
|
+
# Reset the _Hass threading.local object
|
|
418
421
|
ha._hass.__dict__.clear()
|
|
419
422
|
|
|
423
|
+
# Reset the frame helper globals
|
|
424
|
+
frame.async_setup(None)
|
|
425
|
+
frame._REPORTED_INTEGRATIONS.clear()
|
|
426
|
+
|
|
427
|
+
# Reset patch_json
|
|
428
|
+
if patch_json.mock_objects:
|
|
429
|
+
obj = patch_json.mock_objects.pop()
|
|
430
|
+
patch_json.mock_objects.clear()
|
|
431
|
+
pytest.fail(f"Test attempted to serialize mock object {obj}")
|
|
420
432
|
|
|
421
|
-
|
|
433
|
+
|
|
434
|
+
@pytest.fixture(autouse=True, scope="session")
|
|
422
435
|
def bcrypt_cost() -> Generator[None]:
|
|
423
436
|
"""Run with reduced rounds during tests, to speed up uses."""
|
|
424
437
|
gensalt_orig = bcrypt.gensalt
|
|
@@ -488,9 +501,7 @@ def aiohttp_client_cls() -> type[CoalescingClient]:
|
|
|
488
501
|
|
|
489
502
|
|
|
490
503
|
@pytest.fixture
|
|
491
|
-
def aiohttp_client(
|
|
492
|
-
event_loop: asyncio.AbstractEventLoop,
|
|
493
|
-
) -> Generator[ClientSessionGenerator]:
|
|
504
|
+
def aiohttp_client() -> Generator[ClientSessionGenerator]:
|
|
494
505
|
"""Override the default aiohttp_client since 3.x does not support aiohttp_client_cls.
|
|
495
506
|
|
|
496
507
|
Remove this when upgrading to 4.x as aiohttp_client_cls
|
|
@@ -500,34 +511,35 @@ def aiohttp_client(
|
|
|
500
511
|
aiohttp_client(server, **kwargs)
|
|
501
512
|
aiohttp_client(raw_server, **kwargs)
|
|
502
513
|
"""
|
|
503
|
-
loop =
|
|
514
|
+
loop = asyncio.get_event_loop()
|
|
504
515
|
clients = []
|
|
505
516
|
|
|
506
517
|
async def go(
|
|
507
|
-
|
|
518
|
+
param: Application | BaseTestServer,
|
|
519
|
+
/,
|
|
508
520
|
*args: Any,
|
|
509
521
|
server_kwargs: dict[str, Any] | None = None,
|
|
510
522
|
**kwargs: Any,
|
|
511
523
|
) -> TestClient:
|
|
512
|
-
if isinstance(
|
|
513
|
-
|
|
524
|
+
if isinstance(param, Callable) and not isinstance( # type: ignore[arg-type]
|
|
525
|
+
param, (Application, BaseTestServer)
|
|
514
526
|
):
|
|
515
|
-
|
|
527
|
+
param = param(loop, *args, **kwargs)
|
|
516
528
|
kwargs = {}
|
|
517
529
|
else:
|
|
518
530
|
assert not args, "args should be empty"
|
|
519
531
|
|
|
520
532
|
client: TestClient
|
|
521
|
-
if isinstance(
|
|
533
|
+
if isinstance(param, Application):
|
|
522
534
|
server_kwargs = server_kwargs or {}
|
|
523
|
-
server = TestServer(
|
|
535
|
+
server = TestServer(param, loop=loop, **server_kwargs)
|
|
524
536
|
# Registering a view after starting the server should still work.
|
|
525
537
|
server.app._router.freeze = lambda: None
|
|
526
538
|
client = CoalescingClient(server, loop=loop, **kwargs)
|
|
527
|
-
elif isinstance(
|
|
528
|
-
client = TestClient(
|
|
539
|
+
elif isinstance(param, BaseTestServer):
|
|
540
|
+
client = TestClient(param, loop=loop, **kwargs)
|
|
529
541
|
else:
|
|
530
|
-
raise TypeError(f"Unknown argument type: {type(
|
|
542
|
+
raise TypeError(f"Unknown argument type: {type(param)!r}")
|
|
531
543
|
|
|
532
544
|
await client.start_server()
|
|
533
545
|
clients.append(client)
|
|
@@ -572,7 +584,7 @@ async def hass(
|
|
|
572
584
|
exceptions.append(
|
|
573
585
|
Exception(
|
|
574
586
|
"Received exception handler without exception, "
|
|
575
|
-
f"but with message: {context[
|
|
587
|
+
f"but with message: {context['message']}"
|
|
576
588
|
)
|
|
577
589
|
)
|
|
578
590
|
orig_exception_handler(loop, context)
|
|
@@ -581,6 +593,7 @@ async def hass(
|
|
|
581
593
|
async with async_test_home_assistant(loop, load_registries) as hass:
|
|
582
594
|
orig_exception_handler = loop.get_exception_handler()
|
|
583
595
|
loop.set_exception_handler(exc_handle)
|
|
596
|
+
frame.async_setup(hass)
|
|
584
597
|
|
|
585
598
|
yield hass
|
|
586
599
|
|
|
@@ -821,7 +834,7 @@ def hass_client_no_auth(
|
|
|
821
834
|
@pytest.fixture
|
|
822
835
|
def current_request() -> Generator[MagicMock]:
|
|
823
836
|
"""Mock current request."""
|
|
824
|
-
with patch("homeassistant.
|
|
837
|
+
with patch("homeassistant.helpers.http.current_request") as mock_request_context:
|
|
825
838
|
mocked_request = make_mocked_request(
|
|
826
839
|
"GET",
|
|
827
840
|
"/some/request",
|
|
@@ -916,7 +929,19 @@ def fail_on_log_exception(
|
|
|
916
929
|
|
|
917
930
|
@pytest.fixture
|
|
918
931
|
def mqtt_config_entry_data() -> dict[str, Any] | None:
|
|
919
|
-
"""Fixture to allow overriding MQTT
|
|
932
|
+
"""Fixture to allow overriding MQTT entry data."""
|
|
933
|
+
return None
|
|
934
|
+
|
|
935
|
+
|
|
936
|
+
@pytest.fixture
|
|
937
|
+
def mqtt_config_subentries_data() -> tuple[ConfigSubentryData] | None:
|
|
938
|
+
"""Fixture to allow overriding MQTT subentries data."""
|
|
939
|
+
return None
|
|
940
|
+
|
|
941
|
+
|
|
942
|
+
@pytest.fixture
|
|
943
|
+
def mqtt_config_entry_options() -> dict[str, Any] | None:
|
|
944
|
+
"""Fixture to allow overriding MQTT entry options."""
|
|
920
945
|
return None
|
|
921
946
|
|
|
922
947
|
|
|
@@ -949,17 +974,23 @@ def mqtt_client_mock(hass: HomeAssistant) -> Generator[MqttMockPahoClient]:
|
|
|
949
974
|
def _async_fire_mqtt_message(topic, payload, qos, retain):
|
|
950
975
|
async_fire_mqtt_message(hass, topic, payload or b"", qos, retain)
|
|
951
976
|
mid = get_mid()
|
|
952
|
-
hass.loop.call_soon(
|
|
977
|
+
hass.loop.call_soon(
|
|
978
|
+
mock_client.on_publish, Mock(), 0, mid, MockMqttReasonCode(), None
|
|
979
|
+
)
|
|
953
980
|
return FakeInfo(mid)
|
|
954
981
|
|
|
955
982
|
def _subscribe(topic, qos=0):
|
|
956
983
|
mid = get_mid()
|
|
957
|
-
hass.loop.call_soon(
|
|
984
|
+
hass.loop.call_soon(
|
|
985
|
+
mock_client.on_subscribe, Mock(), 0, mid, [MockMqttReasonCode()], None
|
|
986
|
+
)
|
|
958
987
|
return (0, mid)
|
|
959
988
|
|
|
960
989
|
def _unsubscribe(topic):
|
|
961
990
|
mid = get_mid()
|
|
962
|
-
hass.loop.call_soon(
|
|
991
|
+
hass.loop.call_soon(
|
|
992
|
+
mock_client.on_unsubscribe, Mock(), 0, mid, [MockMqttReasonCode()], None
|
|
993
|
+
)
|
|
963
994
|
return (0, mid)
|
|
964
995
|
|
|
965
996
|
def _connect(*args, **kwargs):
|
|
@@ -968,7 +999,7 @@ def mqtt_client_mock(hass: HomeAssistant) -> Generator[MqttMockPahoClient]:
|
|
|
968
999
|
# the behavior.
|
|
969
1000
|
mock_client.reconnect()
|
|
970
1001
|
hass.loop.call_soon_threadsafe(
|
|
971
|
-
mock_client.on_connect, mock_client, None, 0,
|
|
1002
|
+
mock_client.on_connect, mock_client, None, 0, MockMqttReasonCode()
|
|
972
1003
|
)
|
|
973
1004
|
mock_client.on_socket_open(
|
|
974
1005
|
mock_client, None, Mock(fileno=Mock(return_value=-1))
|
|
@@ -993,6 +1024,8 @@ async def mqtt_mock(
|
|
|
993
1024
|
mock_hass_config: None,
|
|
994
1025
|
mqtt_client_mock: MqttMockPahoClient,
|
|
995
1026
|
mqtt_config_entry_data: dict[str, Any] | None,
|
|
1027
|
+
mqtt_config_entry_options: dict[str, Any] | None,
|
|
1028
|
+
mqtt_config_subentries_data: tuple[ConfigSubentryData] | None,
|
|
996
1029
|
mqtt_mock_entry: MqttMockHAClientGenerator,
|
|
997
1030
|
) -> AsyncGenerator[MqttMockHAClient]:
|
|
998
1031
|
"""Fixture to mock MQTT component."""
|
|
@@ -1004,24 +1037,29 @@ async def _mqtt_mock_entry(
|
|
|
1004
1037
|
hass: HomeAssistant,
|
|
1005
1038
|
mqtt_client_mock: MqttMockPahoClient,
|
|
1006
1039
|
mqtt_config_entry_data: dict[str, Any] | None,
|
|
1040
|
+
mqtt_config_entry_options: dict[str, Any] | None,
|
|
1041
|
+
mqtt_config_subentries_data: tuple[ConfigSubentryData] | None,
|
|
1007
1042
|
) -> AsyncGenerator[MqttMockHAClientGenerator]:
|
|
1008
1043
|
"""Fixture to mock a delayed setup of the MQTT config entry."""
|
|
1009
1044
|
# Local import to avoid processing MQTT modules when running a testcase
|
|
1010
1045
|
# which does not use MQTT.
|
|
1011
|
-
from homeassistant.components import mqtt #
|
|
1046
|
+
from homeassistant.components import mqtt # noqa: PLC0415
|
|
1012
1047
|
|
|
1013
1048
|
if mqtt_config_entry_data is None:
|
|
1014
|
-
mqtt_config_entry_data = {
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
}
|
|
1049
|
+
mqtt_config_entry_data = {mqtt.CONF_BROKER: "mock-broker"}
|
|
1050
|
+
if mqtt_config_entry_options is None:
|
|
1051
|
+
mqtt_config_entry_options = {mqtt.CONF_BIRTH_MESSAGE: {}}
|
|
1018
1052
|
|
|
1019
1053
|
await hass.async_block_till_done()
|
|
1020
1054
|
|
|
1021
1055
|
entry = MockConfigEntry(
|
|
1022
1056
|
data=mqtt_config_entry_data,
|
|
1057
|
+
options=mqtt_config_entry_options,
|
|
1058
|
+
subentries_data=mqtt_config_subentries_data,
|
|
1023
1059
|
domain=mqtt.DOMAIN,
|
|
1024
1060
|
title="MQTT",
|
|
1061
|
+
version=1,
|
|
1062
|
+
minor_version=2,
|
|
1025
1063
|
)
|
|
1026
1064
|
entry.add_to_hass(hass)
|
|
1027
1065
|
|
|
@@ -1037,12 +1075,11 @@ async def _mqtt_mock_entry(
|
|
|
1037
1075
|
|
|
1038
1076
|
# Assert that MQTT is setup
|
|
1039
1077
|
assert real_mqtt_instance is not None, "MQTT was not setup correctly"
|
|
1040
|
-
mock_mqtt_instance.conf = real_mqtt_instance.conf # For diagnostics
|
|
1041
1078
|
mock_mqtt_instance._mqttc = mqtt_client_mock
|
|
1042
1079
|
|
|
1043
1080
|
# connected set to True to get a more realistic behavior when subscribing
|
|
1044
1081
|
mock_mqtt_instance.connected = True
|
|
1045
|
-
mqtt_client_mock.on_connect(mqtt_client_mock, None, 0,
|
|
1082
|
+
mqtt_client_mock.on_connect(mqtt_client_mock, None, 0, MockMqttReasonCode())
|
|
1046
1083
|
|
|
1047
1084
|
async_dispatcher_send(hass, mqtt.MQTT_CONNECTION_STATE, True)
|
|
1048
1085
|
await hass.async_block_till_done()
|
|
@@ -1132,6 +1169,8 @@ async def mqtt_mock_entry(
|
|
|
1132
1169
|
hass: HomeAssistant,
|
|
1133
1170
|
mqtt_client_mock: MqttMockPahoClient,
|
|
1134
1171
|
mqtt_config_entry_data: dict[str, Any] | None,
|
|
1172
|
+
mqtt_config_entry_options: dict[str, Any] | None,
|
|
1173
|
+
mqtt_config_subentries_data: tuple[ConfigSubentryData] | None,
|
|
1135
1174
|
) -> AsyncGenerator[MqttMockHAClientGenerator]:
|
|
1136
1175
|
"""Set up an MQTT config entry."""
|
|
1137
1176
|
|
|
@@ -1148,7 +1187,11 @@ async def mqtt_mock_entry(
|
|
|
1148
1187
|
return await mqtt_mock_entry(_async_setup_config_entry)
|
|
1149
1188
|
|
|
1150
1189
|
async with _mqtt_mock_entry(
|
|
1151
|
-
hass,
|
|
1190
|
+
hass,
|
|
1191
|
+
mqtt_client_mock,
|
|
1192
|
+
mqtt_config_entry_data,
|
|
1193
|
+
mqtt_config_entry_options,
|
|
1194
|
+
mqtt_config_subentries_data,
|
|
1152
1195
|
) as mqtt_mock_entry:
|
|
1153
1196
|
yield _setup_mqtt_entry
|
|
1154
1197
|
|
|
@@ -1156,15 +1199,31 @@ async def mqtt_mock_entry(
|
|
|
1156
1199
|
@pytest.fixture(autouse=True, scope="session")
|
|
1157
1200
|
def mock_network() -> Generator[None]:
|
|
1158
1201
|
"""Mock network."""
|
|
1159
|
-
with
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1202
|
+
with (
|
|
1203
|
+
patch(
|
|
1204
|
+
"homeassistant.components.network.util.ifaddr.get_adapters",
|
|
1205
|
+
return_value=[
|
|
1206
|
+
Mock(
|
|
1207
|
+
nice_name="eth0",
|
|
1208
|
+
ips=[Mock(is_IPv6=False, ip="10.10.10.10", network_prefix=24)],
|
|
1209
|
+
index=0,
|
|
1210
|
+
)
|
|
1211
|
+
],
|
|
1212
|
+
),
|
|
1213
|
+
patch(
|
|
1214
|
+
"homeassistant.components.network.async_get_loaded_adapters",
|
|
1215
|
+
return_value=[
|
|
1216
|
+
{
|
|
1217
|
+
"auto": True,
|
|
1218
|
+
"default": True,
|
|
1219
|
+
"enabled": True,
|
|
1220
|
+
"index": 0,
|
|
1221
|
+
"ipv4": [{"address": "10.10.10.10", "network_prefix": 24}],
|
|
1222
|
+
"ipv6": [],
|
|
1223
|
+
"name": "eth0",
|
|
1224
|
+
}
|
|
1225
|
+
],
|
|
1226
|
+
),
|
|
1168
1227
|
):
|
|
1169
1228
|
yield
|
|
1170
1229
|
|
|
@@ -1185,7 +1244,11 @@ def mock_get_source_ip() -> Generator[_patch]:
|
|
|
1185
1244
|
|
|
1186
1245
|
@pytest.fixture(autouse=True, scope="session")
|
|
1187
1246
|
def translations_once() -> Generator[_patch]:
|
|
1188
|
-
"""Only load translations once per session.
|
|
1247
|
+
"""Only load translations once per session.
|
|
1248
|
+
|
|
1249
|
+
Note: To avoid issues with tests that mock integrations, translations for
|
|
1250
|
+
mocked integrations are cleaned up by the evict_faked_translations fixture.
|
|
1251
|
+
"""
|
|
1189
1252
|
cache = _TranslationsCacheData({}, {})
|
|
1190
1253
|
patcher = patch(
|
|
1191
1254
|
"homeassistant.helpers.translation._TranslationsCacheData",
|
|
@@ -1198,6 +1261,30 @@ def translations_once() -> Generator[_patch]:
|
|
|
1198
1261
|
patcher.stop()
|
|
1199
1262
|
|
|
1200
1263
|
|
|
1264
|
+
@pytest.fixture(autouse=True, scope="module")
|
|
1265
|
+
def evict_faked_translations(translations_once) -> Generator[_patch]:
|
|
1266
|
+
"""Clear translations for mocked integrations from the cache after each module."""
|
|
1267
|
+
real_component_strings = translation_helper._async_get_component_strings
|
|
1268
|
+
with patch(
|
|
1269
|
+
"homeassistant.helpers.translation._async_get_component_strings",
|
|
1270
|
+
wraps=real_component_strings,
|
|
1271
|
+
) as mock_component_strings:
|
|
1272
|
+
yield
|
|
1273
|
+
cache: _TranslationsCacheData = translations_once.kwargs["return_value"]
|
|
1274
|
+
component_paths = components.__path__
|
|
1275
|
+
|
|
1276
|
+
for call in mock_component_strings.mock_calls:
|
|
1277
|
+
integrations: dict[str, loader.Integration] = call.args[3]
|
|
1278
|
+
for domain, integration in integrations.items():
|
|
1279
|
+
if any(
|
|
1280
|
+
pathlib.Path(f"{component_path}/{domain}") == integration.file_path
|
|
1281
|
+
for component_path in component_paths
|
|
1282
|
+
):
|
|
1283
|
+
continue
|
|
1284
|
+
for loaded_for_lang in cache.loaded.values():
|
|
1285
|
+
loaded_for_lang.discard(domain)
|
|
1286
|
+
|
|
1287
|
+
|
|
1201
1288
|
@pytest.fixture
|
|
1202
1289
|
def disable_translations_once(
|
|
1203
1290
|
translations_once: _patch,
|
|
@@ -1208,15 +1295,45 @@ def disable_translations_once(
|
|
|
1208
1295
|
translations_once.start()
|
|
1209
1296
|
|
|
1210
1297
|
|
|
1298
|
+
@pytest_asyncio.fixture(autouse=True, scope="session", loop_scope="session")
|
|
1299
|
+
async def mock_zeroconf_resolver() -> AsyncGenerator[_patch]:
|
|
1300
|
+
"""Mock out the zeroconf resolver."""
|
|
1301
|
+
resolver = AsyncResolver()
|
|
1302
|
+
resolver.real_close = resolver.close
|
|
1303
|
+
patcher = patch(
|
|
1304
|
+
"homeassistant.helpers.aiohttp_client._async_make_resolver",
|
|
1305
|
+
return_value=resolver,
|
|
1306
|
+
)
|
|
1307
|
+
patcher.start()
|
|
1308
|
+
try:
|
|
1309
|
+
yield patcher
|
|
1310
|
+
finally:
|
|
1311
|
+
patcher.stop()
|
|
1312
|
+
|
|
1313
|
+
|
|
1314
|
+
@pytest.fixture
|
|
1315
|
+
def disable_mock_zeroconf_resolver(
|
|
1316
|
+
mock_zeroconf_resolver: _patch,
|
|
1317
|
+
) -> Generator[None]:
|
|
1318
|
+
"""Disable the zeroconf resolver."""
|
|
1319
|
+
mock_zeroconf_resolver.stop()
|
|
1320
|
+
yield
|
|
1321
|
+
mock_zeroconf_resolver.start()
|
|
1322
|
+
|
|
1323
|
+
|
|
1211
1324
|
@pytest.fixture
|
|
1212
1325
|
def mock_zeroconf() -> Generator[MagicMock]:
|
|
1213
1326
|
"""Mock zeroconf."""
|
|
1214
|
-
from zeroconf import DNSCache #
|
|
1327
|
+
from zeroconf import DNSCache # noqa: PLC0415
|
|
1215
1328
|
|
|
1216
1329
|
with (
|
|
1217
|
-
patch("homeassistant.components.zeroconf.HaZeroconf"
|
|
1218
|
-
patch(
|
|
1330
|
+
patch("homeassistant.components.zeroconf.HaZeroconf") as mock_zc,
|
|
1331
|
+
patch(
|
|
1332
|
+
"homeassistant.components.zeroconf.discovery.AsyncServiceBrowser",
|
|
1333
|
+
) as mock_browser,
|
|
1219
1334
|
):
|
|
1335
|
+
asb = mock_browser.return_value
|
|
1336
|
+
asb.async_cancel = AsyncMock()
|
|
1220
1337
|
zc = mock_zc.return_value
|
|
1221
1338
|
# DNSCache has strong Cython type checks, and MagicMock does not work
|
|
1222
1339
|
# so we must mock the class directly
|
|
@@ -1227,10 +1344,8 @@ def mock_zeroconf() -> Generator[MagicMock]:
|
|
|
1227
1344
|
@pytest.fixture
|
|
1228
1345
|
def mock_async_zeroconf(mock_zeroconf: MagicMock) -> Generator[MagicMock]:
|
|
1229
1346
|
"""Mock AsyncZeroconf."""
|
|
1230
|
-
from zeroconf import DNSCache, Zeroconf #
|
|
1231
|
-
from zeroconf.asyncio import
|
|
1232
|
-
AsyncZeroconf,
|
|
1233
|
-
)
|
|
1347
|
+
from zeroconf import DNSCache, Zeroconf # noqa: PLC0415
|
|
1348
|
+
from zeroconf.asyncio import AsyncZeroconf # noqa: PLC0415
|
|
1234
1349
|
|
|
1235
1350
|
with patch(
|
|
1236
1351
|
"homeassistant.components.zeroconf.HaAsyncZeroconf", spec=AsyncZeroconf
|
|
@@ -1297,11 +1412,21 @@ def enable_nightly_purge() -> bool:
|
|
|
1297
1412
|
|
|
1298
1413
|
|
|
1299
1414
|
@pytest.fixture
|
|
1300
|
-
def
|
|
1415
|
+
def enable_migrate_event_context_ids() -> bool:
|
|
1416
|
+
"""Fixture to control enabling of recorder's context id migration.
|
|
1417
|
+
|
|
1418
|
+
To enable context id migration, tests can be marked with:
|
|
1419
|
+
@pytest.mark.parametrize("enable_migrate_event_context_ids", [True])
|
|
1420
|
+
"""
|
|
1421
|
+
return False
|
|
1422
|
+
|
|
1423
|
+
|
|
1424
|
+
@pytest.fixture
|
|
1425
|
+
def enable_migrate_state_context_ids() -> bool:
|
|
1301
1426
|
"""Fixture to control enabling of recorder's context id migration.
|
|
1302
1427
|
|
|
1303
1428
|
To enable context id migration, tests can be marked with:
|
|
1304
|
-
@pytest.mark.parametrize("
|
|
1429
|
+
@pytest.mark.parametrize("enable_migrate_state_context_ids", [True])
|
|
1305
1430
|
"""
|
|
1306
1431
|
return False
|
|
1307
1432
|
|
|
@@ -1372,47 +1497,58 @@ def recorder_db_url(
|
|
|
1372
1497
|
assert not hass_fixture_setup
|
|
1373
1498
|
|
|
1374
1499
|
db_url = cast(str, pytestconfig.getoption("dburl"))
|
|
1500
|
+
drop_existing_db = pytestconfig.getoption("drop_existing_db")
|
|
1501
|
+
|
|
1502
|
+
def drop_db() -> None:
|
|
1503
|
+
import sqlalchemy as sa # noqa: PLC0415
|
|
1504
|
+
import sqlalchemy_utils # noqa: PLC0415
|
|
1505
|
+
|
|
1506
|
+
if db_url.startswith("mysql://"):
|
|
1507
|
+
made_url = sa.make_url(db_url)
|
|
1508
|
+
db = made_url.database
|
|
1509
|
+
engine = sa.create_engine(db_url)
|
|
1510
|
+
# Check for any open connections to the database before dropping it
|
|
1511
|
+
# to ensure that InnoDB does not deadlock.
|
|
1512
|
+
with engine.begin() as connection:
|
|
1513
|
+
query = sa.text(
|
|
1514
|
+
"select id FROM information_schema.processlist WHERE db=:db and id != CONNECTION_ID()"
|
|
1515
|
+
)
|
|
1516
|
+
rows = connection.execute(query, parameters={"db": db}).fetchall()
|
|
1517
|
+
if rows:
|
|
1518
|
+
raise RuntimeError(
|
|
1519
|
+
f"Unable to drop database {db} because it is in use by {rows}"
|
|
1520
|
+
)
|
|
1521
|
+
engine.dispose()
|
|
1522
|
+
sqlalchemy_utils.drop_database(db_url)
|
|
1523
|
+
elif db_url.startswith("postgresql://"):
|
|
1524
|
+
sqlalchemy_utils.drop_database(db_url)
|
|
1525
|
+
|
|
1375
1526
|
if db_url == "sqlite://" and persistent_database:
|
|
1376
1527
|
tmp_path = tmp_path_factory.mktemp("recorder")
|
|
1377
1528
|
db_url = "sqlite:///" + str(tmp_path / "pytest.db")
|
|
1378
|
-
elif db_url.startswith("mysql://"):
|
|
1379
|
-
#
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
sqlalchemy_utils.
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
sqlalchemy_utils.create_database(
|
|
1529
|
+
elif db_url.startswith(("mysql://", "postgresql://")):
|
|
1530
|
+
import sqlalchemy_utils # noqa: PLC0415
|
|
1531
|
+
|
|
1532
|
+
if drop_existing_db and sqlalchemy_utils.database_exists(db_url):
|
|
1533
|
+
drop_db()
|
|
1534
|
+
|
|
1535
|
+
if sqlalchemy_utils.database_exists(db_url):
|
|
1536
|
+
raise RuntimeError(
|
|
1537
|
+
f"Database {db_url} already exists. Use --drop-existing-db "
|
|
1538
|
+
"to automatically drop existing database before start of test."
|
|
1539
|
+
)
|
|
1540
|
+
|
|
1541
|
+
sqlalchemy_utils.create_database(
|
|
1542
|
+
db_url,
|
|
1543
|
+
encoding="utf8mb4' COLLATE = 'utf8mb4_unicode_ci"
|
|
1544
|
+
if db_url.startswith("mysql://")
|
|
1545
|
+
else "utf8",
|
|
1546
|
+
)
|
|
1391
1547
|
yield db_url
|
|
1392
1548
|
if db_url == "sqlite://" and persistent_database:
|
|
1393
1549
|
rmtree(tmp_path, ignore_errors=True)
|
|
1394
|
-
elif db_url.startswith("mysql://"):
|
|
1395
|
-
|
|
1396
|
-
import sqlalchemy as sa
|
|
1397
|
-
|
|
1398
|
-
made_url = sa.make_url(db_url)
|
|
1399
|
-
db = made_url.database
|
|
1400
|
-
engine = sa.create_engine(db_url)
|
|
1401
|
-
# Check for any open connections to the database before dropping it
|
|
1402
|
-
# to ensure that InnoDB does not deadlock.
|
|
1403
|
-
with engine.begin() as connection:
|
|
1404
|
-
query = sa.text(
|
|
1405
|
-
"select id FROM information_schema.processlist WHERE db=:db and id != CONNECTION_ID()"
|
|
1406
|
-
)
|
|
1407
|
-
rows = connection.execute(query, parameters={"db": db}).fetchall()
|
|
1408
|
-
if rows:
|
|
1409
|
-
raise RuntimeError(
|
|
1410
|
-
f"Unable to drop database {db} because it is in use by {rows}"
|
|
1411
|
-
)
|
|
1412
|
-
engine.dispose()
|
|
1413
|
-
sqlalchemy_utils.drop_database(db_url)
|
|
1414
|
-
elif db_url.startswith("postgresql://"):
|
|
1415
|
-
sqlalchemy_utils.drop_database(db_url)
|
|
1550
|
+
elif db_url.startswith(("mysql://", "postgresql://")):
|
|
1551
|
+
drop_db()
|
|
1416
1552
|
|
|
1417
1553
|
|
|
1418
1554
|
async def _async_init_recorder_component(
|
|
@@ -1424,8 +1560,7 @@ async def _async_init_recorder_component(
|
|
|
1424
1560
|
wait_setup: bool,
|
|
1425
1561
|
) -> None:
|
|
1426
1562
|
"""Initialize the recorder asynchronously."""
|
|
1427
|
-
#
|
|
1428
|
-
from homeassistant.components import recorder
|
|
1563
|
+
from homeassistant.components import recorder # noqa: PLC0415
|
|
1429
1564
|
|
|
1430
1565
|
config = dict(add_config) if add_config else {}
|
|
1431
1566
|
if recorder.CONF_DB_URL not in config:
|
|
@@ -1446,7 +1581,7 @@ async def _async_init_recorder_component(
|
|
|
1446
1581
|
assert (recorder.DOMAIN in hass.config.components) == expected_setup_result
|
|
1447
1582
|
else:
|
|
1448
1583
|
# Wait for recorder to connect to the database
|
|
1449
|
-
await recorder_helper.
|
|
1584
|
+
await hass.data[recorder_helper.DATA_RECORDER].db_connected
|
|
1450
1585
|
_LOGGER.info(
|
|
1451
1586
|
"Test recorder successfully started, database location: %s",
|
|
1452
1587
|
config[recorder.CONF_DB_URL],
|
|
@@ -1469,27 +1604,23 @@ async def async_test_recorder(
|
|
|
1469
1604
|
enable_statistics: bool,
|
|
1470
1605
|
enable_missing_statistics: bool,
|
|
1471
1606
|
enable_schema_validation: bool,
|
|
1472
|
-
|
|
1607
|
+
enable_migrate_event_context_ids: bool,
|
|
1608
|
+
enable_migrate_state_context_ids: bool,
|
|
1473
1609
|
enable_migrate_event_type_ids: bool,
|
|
1474
1610
|
enable_migrate_entity_ids: bool,
|
|
1475
1611
|
enable_migrate_event_ids: bool,
|
|
1476
|
-
) -> AsyncGenerator[
|
|
1612
|
+
) -> AsyncGenerator[RecorderInstanceContextManager]:
|
|
1477
1613
|
"""Yield context manager to setup recorder instance."""
|
|
1478
|
-
#
|
|
1479
|
-
from homeassistant.components import
|
|
1614
|
+
from homeassistant.components import recorder # noqa: PLC0415
|
|
1615
|
+
from homeassistant.components.recorder import migration # noqa: PLC0415
|
|
1480
1616
|
|
|
1481
|
-
#
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
#
|
|
1485
|
-
from .components.recorder.common import async_recorder_block_till_done
|
|
1486
|
-
|
|
1487
|
-
# pylint: disable-next=import-outside-toplevel
|
|
1488
|
-
from .patch_recorder import real_session_scope
|
|
1617
|
+
from .components.recorder.common import ( # noqa: PLC0415
|
|
1618
|
+
async_recorder_block_till_done,
|
|
1619
|
+
)
|
|
1620
|
+
from .patch_recorder import real_session_scope # noqa: PLC0415
|
|
1489
1621
|
|
|
1490
1622
|
if TYPE_CHECKING:
|
|
1491
|
-
#
|
|
1492
|
-
from sqlalchemy.orm.session import Session
|
|
1623
|
+
from sqlalchemy.orm.session import Session # noqa: PLC0415
|
|
1493
1624
|
|
|
1494
1625
|
@contextmanager
|
|
1495
1626
|
def debug_session_scope(
|
|
@@ -1531,12 +1662,12 @@ async def async_test_recorder(
|
|
|
1531
1662
|
)
|
|
1532
1663
|
migrate_states_context_ids = (
|
|
1533
1664
|
migration.StatesContextIDMigration.migrate_data
|
|
1534
|
-
if
|
|
1665
|
+
if enable_migrate_state_context_ids
|
|
1535
1666
|
else None
|
|
1536
1667
|
)
|
|
1537
1668
|
migrate_events_context_ids = (
|
|
1538
1669
|
migration.EventsContextIDMigration.migrate_data
|
|
1539
|
-
if
|
|
1670
|
+
if enable_migrate_event_context_ids
|
|
1540
1671
|
else None
|
|
1541
1672
|
)
|
|
1542
1673
|
migrate_event_type_ids = (
|
|
@@ -1547,10 +1678,12 @@ async def async_test_recorder(
|
|
|
1547
1678
|
migrate_entity_ids = (
|
|
1548
1679
|
migration.EntityIDMigration.migrate_data if enable_migrate_entity_ids else None
|
|
1549
1680
|
)
|
|
1550
|
-
|
|
1551
|
-
migration.EventIDPostMigration.
|
|
1681
|
+
post_migrate_event_ids = (
|
|
1682
|
+
migration.EventIDPostMigration.needs_migrate_impl
|
|
1552
1683
|
if enable_migrate_event_ids
|
|
1553
|
-
else lambda
|
|
1684
|
+
else lambda _1, _2, _3: migration.DataMigrationStatus(
|
|
1685
|
+
needs_migrate=False, migration_done=True
|
|
1686
|
+
)
|
|
1554
1687
|
)
|
|
1555
1688
|
with (
|
|
1556
1689
|
patch(
|
|
@@ -1589,8 +1722,8 @@ async def async_test_recorder(
|
|
|
1589
1722
|
autospec=True,
|
|
1590
1723
|
),
|
|
1591
1724
|
patch(
|
|
1592
|
-
"homeassistant.components.recorder.migration.EventIDPostMigration.
|
|
1593
|
-
side_effect=
|
|
1725
|
+
"homeassistant.components.recorder.migration.EventIDPostMigration.needs_migrate_impl",
|
|
1726
|
+
side_effect=post_migrate_event_ids,
|
|
1594
1727
|
autospec=True,
|
|
1595
1728
|
),
|
|
1596
1729
|
patch(
|
|
@@ -1615,7 +1748,7 @@ async def async_test_recorder(
|
|
|
1615
1748
|
wait_recorder: bool = True,
|
|
1616
1749
|
wait_recorder_setup: bool = True,
|
|
1617
1750
|
) -> AsyncGenerator[recorder.Recorder]:
|
|
1618
|
-
"""Setup and return recorder instance."""
|
|
1751
|
+
"""Setup and return recorder instance."""
|
|
1619
1752
|
await _async_init_recorder_component(
|
|
1620
1753
|
hass,
|
|
1621
1754
|
config,
|
|
@@ -1639,7 +1772,7 @@ async def async_test_recorder(
|
|
|
1639
1772
|
|
|
1640
1773
|
@pytest.fixture
|
|
1641
1774
|
async def async_setup_recorder_instance(
|
|
1642
|
-
async_test_recorder:
|
|
1775
|
+
async_test_recorder: RecorderInstanceContextManager,
|
|
1643
1776
|
) -> AsyncGenerator[RecorderInstanceGenerator]:
|
|
1644
1777
|
"""Yield callable to setup recorder instance."""
|
|
1645
1778
|
|
|
@@ -1652,7 +1785,7 @@ async def async_setup_recorder_instance(
|
|
|
1652
1785
|
expected_setup_result: bool = True,
|
|
1653
1786
|
wait_recorder: bool = True,
|
|
1654
1787
|
wait_recorder_setup: bool = True,
|
|
1655
|
-
) ->
|
|
1788
|
+
) -> recorder.Recorder:
|
|
1656
1789
|
"""Set up and return recorder instance."""
|
|
1657
1790
|
|
|
1658
1791
|
return await stack.enter_async_context(
|
|
@@ -1671,7 +1804,7 @@ async def async_setup_recorder_instance(
|
|
|
1671
1804
|
@pytest.fixture
|
|
1672
1805
|
async def recorder_mock(
|
|
1673
1806
|
recorder_config: dict[str, Any] | None,
|
|
1674
|
-
async_test_recorder:
|
|
1807
|
+
async_test_recorder: RecorderInstanceContextManager,
|
|
1675
1808
|
hass: HomeAssistant,
|
|
1676
1809
|
) -> AsyncGenerator[recorder.Recorder]:
|
|
1677
1810
|
"""Fixture with in-memory recorder."""
|
|
@@ -1704,10 +1837,11 @@ async def mock_enable_bluetooth(
|
|
|
1704
1837
|
await hass.async_block_till_done()
|
|
1705
1838
|
|
|
1706
1839
|
|
|
1707
|
-
@pytest.fixture(scope="session")
|
|
1840
|
+
@pytest.fixture(autouse=True, scope="session")
|
|
1708
1841
|
def mock_bluetooth_adapters() -> Generator[None]:
|
|
1709
1842
|
"""Fixture to mock bluetooth adapters."""
|
|
1710
1843
|
with (
|
|
1844
|
+
patch("habluetooth.util.recover_adapter"),
|
|
1711
1845
|
patch("bluetooth_auto_recovery.recover_adapter"),
|
|
1712
1846
|
patch("bluetooth_adapters.systems.platform.system", return_value="Linux"),
|
|
1713
1847
|
patch("bluetooth_adapters.systems.linux.LinuxAdapters.refresh"),
|
|
@@ -1736,33 +1870,122 @@ def mock_bleak_scanner_start() -> Generator[MagicMock]:
|
|
|
1736
1870
|
|
|
1737
1871
|
# Late imports to avoid loading bleak unless we need it
|
|
1738
1872
|
|
|
1739
|
-
#
|
|
1740
|
-
|
|
1873
|
+
from habluetooth import ( # noqa: PLC0415
|
|
1874
|
+
manager as bluetooth_manager,
|
|
1875
|
+
scanner as bluetooth_scanner,
|
|
1876
|
+
)
|
|
1741
1877
|
|
|
1742
1878
|
# We need to drop the stop method from the object since we patched
|
|
1743
1879
|
# out start and this fixture will expire before the stop method is called
|
|
1744
1880
|
# when EVENT_HOMEASSISTANT_STOP is fired.
|
|
1745
1881
|
# pylint: disable-next=c-extension-no-member
|
|
1746
1882
|
bluetooth_scanner.OriginalBleakScanner.stop = AsyncMock() # type: ignore[assignment]
|
|
1883
|
+
|
|
1884
|
+
# Mock BlueZ management controller to successfully setup
|
|
1885
|
+
# This prevents the manager from operating in degraded mode
|
|
1886
|
+
mock_mgmt_bluetooth_ctl = Mock()
|
|
1887
|
+
mock_mgmt_bluetooth_ctl.setup = AsyncMock(return_value=None)
|
|
1888
|
+
|
|
1747
1889
|
with (
|
|
1748
1890
|
patch.object(
|
|
1749
1891
|
bluetooth_scanner.OriginalBleakScanner, # pylint: disable=c-extension-no-member
|
|
1750
1892
|
"start",
|
|
1751
1893
|
) as mock_bleak_scanner_start,
|
|
1752
1894
|
patch.object(bluetooth_scanner, "HaScanner"),
|
|
1895
|
+
patch.object(
|
|
1896
|
+
bluetooth_manager, "MGMTBluetoothCtl", return_value=mock_mgmt_bluetooth_ctl
|
|
1897
|
+
),
|
|
1753
1898
|
):
|
|
1754
1899
|
yield mock_bleak_scanner_start
|
|
1755
1900
|
|
|
1756
1901
|
|
|
1757
1902
|
@pytest.fixture
|
|
1758
|
-
def
|
|
1759
|
-
"""
|
|
1903
|
+
def hassio_env(supervisor_is_connected: AsyncMock) -> Generator[None]:
|
|
1904
|
+
"""Fixture to inject hassio env."""
|
|
1905
|
+
from homeassistant.components.hassio import HassioAPIError # noqa: PLC0415
|
|
1906
|
+
|
|
1907
|
+
from .components.hassio import SUPERVISOR_TOKEN # noqa: PLC0415
|
|
1908
|
+
|
|
1909
|
+
with (
|
|
1910
|
+
patch.dict(os.environ, {"SUPERVISOR": "127.0.0.1"}),
|
|
1911
|
+
patch.dict(os.environ, {"SUPERVISOR_TOKEN": SUPERVISOR_TOKEN}),
|
|
1912
|
+
patch(
|
|
1913
|
+
"homeassistant.components.hassio.HassIO.get_info",
|
|
1914
|
+
Mock(side_effect=HassioAPIError()),
|
|
1915
|
+
),
|
|
1916
|
+
):
|
|
1917
|
+
yield
|
|
1918
|
+
|
|
1919
|
+
|
|
1920
|
+
@pytest.fixture
|
|
1921
|
+
async def hassio_stubs(
|
|
1922
|
+
hassio_env: None,
|
|
1923
|
+
hass: HomeAssistant,
|
|
1924
|
+
hass_client: ClientSessionGenerator,
|
|
1925
|
+
aioclient_mock: AiohttpClientMocker,
|
|
1926
|
+
supervisor_client: AsyncMock,
|
|
1927
|
+
) -> RefreshToken:
|
|
1928
|
+
"""Create mock hassio http client."""
|
|
1929
|
+
from homeassistant.components.hassio import HassioAPIError # noqa: PLC0415
|
|
1930
|
+
|
|
1931
|
+
with (
|
|
1932
|
+
patch(
|
|
1933
|
+
"homeassistant.components.hassio.HassIO.update_hass_api",
|
|
1934
|
+
return_value={"result": "ok"},
|
|
1935
|
+
) as hass_api,
|
|
1936
|
+
patch(
|
|
1937
|
+
"homeassistant.components.hassio.HassIO.update_hass_config",
|
|
1938
|
+
return_value={"result": "ok"},
|
|
1939
|
+
),
|
|
1940
|
+
patch(
|
|
1941
|
+
"homeassistant.components.hassio.HassIO.get_info",
|
|
1942
|
+
side_effect=HassioAPIError(),
|
|
1943
|
+
),
|
|
1944
|
+
patch(
|
|
1945
|
+
"homeassistant.components.hassio.HassIO.get_ingress_panels",
|
|
1946
|
+
return_value={"panels": []},
|
|
1947
|
+
),
|
|
1948
|
+
patch(
|
|
1949
|
+
"homeassistant.components.hassio.issues.SupervisorIssues.setup",
|
|
1950
|
+
),
|
|
1951
|
+
):
|
|
1952
|
+
await async_setup_component(hass, "hassio", {})
|
|
1953
|
+
|
|
1954
|
+
return hass_api.call_args[0][1]
|
|
1955
|
+
|
|
1956
|
+
|
|
1957
|
+
@pytest.fixture
|
|
1958
|
+
def integration_frame_path() -> str:
|
|
1959
|
+
"""Return the path to the integration frame.
|
|
1960
|
+
|
|
1961
|
+
Can be parametrized with
|
|
1962
|
+
`@pytest.mark.parametrize("integration_frame_path", ["path_to_frame"])`
|
|
1963
|
+
|
|
1964
|
+
- "custom_components/XYZ" for a custom integration
|
|
1965
|
+
- "homeassistant/components/XYZ" for a core integration
|
|
1966
|
+
- "homeassistant/XYZ" for core (no integration)
|
|
1967
|
+
|
|
1968
|
+
Defaults to core component `hue`
|
|
1969
|
+
"""
|
|
1970
|
+
return "homeassistant/components/hue"
|
|
1971
|
+
|
|
1972
|
+
|
|
1973
|
+
@pytest.fixture
|
|
1974
|
+
def mock_integration_frame(integration_frame_path: str) -> Generator[Mock]:
|
|
1975
|
+
"""Mock where we are calling code from.
|
|
1976
|
+
|
|
1977
|
+
Defaults to calling from `hue` core integration, and can be parametrized
|
|
1978
|
+
with `integration_frame_path`.
|
|
1979
|
+
"""
|
|
1980
|
+
correct_filename = f"/home/paulus/{integration_frame_path}/light.py"
|
|
1981
|
+
correct_module_name = f"{integration_frame_path.replace('/', '.')}.light"
|
|
1760
1982
|
correct_frame = Mock(
|
|
1761
|
-
filename="/home/paulus/
|
|
1983
|
+
filename=f"/home/paulus/{integration_frame_path}/light.py",
|
|
1762
1984
|
lineno="23",
|
|
1763
1985
|
line="self.light.is_on",
|
|
1764
1986
|
)
|
|
1765
1987
|
with (
|
|
1988
|
+
patch.dict(sys.modules, {correct_module_name: Mock(__file__=correct_filename)}),
|
|
1766
1989
|
patch(
|
|
1767
1990
|
"homeassistant.helpers.frame.linecache.getline",
|
|
1768
1991
|
return_value=correct_frame.line,
|
|
@@ -1856,7 +2079,7 @@ def service_calls(hass: HomeAssistant) -> Generator[list[ServiceCall]]:
|
|
|
1856
2079
|
return_response: bool = False,
|
|
1857
2080
|
) -> ServiceResponse:
|
|
1858
2081
|
calls.append(
|
|
1859
|
-
ServiceCall(domain, service, service_data, context, return_response)
|
|
2082
|
+
ServiceCall(hass, domain, service, service_data, context, return_response)
|
|
1860
2083
|
)
|
|
1861
2084
|
try:
|
|
1862
2085
|
return await _original_async_call(
|
|
@@ -1892,3 +2115,32 @@ def disable_block_async_io() -> Generator[None]:
|
|
|
1892
2115
|
blocking_call.object, blocking_call.function, blocking_call.original_func
|
|
1893
2116
|
)
|
|
1894
2117
|
calls.clear()
|
|
2118
|
+
|
|
2119
|
+
|
|
2120
|
+
# Ensure that incorrectly formatted mac addresses are rejected during
|
|
2121
|
+
# DhcpServiceInfo initialisation
|
|
2122
|
+
_real_dhcp_service_info_init = DhcpServiceInfo.__init__
|
|
2123
|
+
|
|
2124
|
+
|
|
2125
|
+
def _dhcp_service_info_init(self: DhcpServiceInfo, *args: Any, **kwargs: Any) -> None:
|
|
2126
|
+
"""Override __init__ for DhcpServiceInfo.
|
|
2127
|
+
|
|
2128
|
+
Ensure that the macaddress is always in lowercase and without colons to match DHCP service.
|
|
2129
|
+
"""
|
|
2130
|
+
_real_dhcp_service_info_init(self, *args, **kwargs)
|
|
2131
|
+
if self.macaddress != self.macaddress.lower().replace(":", ""):
|
|
2132
|
+
raise ValueError("macaddress is not correctly formatted")
|
|
2133
|
+
|
|
2134
|
+
|
|
2135
|
+
DhcpServiceInfo.__init__ = _dhcp_service_info_init
|
|
2136
|
+
|
|
2137
|
+
|
|
2138
|
+
@pytest.fixture(autouse=True)
|
|
2139
|
+
def disable_http_server() -> Generator[None]:
|
|
2140
|
+
"""Disable automatic start of HTTP server during .
|
|
2141
|
+
|
|
2142
|
+
This prevents the HTTP server from starting in tests that setup
|
|
2143
|
+
integrations which depend on the HTTP component.
|
|
2144
|
+
"""
|
|
2145
|
+
with patch("homeassistant.components.http.start_http_server_and_save_config"):
|
|
2146
|
+
yield
|