From ea78c09e678ee138e0eac91ea17810aaa63c4c45 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Mon, 21 Nov 2022 10:44:09 +0100 Subject: [PATCH 01/10] Upgrade pytest-aiohttp --- homeassistant/package_constraints.txt | 3 --- pyproject.toml | 1 + requirements_test.txt | 3 ++- script/gen_requirements_all.py | 3 --- 4 files changed, 3 insertions(+), 7 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 532bd23428cf41..dbd749d7ca72cc 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -98,9 +98,6 @@ hyperframe>=5.2.0 # Ensure we run compatible with musllinux build env numpy==1.23.2 -# pytest_asyncio breaks our test suite. We rely on pytest-aiohttp instead -pytest_asyncio==1000000000.0.0 - # Prevent dependency conflicts between sisyphus-control and aioambient # until upper bounds for sisyphus-control have been updated # https://github.com/jkeljo/sisyphus-control/issues/6 diff --git a/pyproject.toml b/pyproject.toml index a5b6c205c8927b..710b025bee3c85 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -227,3 +227,4 @@ norecursedirs = [ ] log_format = "%(asctime)s.%(msecs)03d %(levelname)-8s %(threadName)s %(name)s:%(filename)s:%(lineno)s %(message)s" log_date_format = "%Y-%m-%d %H:%M:%S" +asyncio_mode = "auto" diff --git a/requirements_test.txt b/requirements_test.txt index 38ea4aa37ab134..7db57fb5bcfc57 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -16,7 +16,8 @@ mypy==0.991 pre-commit==2.20.0 pylint==2.15.6 pipdeptree==2.3.1 -pytest-aiohttp==0.3.0 +pytest-asyncio==0.20.2 +pytest-aiohttp==1.0.4 pytest-cov==3.0.0 pytest-freezegun==0.4.2 pytest-socket==0.5.1 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 6d547ab9fed383..355caa6cb62519 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -110,9 +110,6 @@ # Ensure we run compatible with musllinux build env numpy==1.23.2 -# pytest_asyncio breaks our test suite. We rely on pytest-aiohttp instead -pytest_asyncio==1000000000.0.0 - # Prevent dependency conflicts between sisyphus-control and aioambient # until upper bounds for sisyphus-control have been updated # https://github.com/jkeljo/sisyphus-control/issues/6 From c47a210f56ad0d3b8a32b17071aeeff8ac8ff593 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Mon, 21 Nov 2022 18:42:12 +0100 Subject: [PATCH 02/10] Make sure executors, tasks and timers are closed Some test will trigger warnings on garbage collect, these warnings spills over into next test. Some test trigger tasks that raise errors on shutdown, these spill over into next test. This is to mimic older pytest-aiohttp and it's behaviour on test cleanup. Discussions on similar changes for pytest-aiohttp are here: https://github.com/pytest-dev/pytest-asyncio/pull/309 --- tests/conftest.py | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index b6638968182978..e337573a7cec94 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,6 +6,7 @@ from contextlib import asynccontextmanager import functools import itertools +import gc from json import JSONDecoder, loads import logging import sqlite3 @@ -13,6 +14,7 @@ import threading from typing import Any from unittest.mock import AsyncMock, MagicMock, Mock, patch +import warnings from aiohttp import client from aiohttp.pytest_plugin import AiohttpClient @@ -188,12 +190,14 @@ async def guard_func(*args, **kwargs): @pytest.fixture(autouse=True) -def verify_cleanup(): +def verify_cleanup(event_loop: asyncio.AbstractEventLoop): """Verify that the test has cleaned up resources correctly.""" threads_before = frozenset(threading.enumerate()) - + tasks_before = asyncio.all_tasks(event_loop) yield + event_loop.run_until_complete(event_loop.shutdown_default_executor()) + if len(INSTANCES) >= 2: count = len(INSTANCES) for inst in INSTANCES: @@ -204,6 +208,26 @@ def verify_cleanup(): for thread in threads: assert isinstance(thread, threading._DummyThread) + # Warn and clean-up lingering tasks and timers + # before moving on to the next test. + tasks = asyncio.all_tasks(event_loop) - tasks_before + for task in tasks: + warnings.warn(f"Linger task after test {task}") + task.cancel() + if tasks: + event_loop.run_until_complete(asyncio.wait(tasks)) + + for handle in event_loop._scheduled: # pylint: disable=protected-access + if not handle.cancelled(): + warnings.warn(f"Lingering timer after test {handle}") + handle.cancel() + + # Make sure garbage collect run in same test as allocation + # this is to mimic the behavior of pytest-aiohttp, and is + # required to avoid warnings from spilling over into next + # test case. + gc.collect() + @pytest.fixture(autouse=True) def bcrypt_cost(): @@ -382,7 +406,7 @@ def exc_handle(loop, context): @pytest.fixture -async def stop_hass(): +async def stop_hass(event_loop): """Make sure all hass are stopped.""" orig_hass = ha.HomeAssistant @@ -403,6 +427,7 @@ def mock_hass(): with patch.object(hass_inst.loop, "stop"): await hass_inst.async_block_till_done() await hass_inst.async_stop(force=True) + await event_loop.shutdown_default_executor() @pytest.fixture From 72b5fcab940dc170d7b58ec19a3a5621d9dc3996 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Mon, 21 Nov 2022 13:18:04 +0100 Subject: [PATCH 03/10] Replace loop with event_loop --- tests/auth/test_init.py | 2 +- tests/common.py | 2 +- tests/components/alexa/test_flash_briefings.py | 3 ++- tests/components/alexa/test_intent.py | 3 ++- tests/components/auth/conftest.py | 2 +- tests/components/config/test_init.py | 2 +- tests/components/emulated_hue/test_upnp.py | 2 +- tests/components/emulated_roku/test_binding.py | 2 +- tests/components/fido/test_sensor.py | 2 +- tests/components/frontend/test_init.py | 2 +- tests/components/geofency/test_init.py | 4 ++-- .../google_assistant/test_google_assistant.py | 7 +++++-- tests/components/gpslogger/test_init.py | 4 ++-- tests/components/homekit/conftest.py | 12 ++++++------ tests/components/http/conftest.py | 2 +- tests/components/http/test_cors.py | 4 ++-- tests/components/image_processing/test_init.py | 2 +- tests/components/locative/test_init.py | 2 +- tests/components/meraki/test_device_tracker.py | 3 ++- tests/components/motioneye/test_camera.py | 2 +- tests/components/nest/conftest.py | 2 +- tests/components/nest/test_api.py | 8 ++++++-- tests/components/recorder/test_util.py | 2 +- tests/components/rss_feed_template/test_init.py | 3 ++- tests/components/traccar/test_init.py | 4 ++-- tests/conftest.py | 6 ++++-- tests/scripts/test_auth.py | 2 +- tests/scripts/test_check_config.py | 12 ++++++------ tests/test_bootstrap.py | 14 +++++++------- tests/test_core.py | 16 ++++++++-------- tests/util/test_location.py | 2 +- 31 files changed, 74 insertions(+), 61 deletions(-) diff --git a/tests/auth/test_init.py b/tests/auth/test_init.py index 49c2c776684018..f8ba8108f59c62 100644 --- a/tests/auth/test_init.py +++ b/tests/auth/test_init.py @@ -28,7 +28,7 @@ @pytest.fixture -def mock_hass(loop): +def mock_hass(event_loop): """Home Assistant mock with minimum amount of data set to make it work with auth.""" hass = Mock() hass.config.skip_pip = True diff --git a/tests/common.py b/tests/common.py index b7a26cb5f6dcef..f32dbd11081d97 100644 --- a/tests/common.py +++ b/tests/common.py @@ -160,7 +160,7 @@ def stop_hass(): # pylint: disable=protected-access -async def async_test_home_assistant(loop, load_registries=True): +async def async_test_home_assistant(event_loop, load_registries=True): """Return a Home Assistant object pointing at test config dir.""" hass = ha.HomeAssistant() store = auth_store.AuthStore(hass) diff --git a/tests/components/alexa/test_flash_briefings.py b/tests/components/alexa/test_flash_briefings.py index 8b9c91e28b5b8c..499848f01db2cb 100644 --- a/tests/components/alexa/test_flash_briefings.py +++ b/tests/components/alexa/test_flash_briefings.py @@ -21,8 +21,9 @@ @pytest.fixture -def alexa_client(loop, hass, hass_client): +def alexa_client(event_loop, hass, hass_client): """Initialize a Home Assistant server for testing this module.""" + loop = event_loop @callback def mock_service(call): diff --git a/tests/components/alexa/test_intent.py b/tests/components/alexa/test_intent.py index 9c71bc32e4d3bd..54708e9d0f048c 100644 --- a/tests/components/alexa/test_intent.py +++ b/tests/components/alexa/test_intent.py @@ -27,8 +27,9 @@ @pytest.fixture -def alexa_client(loop, hass, hass_client): +def alexa_client(event_loop, hass, hass_client): """Initialize a Home Assistant server for testing this module.""" + loop = event_loop @callback def mock_service(call): diff --git a/tests/components/auth/conftest.py b/tests/components/auth/conftest.py index 867f44d9f15f74..b7e69a44a0d0ee 100644 --- a/tests/components/auth/conftest.py +++ b/tests/components/auth/conftest.py @@ -3,6 +3,6 @@ @pytest.fixture -def aiohttp_client(loop, aiohttp_client, socket_enabled): +def aiohttp_client(event_loop, aiohttp_client, socket_enabled): """Return aiohttp_client and allow opening sockets.""" return aiohttp_client diff --git a/tests/components/config/test_init.py b/tests/components/config/test_init.py index 2d5610cadfb6bf..9a54c55ff9edca 100644 --- a/tests/components/config/test_init.py +++ b/tests/components/config/test_init.py @@ -2,7 +2,7 @@ from homeassistant.setup import async_setup_component -async def test_config_setup(hass, loop): +async def test_config_setup(hass, event_loop): """Test it sets up hassbian.""" await async_setup_component(hass, "config", {}) assert "config" in hass.config.components diff --git a/tests/components/emulated_hue/test_upnp.py b/tests/components/emulated_hue/test_upnp.py index ce7f013963ca0e..770f636098cf10 100644 --- a/tests/components/emulated_hue/test_upnp.py +++ b/tests/components/emulated_hue/test_upnp.py @@ -31,7 +31,7 @@ def sendto(self, response, addr): @pytest.fixture -def aiohttp_client(loop, aiohttp_client, socket_enabled): +def aiohttp_client(event_loop, aiohttp_client, socket_enabled): """Return aiohttp_client and allow opening sockets.""" return aiohttp_client diff --git a/tests/components/emulated_roku/test_binding.py b/tests/components/emulated_roku/test_binding.py index 5afee6f6cc31c9..8adf1a18f19b43 100644 --- a/tests/components/emulated_roku/test_binding.py +++ b/tests/components/emulated_roku/test_binding.py @@ -25,7 +25,7 @@ async def test_events_fired_properly(hass): roku_event_handler = None def instantiate( - loop, + event_loop, handler, roku_usn, host_ip, diff --git a/tests/components/fido/test_sensor.py b/tests/components/fido/test_sensor.py index 0ab1de9411746e..13d8515f8f2517 100644 --- a/tests/components/fido/test_sensor.py +++ b/tests/components/fido/test_sensor.py @@ -38,7 +38,7 @@ async def fetch_data(self): raise PyFidoError("Fake Error") -async def test_fido_sensor(loop, hass): +async def test_fido_sensor(event_loop, hass): """Test the Fido number sensor.""" with patch("homeassistant.components.fido.sensor.FidoClient", new=FidoClientMock): config = { diff --git a/tests/components/frontend/test_init.py b/tests/components/frontend/test_init.py index 661b3ace38a413..6bff327d397347 100644 --- a/tests/components/frontend/test_init.py +++ b/tests/components/frontend/test_init.py @@ -80,7 +80,7 @@ async def frontend_themes(hass): @pytest.fixture -def aiohttp_client(loop, aiohttp_client, socket_enabled): +def aiohttp_client(event_loop, aiohttp_client, socket_enabled): """Return aiohttp_client and allow opening sockets.""" return aiohttp_client diff --git a/tests/components/geofency/test_init.py b/tests/components/geofency/test_init.py index 9959023e8a61de..9aaa356085b5d0 100644 --- a/tests/components/geofency/test_init.py +++ b/tests/components/geofency/test_init.py @@ -117,7 +117,7 @@ def mock_dev_track(mock_device_tracker_conf): @pytest.fixture -async def geofency_client(loop, hass, hass_client_no_auth): +async def geofency_client(event_loop, hass, hass_client_no_auth): """Geofency mock client (unauthenticated).""" assert await async_setup_component( @@ -130,7 +130,7 @@ async def geofency_client(loop, hass, hass_client_no_auth): @pytest.fixture(autouse=True) -async def setup_zones(loop, hass): +async def setup_zones(event_loop, hass): """Set up Zone config in HA.""" assert await async_setup_component( hass, diff --git a/tests/components/google_assistant/test_google_assistant.py b/tests/components/google_assistant/test_google_assistant.py index 3fd35846d7e733..97484f313418a1 100644 --- a/tests/components/google_assistant/test_google_assistant.py +++ b/tests/components/google_assistant/test_google_assistant.py @@ -42,8 +42,9 @@ def auth_header(hass_access_token): @pytest.fixture -def assistant_client(loop, hass, hass_client_no_auth): +def assistant_client(event_loop, hass, hass_client_no_auth): """Create web client for the Google Assistant API.""" + loop = event_loop loop.run_until_complete( setup.async_setup_component( hass, @@ -66,8 +67,10 @@ def assistant_client(loop, hass, hass_client_no_auth): @pytest.fixture -def hass_fixture(loop, hass): +def hass_fixture(event_loop, hass): """Set up a Home Assistant instance for these tests.""" + loop = event_loop + # We need to do this to get access to homeassistant/turn_(on,off) loop.run_until_complete(setup.async_setup_component(hass, core.DOMAIN, {})) diff --git a/tests/components/gpslogger/test_init.py b/tests/components/gpslogger/test_init.py index 4d246cff589222..77357065a1ac9e 100644 --- a/tests/components/gpslogger/test_init.py +++ b/tests/components/gpslogger/test_init.py @@ -26,7 +26,7 @@ def mock_dev_track(mock_device_tracker_conf): @pytest.fixture -async def gpslogger_client(loop, hass, hass_client_no_auth): +async def gpslogger_client(event_loop, hass, hass_client_no_auth): """Mock client for GPSLogger (unauthenticated).""" assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) @@ -38,7 +38,7 @@ async def gpslogger_client(loop, hass, hass_client_no_auth): @pytest.fixture(autouse=True) -async def setup_zones(loop, hass): +async def setup_zones(event_loop, hass): """Set up Zone config in HA.""" assert await async_setup_component( hass, diff --git a/tests/components/homekit/conftest.py b/tests/components/homekit/conftest.py index b0422a40f7286e..2800dfb9be71a1 100644 --- a/tests/components/homekit/conftest.py +++ b/tests/components/homekit/conftest.py @@ -21,7 +21,7 @@ def iid_storage(hass): @pytest.fixture() -def run_driver(hass, loop, iid_storage): +def run_driver(hass, event_loop, iid_storage): """Return a custom AccessoryDriver instance for HomeKit accessory init. This mock does not mock async_stop, so the driver will not be stopped @@ -41,12 +41,12 @@ def run_driver(hass, loop, iid_storage): bridge_name=BRIDGE_NAME, iid_storage=iid_storage, address="127.0.0.1", - loop=loop, + loop=event_loop, ) @pytest.fixture -def hk_driver(hass, loop, iid_storage): +def hk_driver(hass, event_loop, iid_storage): """Return a custom AccessoryDriver instance for HomeKit accessory init.""" with patch("pyhap.accessory_driver.AsyncZeroconf"), patch( "pyhap.accessory_driver.AccessoryEncoder" @@ -65,12 +65,12 @@ def hk_driver(hass, loop, iid_storage): bridge_name=BRIDGE_NAME, iid_storage=iid_storage, address="127.0.0.1", - loop=loop, + loop=event_loop, ) @pytest.fixture -def mock_hap(hass, loop, iid_storage, mock_zeroconf): +def mock_hap(hass, event_loop, iid_storage, mock_zeroconf): """Return a custom AccessoryDriver instance for HomeKit accessory init.""" with patch("pyhap.accessory_driver.AsyncZeroconf"), patch( "pyhap.accessory_driver.AccessoryEncoder" @@ -93,7 +93,7 @@ def mock_hap(hass, loop, iid_storage, mock_zeroconf): bridge_name=BRIDGE_NAME, iid_storage=iid_storage, address="127.0.0.1", - loop=loop, + loop=event_loop, ) diff --git a/tests/components/http/conftest.py b/tests/components/http/conftest.py index c796ec50b51f16..ed2c78bafd7a57 100644 --- a/tests/components/http/conftest.py +++ b/tests/components/http/conftest.py @@ -3,6 +3,6 @@ @pytest.fixture -def aiohttp_client(loop, aiohttp_client, socket_enabled): +def aiohttp_client(event_loop, aiohttp_client, socket_enabled): """Return aiohttp_client and allow opening sockets.""" return aiohttp_client diff --git a/tests/components/http/test_cors.py b/tests/components/http/test_cors.py index 599df194195c2c..8de081c840a688 100644 --- a/tests/components/http/test_cors.py +++ b/tests/components/http/test_cors.py @@ -49,12 +49,12 @@ async def mock_handler(request): @pytest.fixture -def client(loop, aiohttp_client): +def client(event_loop, aiohttp_client): """Fixture to set up a web.Application.""" app = web.Application() setup_cors(app, [TRUSTED_ORIGIN]) app["allow_configured_cors"](app.router.add_get("/", mock_handler)) - return loop.run_until_complete(aiohttp_client(app)) + return event_loop.run_until_complete(aiohttp_client(app)) async def test_cors_requests(client): diff --git a/tests/components/image_processing/test_init.py b/tests/components/image_processing/test_init.py index 9953df041ae7fd..37f84fb971f3cb 100644 --- a/tests/components/image_processing/test_init.py +++ b/tests/components/image_processing/test_init.py @@ -15,7 +15,7 @@ @pytest.fixture -def aiohttp_unused_port(loop, aiohttp_unused_port, socket_enabled): +def aiohttp_unused_port(event_loop, aiohttp_unused_port, socket_enabled): """Return aiohttp_unused_port and allow opening sockets.""" return aiohttp_unused_port diff --git a/tests/components/locative/test_init.py b/tests/components/locative/test_init.py index 5f608803e4ac3a..a0af04145b00b2 100644 --- a/tests/components/locative/test_init.py +++ b/tests/components/locative/test_init.py @@ -21,7 +21,7 @@ def mock_dev_track(mock_device_tracker_conf): @pytest.fixture -async def locative_client(loop, hass, hass_client): +async def locative_client(event_loop, hass, hass_client): """Locative mock client.""" assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) await hass.async_block_till_done() diff --git a/tests/components/meraki/test_device_tracker.py b/tests/components/meraki/test_device_tracker.py index 4726544410516c..e95241cbbc7789 100644 --- a/tests/components/meraki/test_device_tracker.py +++ b/tests/components/meraki/test_device_tracker.py @@ -15,8 +15,9 @@ @pytest.fixture -def meraki_client(loop, hass, hass_client): +def meraki_client(event_loop, hass, hass_client): """Meraki mock client.""" + loop = event_loop assert loop.run_until_complete( async_setup_component( hass, diff --git a/tests/components/motioneye/test_camera.py b/tests/components/motioneye/test_camera.py index 3e2db3bf897d2f..3d42e4c58269a0 100644 --- a/tests/components/motioneye/test_camera.py +++ b/tests/components/motioneye/test_camera.py @@ -62,7 +62,7 @@ @pytest.fixture -def aiohttp_server(loop, aiohttp_server, socket_enabled): +def aiohttp_server(event_loop, aiohttp_server, socket_enabled): """Return aiohttp_server and allow opening sockets.""" return aiohttp_server diff --git a/tests/components/nest/conftest.py b/tests/components/nest/conftest.py index 458685cde7021c..db89063553f35e 100644 --- a/tests/components/nest/conftest.py +++ b/tests/components/nest/conftest.py @@ -82,7 +82,7 @@ async def response_handler(self, request): @pytest.fixture -def aiohttp_client(loop, aiohttp_client, socket_enabled): +def aiohttp_client(event_loop, aiohttp_client, socket_enabled): """Return aiohttp_client and allow opening sockets.""" return aiohttp_client diff --git a/tests/components/nest/test_api.py b/tests/components/nest/test_api.py index 7d88ba1d329e24..a998a08d0c4c6f 100644 --- a/tests/components/nest/test_api.py +++ b/tests/components/nest/test_api.py @@ -53,7 +53,9 @@ async def test_auth(hass, aioclient_mock): # Prepare to capture credentials for Subscriber captured_creds = None - async def async_new_subscriber(creds, subscription_name, loop, async_callback): + async def async_new_subscriber( + creds, subscription_name, event_loop, async_callback + ): """Capture credentials for tests.""" nonlocal captured_creds captured_creds = creds @@ -112,7 +114,9 @@ async def test_auth_expired_token(hass, aioclient_mock): # Prepare to capture credentials for Subscriber captured_creds = None - async def async_new_subscriber(creds, subscription_name, loop, async_callback): + async def async_new_subscriber( + creds, subscription_name, event_loop, async_callback + ): """Capture credentials for tests.""" nonlocal captured_creds captured_creds = creds diff --git a/tests/components/recorder/test_util.py b/tests/components/recorder/test_util.py index 54f9bb9b9f26b8..ecdd729a163a09 100644 --- a/tests/components/recorder/test_util.py +++ b/tests/components/recorder/test_util.py @@ -110,7 +110,7 @@ def test_validate_or_move_away_sqlite_database(hass, tmpdir, caplog): async def test_last_run_was_recently_clean( - loop, async_setup_recorder_instance: SetupRecorderInstanceT, tmp_path + event_loop, async_setup_recorder_instance: SetupRecorderInstanceT, tmp_path ): """Test we can check if the last recorder run was recently clean.""" config = { diff --git a/tests/components/rss_feed_template/test_init.py b/tests/components/rss_feed_template/test_init.py index ffdb4e5ba9af80..0ed22ac84fa395 100644 --- a/tests/components/rss_feed_template/test_init.py +++ b/tests/components/rss_feed_template/test_init.py @@ -8,8 +8,9 @@ @pytest.fixture -def mock_http_client(loop, hass, hass_client): +def mock_http_client(event_loop, hass, hass_client): """Set up test fixture.""" + loop = event_loop config = { "rss_feed_template": { "testfeed": { diff --git a/tests/components/traccar/test_init.py b/tests/components/traccar/test_init.py index 830670efc114b1..4c6c235a3aa50f 100644 --- a/tests/components/traccar/test_init.py +++ b/tests/components/traccar/test_init.py @@ -24,7 +24,7 @@ def mock_dev_track(mock_device_tracker_conf): @pytest.fixture(name="client") -async def traccar_client(loop, hass, hass_client_no_auth): +async def traccar_client(event_loop, hass, hass_client_no_auth): """Mock client for Traccar (unauthenticated).""" assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) @@ -36,7 +36,7 @@ async def traccar_client(loop, hass, hass_client_no_auth): @pytest.fixture(autouse=True) -async def setup_zones(loop, hass): +async def setup_zones(event_loop, hass): """Set up Zone config in HA.""" assert await async_setup_component( hass, diff --git a/tests/conftest.py b/tests/conftest.py index e337573a7cec94..0d7858a0e3eafd 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -302,7 +302,7 @@ def aiohttp_client_cls(): @pytest.fixture def aiohttp_client( - loop: asyncio.AbstractEventLoop, + event_loop: asyncio.AbstractEventLoop, ) -> Generator[AiohttpClient, None, None]: """Override the default aiohttp_client since 3.x does not support aiohttp_client_cls. @@ -313,6 +313,7 @@ def aiohttp_client( aiohttp_client(server, **kwargs) aiohttp_client(raw_server, **kwargs) """ + loop = event_loop clients = [] async def go( @@ -359,9 +360,10 @@ def hass_fixture_setup(): @pytest.fixture -def hass(hass_fixture_setup, loop, load_registries, hass_storage, request): +def hass(hass_fixture_setup, event_loop, load_registries, hass_storage, request): """Fixture to provide a test instance of Home Assistant.""" + loop = event_loop hass_fixture_setup.append(True) orig_tz = dt_util.DEFAULT_TIME_ZONE diff --git a/tests/scripts/test_auth.py b/tests/scripts/test_auth.py index 3ab194508793e2..a5478d149ec0cc 100644 --- a/tests/scripts/test_auth.py +++ b/tests/scripts/test_auth.py @@ -108,7 +108,7 @@ async def test_change_password_invalid_user(hass, provider, capsys, hass_storage data.validate_login("invalid-user", "new-pass") -def test_parsing_args(loop): +def test_parsing_args(event_loop): """Test we parse args correctly.""" called = False diff --git a/tests/scripts/test_check_config.py b/tests/scripts/test_check_config.py index 05ebb8fb0e548d..fc35b28f7c9b39 100644 --- a/tests/scripts/test_check_config.py +++ b/tests/scripts/test_check_config.py @@ -43,7 +43,7 @@ def normalize_yaml_files(check_dict): return [key.replace(root, "...") for key in sorted(check_dict["yaml_files"].keys())] -def test_bad_core_config(mock_is_file, loop): +def test_bad_core_config(mock_is_file, event_loop): """Test a bad core config setup.""" files = {YAML_CONFIG_FILE: BAD_CORE_CONFIG} with patch_yaml_files(files): @@ -52,7 +52,7 @@ def test_bad_core_config(mock_is_file, loop): assert res["except"]["homeassistant"][1] == {"unit_system": "bad"} -def test_config_platform_valid(mock_is_file, loop): +def test_config_platform_valid(mock_is_file, event_loop): """Test a valid platform setup.""" files = {YAML_CONFIG_FILE: BASE_CONFIG + "light:\n platform: demo"} with patch_yaml_files(files): @@ -65,7 +65,7 @@ def test_config_platform_valid(mock_is_file, loop): assert len(res["yaml_files"]) == 1 -def test_component_platform_not_found(mock_is_file, loop): +def test_component_platform_not_found(mock_is_file, event_loop): """Test errors if component or platform not found.""" # Make sure they don't exist files = {YAML_CONFIG_FILE: BASE_CONFIG + "beer:"} @@ -96,7 +96,7 @@ def test_component_platform_not_found(mock_is_file, loop): assert len(res["yaml_files"]) == 1 -def test_secrets(mock_is_file, loop): +def test_secrets(mock_is_file, event_loop): """Test secrets config checking method.""" secrets_path = get_test_config_dir("secrets.yaml") @@ -127,7 +127,7 @@ def test_secrets(mock_is_file, loop): ] -def test_package_invalid(mock_is_file, loop): +def test_package_invalid(mock_is_file, event_loop): """Test an invalid package.""" files = { YAML_CONFIG_FILE: BASE_CONFIG + (" packages:\n p1:\n" ' group: ["a"]') @@ -145,7 +145,7 @@ def test_package_invalid(mock_is_file, loop): assert len(res["yaml_files"]) == 1 -def test_bootstrap_error(loop): +def test_bootstrap_error(event_loop): """Test a valid platform setup.""" files = {YAML_CONFIG_FILE: BASE_CONFIG + "automation: !include no.yaml"} with patch_yaml_files(files): diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index e51f4d315eefcf..c8365c86a9a72d 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -460,7 +460,7 @@ async def test_setup_hass( mock_ensure_config_exists, mock_process_ha_config_upgrade, caplog, - loop, + event_loop, ): """Test it works.""" verbose = Mock() @@ -511,7 +511,7 @@ async def test_setup_hass_takes_longer_than_log_slow_startup( mock_ensure_config_exists, mock_process_ha_config_upgrade, caplog, - loop, + event_loop, ): """Test it works.""" verbose = Mock() @@ -553,7 +553,7 @@ async def test_setup_hass_invalid_yaml( mock_mount_local_lib_path, mock_ensure_config_exists, mock_process_ha_config_upgrade, - loop, + event_loop, ): """Test it works.""" with patch( @@ -581,7 +581,7 @@ async def test_setup_hass_config_dir_nonexistent( mock_mount_local_lib_path, mock_ensure_config_exists, mock_process_ha_config_upgrade, - loop, + event_loop, ): """Test it works.""" mock_ensure_config_exists.return_value = False @@ -608,7 +608,7 @@ async def test_setup_hass_safe_mode( mock_mount_local_lib_path, mock_ensure_config_exists, mock_process_ha_config_upgrade, - loop, + event_loop, ): """Test it works.""" with patch("homeassistant.components.browser.setup") as browser_setup, patch( @@ -641,7 +641,7 @@ async def test_setup_hass_invalid_core_config( mock_mount_local_lib_path, mock_ensure_config_exists, mock_process_ha_config_upgrade, - loop, + event_loop, ): """Test it works.""" with patch( @@ -669,7 +669,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, + event_loop, ): """Test we setup safe mode if frontend didn't load.""" verbose = Mock() diff --git a/tests/test_core.py b/tests/test_core.py index 80f7c18d254ee8..c5f153ad3bd831 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -87,9 +87,9 @@ def test_async_add_hass_job_schedule_partial_callback(): assert len(hass.add_job.mock_calls) == 0 -def test_async_add_hass_job_schedule_coroutinefunction(loop): +def test_async_add_hass_job_schedule_coroutinefunction(event_loop): """Test that we schedule coroutines and add jobs to the job pool.""" - hass = MagicMock(loop=MagicMock(wraps=loop)) + hass = MagicMock(loop=MagicMock(wraps=event_loop)) async def job(): pass @@ -100,9 +100,9 @@ async def job(): assert len(hass.add_job.mock_calls) == 0 -def test_async_add_hass_job_schedule_partial_coroutinefunction(loop): +def test_async_add_hass_job_schedule_partial_coroutinefunction(event_loop): """Test that we schedule partial coros and add jobs to the job pool.""" - hass = MagicMock(loop=MagicMock(wraps=loop)) + hass = MagicMock(loop=MagicMock(wraps=event_loop)) async def job(): pass @@ -128,9 +128,9 @@ def job(): assert len(hass.loop.run_in_executor.mock_calls) == 1 -def test_async_create_task_schedule_coroutine(loop): +def test_async_create_task_schedule_coroutine(event_loop): """Test that we schedule coroutines and add jobs to the job pool.""" - hass = MagicMock(loop=MagicMock(wraps=loop)) + hass = MagicMock(loop=MagicMock(wraps=event_loop)) async def job(): pass @@ -1079,7 +1079,7 @@ async def test_bad_timezone_raises_value_error(hass): await hass.config.async_update(time_zone="not_a_timezone") -async def test_start_taking_too_long(loop, caplog): +async def test_start_taking_too_long(event_loop, caplog): """Test when async_start takes too long.""" hass = ha.HomeAssistant() caplog.set_level(logging.WARNING) @@ -1098,7 +1098,7 @@ async def test_start_taking_too_long(loop, caplog): assert hass.state == ha.CoreState.stopped -async def test_track_task_functions(loop): +async def test_track_task_functions(event_loop): """Test function to start/stop track task and initial state.""" hass = ha.HomeAssistant() try: diff --git a/tests/util/test_location.py b/tests/util/test_location.py index d8d86965733fa9..51cd8d4388fbd7 100644 --- a/tests/util/test_location.py +++ b/tests/util/test_location.py @@ -32,7 +32,7 @@ async def session(hass): @pytest.fixture -async def raising_session(loop): +async def raising_session(event_loop): """Return an aioclient session that only fails.""" return Mock(get=Mock(side_effect=aiohttp.ClientError)) From 194c8c5f9e8ed87e78ce1307004f2f2b985b2a6c Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Mon, 21 Nov 2022 18:30:30 +0100 Subject: [PATCH 04/10] Make sure time is frozen for tests --- tests/components/energy/test_sensor.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/components/energy/test_sensor.py b/tests/components/energy/test_sensor.py index 0108dd1de7693c..636eb74e4d3b83 100644 --- a/tests/components/energy/test_sensor.py +++ b/tests/components/energy/test_sensor.py @@ -3,6 +3,7 @@ from datetime import timedelta from unittest.mock import patch +from freezegun import freeze_time import pytest from homeassistant.components.energy import data @@ -41,6 +42,13 @@ async def setup_integration(hass): return setup_integration +@pytest.fixture(autouse=True) +@freeze_time("2022-04-19 07:53:05") +def frozen_time(): + """Freeze clock for tests.""" + yield + + def get_statistics_for_entity(statistics_results, entity_id): """Get statistics for a certain entity, or None if there is none.""" for statistics_result in statistics_results: From 71b283fcba1d4428d68819dd9f7231a9f3edcba1 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Wed, 23 Nov 2022 20:03:37 +0100 Subject: [PATCH 05/10] Make sure the ConditionType is not async /home-assistant/homeassistant/helpers/template.py:2082: RuntimeWarning: coroutine 'AsyncMockMixin._execute_mock_call' was never awaited def wrapper(*args, **kwargs): Enable tracemalloc to get traceback where the object was allocated. See https://docs.pytest.org/en/stable/how-to/capture-warnings.html#resource-warnings for more info. --- tests/helpers/test_script.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index 519498f2e08b09..a5f3cc0cc912a1 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -8,7 +8,7 @@ import operator from types import MappingProxyType from unittest import mock -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock, MagicMock, patch from async_timeout import timeout import pytest @@ -1734,6 +1734,7 @@ async def test_condition_created_once(async_from_config, hass): ) async_from_config.reset_mock() + async_from_config.return_value = MagicMock() hass.states.async_set("test.entity", "hello") await script_obj.async_run(context=Context()) From ef8df5a4d5a74d0888548adcd6b79c0088996bdb Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sat, 26 Nov 2022 22:24:59 +0100 Subject: [PATCH 06/10] Increase litejet press tests with a factor 10 The times are simulated anyway, and we can't stop the normal event from occuring. --- tests/components/litejet/test_trigger.py | 44 ++++++++++++------------ 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/tests/components/litejet/test_trigger.py b/tests/components/litejet/test_trigger.py index 152d11b0c23d7f..86713b51489e2b 100644 --- a/tests/components/litejet/test_trigger.py +++ b/tests/components/litejet/test_trigger.py @@ -113,12 +113,12 @@ async def test_held_more_than_short(hass, calls, mock_litejet): { "platform": "litejet", "number": ENTITY_OTHER_SWITCH_NUMBER, - "held_more_than": {"milliseconds": "200"}, + "held_more_than": {"milliseconds": "2000"}, }, ) await simulate_press(hass, mock_litejet, ENTITY_OTHER_SWITCH_NUMBER) - await simulate_time(hass, mock_litejet, timedelta(seconds=0.1)) + await simulate_time(hass, mock_litejet, timedelta(seconds=1)) await simulate_release(hass, mock_litejet, ENTITY_OTHER_SWITCH_NUMBER) assert len(calls) == 0 @@ -130,13 +130,13 @@ async def test_held_more_than_long(hass, calls, mock_litejet): { "platform": "litejet", "number": ENTITY_OTHER_SWITCH_NUMBER, - "held_more_than": {"milliseconds": "200"}, + "held_more_than": {"milliseconds": "2000"}, }, ) await simulate_press(hass, mock_litejet, ENTITY_OTHER_SWITCH_NUMBER) assert len(calls) == 0 - await simulate_time(hass, mock_litejet, timedelta(seconds=0.3)) + await simulate_time(hass, mock_litejet, timedelta(seconds=3)) assert len(calls) == 1 assert calls[0].data["id"] == 0 await simulate_release(hass, mock_litejet, ENTITY_OTHER_SWITCH_NUMBER) @@ -150,12 +150,12 @@ async def test_held_less_than_short(hass, calls, mock_litejet): { "platform": "litejet", "number": ENTITY_OTHER_SWITCH_NUMBER, - "held_less_than": {"milliseconds": "200"}, + "held_less_than": {"milliseconds": "2000"}, }, ) await simulate_press(hass, mock_litejet, ENTITY_OTHER_SWITCH_NUMBER) - await simulate_time(hass, mock_litejet, timedelta(seconds=0.1)) + await simulate_time(hass, mock_litejet, timedelta(seconds=1)) assert len(calls) == 0 await simulate_release(hass, mock_litejet, ENTITY_OTHER_SWITCH_NUMBER) assert len(calls) == 1 @@ -169,13 +169,13 @@ async def test_held_less_than_long(hass, calls, mock_litejet): { "platform": "litejet", "number": ENTITY_OTHER_SWITCH_NUMBER, - "held_less_than": {"milliseconds": "200"}, + "held_less_than": {"milliseconds": "2000"}, }, ) await simulate_press(hass, mock_litejet, ENTITY_OTHER_SWITCH_NUMBER) assert len(calls) == 0 - await simulate_time(hass, mock_litejet, timedelta(seconds=0.3)) + await simulate_time(hass, mock_litejet, timedelta(seconds=3)) assert len(calls) == 0 await simulate_release(hass, mock_litejet, ENTITY_OTHER_SWITCH_NUMBER) assert len(calls) == 0 @@ -188,13 +188,13 @@ async def test_held_in_range_short(hass, calls, mock_litejet): { "platform": "litejet", "number": ENTITY_OTHER_SWITCH_NUMBER, - "held_more_than": {"milliseconds": "100"}, - "held_less_than": {"milliseconds": "300"}, + "held_more_than": {"milliseconds": "1000"}, + "held_less_than": {"milliseconds": "3000"}, }, ) await simulate_press(hass, mock_litejet, ENTITY_OTHER_SWITCH_NUMBER) - await simulate_time(hass, mock_litejet, timedelta(seconds=0.05)) + await simulate_time(hass, mock_litejet, timedelta(seconds=0.5)) await simulate_release(hass, mock_litejet, ENTITY_OTHER_SWITCH_NUMBER) assert len(calls) == 0 @@ -206,14 +206,14 @@ async def test_held_in_range_just_right(hass, calls, mock_litejet): { "platform": "litejet", "number": ENTITY_OTHER_SWITCH_NUMBER, - "held_more_than": {"milliseconds": "100"}, - "held_less_than": {"milliseconds": "300"}, + "held_more_than": {"milliseconds": "1000"}, + "held_less_than": {"milliseconds": "3000"}, }, ) await simulate_press(hass, mock_litejet, ENTITY_OTHER_SWITCH_NUMBER) assert len(calls) == 0 - await simulate_time(hass, mock_litejet, timedelta(seconds=0.2)) + await simulate_time(hass, mock_litejet, timedelta(seconds=2)) assert len(calls) == 0 await simulate_release(hass, mock_litejet, ENTITY_OTHER_SWITCH_NUMBER) assert len(calls) == 1 @@ -227,14 +227,14 @@ async def test_held_in_range_long(hass, calls, mock_litejet): { "platform": "litejet", "number": ENTITY_OTHER_SWITCH_NUMBER, - "held_more_than": {"milliseconds": "100"}, - "held_less_than": {"milliseconds": "300"}, + "held_more_than": {"milliseconds": "1000"}, + "held_less_than": {"milliseconds": "3000"}, }, ) await simulate_press(hass, mock_litejet, ENTITY_OTHER_SWITCH_NUMBER) assert len(calls) == 0 - await simulate_time(hass, mock_litejet, timedelta(seconds=0.4)) + await simulate_time(hass, mock_litejet, timedelta(seconds=4)) assert len(calls) == 0 await simulate_release(hass, mock_litejet, ENTITY_OTHER_SWITCH_NUMBER) assert len(calls) == 0 @@ -247,8 +247,8 @@ async def test_reload(hass, calls, mock_litejet): { "platform": "litejet", "number": ENTITY_OTHER_SWITCH_NUMBER, - "held_more_than": {"milliseconds": "100"}, - "held_less_than": {"milliseconds": "300"}, + "held_more_than": {"milliseconds": "1000"}, + "held_less_than": {"milliseconds": "3000"}, }, ) @@ -260,7 +260,7 @@ async def test_reload(hass, calls, mock_litejet): "trigger": { "platform": "litejet", "number": ENTITY_OTHER_SWITCH_NUMBER, - "held_more_than": {"milliseconds": "1000"}, + "held_more_than": {"milliseconds": "10000"}, }, "action": {"service": "test.automation"}, } @@ -275,7 +275,7 @@ async def test_reload(hass, calls, mock_litejet): await simulate_press(hass, mock_litejet, ENTITY_OTHER_SWITCH_NUMBER) assert len(calls) == 0 - await simulate_time(hass, mock_litejet, timedelta(seconds=0.5)) + await simulate_time(hass, mock_litejet, timedelta(seconds=5)) assert len(calls) == 0 - await simulate_time(hass, mock_litejet, timedelta(seconds=1.25)) + await simulate_time(hass, mock_litejet, timedelta(seconds=12.5)) assert len(calls) == 1 From b162712b379b085287121644b7bbb67e1162ea2c Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sun, 27 Nov 2022 09:16:07 +0100 Subject: [PATCH 07/10] Use async handlers for aiohttp tests/components/motioneye/test_camera.py::test_get_still_image_from_camera tests/components/motioneye/test_camera.py::test_get_still_image_from_camera tests/components/motioneye/test_camera.py::test_get_stream_from_camera tests/components/motioneye/test_camera.py::test_get_stream_from_camera tests/components/motioneye/test_camera.py::test_camera_option_stream_url_template tests/components/motioneye/test_camera.py::test_camera_option_stream_url_template /Users/joakim/src/hass/home-assistant/venv/lib/python3.9/site-packages/aiohttp/web_urldispatcher.py:189: DeprecationWarning: Bare functions are deprecated, use async ones warnings.warn( --- tests/components/motioneye/test_camera.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/components/motioneye/test_camera.py b/tests/components/motioneye/test_camera.py index 3d42e4c58269a0..d70405cd297a63 100644 --- a/tests/components/motioneye/test_camera.py +++ b/tests/components/motioneye/test_camera.py @@ -218,7 +218,7 @@ async def test_get_still_image_from_camera( ) -> None: """Test getting a still image.""" - image_handler = Mock(return_value="") + image_handler = AsyncMock(return_value="") app = web.Application() app.add_routes( @@ -258,7 +258,7 @@ async def test_get_still_image_from_camera( async def test_get_stream_from_camera(aiohttp_server: Any, hass: HomeAssistant) -> None: """Test getting a stream.""" - stream_handler = Mock(return_value="") + stream_handler = AsyncMock(return_value="") app = web.Application() app.add_routes([web.get("/", stream_handler)]) stream_server = await aiohttp_server(app) @@ -341,7 +341,7 @@ async def test_camera_option_stream_url_template( """Verify camera with a stream URL template option.""" client = create_mock_motioneye_client() - stream_handler = Mock(return_value="") + stream_handler = AsyncMock(return_value="") app = web.Application() app.add_routes([web.get(f"/{TEST_CAMERA_NAME}/{TEST_CAMERA_ID}", stream_handler)]) stream_server = await aiohttp_server(app) @@ -371,7 +371,7 @@ async def test_camera_option_stream_url_template( # the expected exception, then verify the right handler was called. with pytest.raises(HTTPBadGateway): await async_get_mjpeg_stream(hass, Mock(), TEST_CAMERA_ENTITY_ID) - assert stream_handler.called + assert AsyncMock.called assert not client.get_camera_stream_url.called From 2a208d0f53e861a6434b100da9bd319a1fb24d48 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sun, 27 Nov 2022 10:33:05 +0100 Subject: [PATCH 08/10] Switch to freezegun in modbus tests The tests allowed clock to tick in between steps --- tests/components/modbus/test_init.py | 55 ++++++++++++---------------- 1 file changed, 24 insertions(+), 31 deletions(-) diff --git a/tests/components/modbus/test_init.py b/tests/components/modbus/test_init.py index 73b1f9743c201f..3b704eff161b4f 100644 --- a/tests/components/modbus/test_init.py +++ b/tests/components/modbus/test_init.py @@ -16,6 +16,7 @@ import logging from unittest import mock +from freezegun.api import FrozenDateTimeFactory from pymodbus.exceptions import ModbusException from pymodbus.pdu import ExceptionResponse, IllegalFunctionRequest import pytest @@ -542,6 +543,7 @@ async def mock_modbus_read_pymodbus_fixture( do_exception, caplog, mock_pymodbus, + freezer: FrozenDateTimeFactory, ): """Load integration modbus using mocked pymodbus.""" caplog.clear() @@ -573,16 +575,13 @@ async def mock_modbus_read_pymodbus_fixture( } ], } - now = dt_util.utcnow() - with mock.patch("homeassistant.helpers.event.dt_util.utcnow", return_value=now): - assert await async_setup_component(hass, DOMAIN, config) is True - await hass.async_block_till_done() + assert await async_setup_component(hass, DOMAIN, config) is True + await hass.async_block_till_done() assert DOMAIN in hass.config.components assert caplog.text == "" - now = now + timedelta(seconds=DEFAULT_SCAN_INTERVAL + 60) - with mock.patch("homeassistant.helpers.event.dt_util.utcnow", return_value=now): - async_fire_time_changed(hass, now) - await hass.async_block_till_done() + freezer.tick(timedelta(seconds=DEFAULT_SCAN_INTERVAL + 60)) + async_fire_time_changed(hass) + await hass.async_block_till_done() yield mock_pymodbus @@ -690,7 +689,7 @@ async def test_pymodbus_connect_fail(hass, caplog, mock_pymodbus): assert ExceptionMessage in caplog.text -async def test_delay(hass, mock_pymodbus): +async def test_delay(hass, mock_pymodbus, freezer: FrozenDateTimeFactory): """Run test for startup delay.""" # the purpose of this test is to test startup delay @@ -720,11 +719,8 @@ async def test_delay(hass, mock_pymodbus): } mock_pymodbus.read_coils.return_value = ReadResult([0x01]) start_time = dt_util.utcnow() - with mock.patch( - "homeassistant.helpers.event.dt_util.utcnow", return_value=start_time - ): - assert await async_setup_component(hass, DOMAIN, config) is True - await hass.async_block_till_done() + assert await async_setup_component(hass, DOMAIN, config) is True + await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_UNKNOWN time_sensor_active = start_time + timedelta(seconds=2) @@ -736,19 +732,15 @@ async def test_delay(hass, mock_pymodbus): # This test assumed listeners are always fired at 0 # microseconds which is impossible in production so # we use 999999 microseconds to simulate the real world. - now += timedelta(seconds=1, microseconds=999999) - with mock.patch( - "homeassistant.helpers.event.dt_util.utcnow", - return_value=now, - autospec=True, - ): - async_fire_time_changed(hass, now) - await hass.async_block_till_done() - if now > time_sensor_active: - if now <= time_after_delay: - assert hass.states.get(entity_id).state == STATE_UNAVAILABLE - elif now > time_after_scan: - assert hass.states.get(entity_id).state == STATE_ON + freezer.tick(timedelta(seconds=1, microseconds=999999)) + now = dt_util.utcnow() + async_fire_time_changed(hass, now) + await hass.async_block_till_done() + if now > time_sensor_active: + if now <= time_after_delay: + assert hass.states.get(entity_id).state == STATE_UNAVAILABLE + elif now > time_after_scan: + assert hass.states.get(entity_id).state == STATE_ON @pytest.mark.parametrize( @@ -853,19 +845,20 @@ async def test_write_no_client(hass, mock_modbus): @pytest.mark.parametrize("do_config", [{}]) -async def test_integration_reload(hass, caplog, mock_modbus): +async def test_integration_reload( + hass, caplog, mock_modbus, freezer: FrozenDateTimeFactory +): """Run test for integration reload.""" caplog.set_level(logging.INFO) caplog.clear() yaml_path = get_fixture_path("configuration.yaml", "modbus") - now = dt_util.utcnow() with mock.patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): await hass.services.async_call(DOMAIN, SERVICE_RELOAD, blocking=True) await hass.async_block_till_done() for i in range(4): - now = now + timedelta(seconds=1) - async_fire_time_changed(hass, now) + freezer.tick(timedelta(seconds=1)) + async_fire_time_changed(hass) await hass.async_block_till_done() assert "Modbus reloading" in caplog.text From f8ff68559653c5c7a3b0eba8d920d88b97ebf322 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Mon, 28 Nov 2022 23:32:52 +0100 Subject: [PATCH 09/10] Make sure skybell object are fully mocked Old tests would trigger attempts to post to could services: ``` DEBUG:aioskybell:HTTP post https://cloud.myskybell.com/api/v3/login/ Request with headers: {'content-type': 'application/json', 'accept': '*/*', 'x-skybell-app-id': 'd2b542c7-a7e4-4e1e-b77d-2b76911c7c46', 'x-skybell-client-id': '1f36a3c0-6dee-4997-a6db-4e1c67338e57'} ``` --- tests/components/skybell/__init__.py | 18 --- tests/components/skybell/conftest.py | 25 +++ tests/components/skybell/test_config_flow.py | 152 +++++++++---------- 3 files changed, 98 insertions(+), 97 deletions(-) create mode 100644 tests/components/skybell/conftest.py diff --git a/tests/components/skybell/__init__.py b/tests/components/skybell/__init__.py index dd162ed5d800d0..fc049adcc3dc32 100644 --- a/tests/components/skybell/__init__.py +++ b/tests/components/skybell/__init__.py @@ -1,7 +1,5 @@ """Tests for the SkyBell integration.""" -from unittest.mock import AsyncMock, patch - from homeassistant.const import CONF_EMAIL, CONF_PASSWORD USERNAME = "user" @@ -12,19 +10,3 @@ CONF_EMAIL: USERNAME, CONF_PASSWORD: PASSWORD, } - - -def _patch_skybell_devices() -> None: - mocked_skybell = AsyncMock() - mocked_skybell.user_id = USER_ID - return patch( - "homeassistant.components.skybell.config_flow.Skybell.async_get_devices", - return_value=[mocked_skybell], - ) - - -def _patch_skybell() -> None: - return patch( - "homeassistant.components.skybell.config_flow.Skybell.async_send_request", - return_value={"id": USER_ID}, - ) diff --git a/tests/components/skybell/conftest.py b/tests/components/skybell/conftest.py new file mode 100644 index 00000000000000..46337f612e173d --- /dev/null +++ b/tests/components/skybell/conftest.py @@ -0,0 +1,25 @@ +"""Test setup for the SkyBell integration.""" + +from unittest.mock import AsyncMock, patch + +from aioskybell import Skybell, SkybellDevice +from pytest import fixture + +from . import USER_ID + + +@fixture(autouse=True) +def skybell_mock(): + """Fixture for our skybell tests.""" + mocked_skybell_device = AsyncMock(spec=SkybellDevice) + + mocked_skybell = AsyncMock(spec=Skybell) + mocked_skybell.async_get_devices.return_value = [mocked_skybell_device] + mocked_skybell.async_send_request.return_value = {"id": USER_ID} + mocked_skybell.user_id = USER_ID + + with patch( + "homeassistant.components.skybell.config_flow.Skybell", + return_value=mocked_skybell, + ), patch("homeassistant.components.skybell.Skybell", return_value=mocked_skybell): + yield mocked_skybell diff --git a/tests/components/skybell/test_config_flow.py b/tests/components/skybell/test_config_flow.py index 9b7afd12c93602..318819246a392b 100644 --- a/tests/components/skybell/test_config_flow.py +++ b/tests/components/skybell/test_config_flow.py @@ -2,6 +2,7 @@ from unittest.mock import patch from aioskybell import exceptions +from pytest import fixture from homeassistant import config_entries from homeassistant.components.skybell.const import DOMAIN @@ -10,43 +11,39 @@ from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType -from . import ( - CONF_CONFIG_FLOW, - PASSWORD, - USER_ID, - _patch_skybell, - _patch_skybell_devices, -) +from . import CONF_CONFIG_FLOW, PASSWORD, USER_ID from tests.common import MockConfigEntry -def _patch_setup_entry() -> None: - return patch( +@fixture(autouse=True) +def setup_entry() -> None: + """Make sure component doesn't initialize.""" + with patch( "homeassistant.components.skybell.async_setup_entry", return_value=True, - ) + ): + yield async def test_flow_user(hass: HomeAssistant) -> None: """Test that the user step works.""" - with _patch_skybell(), _patch_skybell_devices(), _patch_setup_entry(): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} - ) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) - assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "user" + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input=CONF_CONFIG_FLOW, - ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=CONF_CONFIG_FLOW, + ) - assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["title"] == "user" - assert result["data"] == CONF_CONFIG_FLOW - assert result["result"].unique_id == USER_ID + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "user" + assert result["data"] == CONF_CONFIG_FLOW + assert result["result"].unique_id == USER_ID async def test_flow_user_already_configured(hass: HomeAssistant) -> None: @@ -57,50 +54,48 @@ async def test_flow_user_already_configured(hass: HomeAssistant) -> None: ) entry.add_to_hass(hass) - with _patch_skybell(), _patch_skybell_devices(): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=CONF_CONFIG_FLOW - ) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=CONF_CONFIG_FLOW + ) assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" -async def test_flow_user_cannot_connect(hass: HomeAssistant) -> None: +async def test_flow_user_cannot_connect(hass: HomeAssistant, skybell_mock) -> None: """Test user initialized flow with unreachable server.""" - with _patch_skybell() as skybell_mock: - skybell_mock.side_effect = exceptions.SkybellException(hass) - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=CONF_CONFIG_FLOW - ) - assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "user" - assert result["errors"] == {"base": "cannot_connect"} + skybell_mock.async_initialize.side_effect = exceptions.SkybellException(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=CONF_CONFIG_FLOW + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "cannot_connect"} -async def test_invalid_credentials(hass: HomeAssistant) -> None: +async def test_invalid_credentials(hass: HomeAssistant, skybell_mock) -> None: """Test that invalid credentials throws an error.""" - with patch("homeassistant.components.skybell.Skybell.async_login") as skybell_mock: - skybell_mock.side_effect = exceptions.SkybellAuthenticationException(hass) - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=CONF_CONFIG_FLOW - ) + skybell_mock.async_initialize.side_effect = ( + exceptions.SkybellAuthenticationException(hass) + ) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=CONF_CONFIG_FLOW + ) - assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "user" - assert result["errors"] == {"base": "invalid_auth"} + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "invalid_auth"} -async def test_flow_user_unknown_error(hass: HomeAssistant) -> None: +async def test_flow_user_unknown_error(hass: HomeAssistant, skybell_mock) -> None: """Test user initialized flow with unreachable server.""" - with _patch_skybell_devices() as skybell_mock: - skybell_mock.side_effect = Exception - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=CONF_CONFIG_FLOW - ) - assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "user" - assert result["errors"] == {"base": "unknown"} + skybell_mock.async_initialize.side_effect = Exception + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=CONF_CONFIG_FLOW + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "unknown"} async def test_step_reauth(hass: HomeAssistant) -> None: @@ -121,17 +116,15 @@ async def test_step_reauth(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.FORM assert result["step_id"] == "reauth_confirm" - with _patch_skybell(), _patch_skybell_devices(), _patch_setup_entry(): - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={CONF_PASSWORD: PASSWORD}, - ) - assert result["type"] == FlowResultType.ABORT - assert result["reason"] == "reauth_successful" + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_PASSWORD: PASSWORD}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "reauth_successful" -async def test_step_reauth_failed(hass: HomeAssistant) -> None: +async def test_step_reauth_failed(hass: HomeAssistant, skybell_mock) -> None: """Test the reauth flow fails and recovers.""" entry = MockConfigEntry(domain=DOMAIN, unique_id=USER_ID, data=CONF_CONFIG_FLOW) entry.add_to_hass(hass) @@ -149,21 +142,22 @@ async def test_step_reauth_failed(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.FORM assert result["step_id"] == "reauth_confirm" - with patch("homeassistant.components.skybell.Skybell.async_login") as skybell_mock: - skybell_mock.side_effect = exceptions.SkybellAuthenticationException(hass) - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={CONF_PASSWORD: PASSWORD}, - ) + skybell_mock.async_initialize.side_effect = ( + exceptions.SkybellAuthenticationException(hass) + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_PASSWORD: PASSWORD}, + ) - assert result["type"] == FlowResultType.FORM - assert result["errors"] == {"base": "invalid_auth"} + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {"base": "invalid_auth"} - with _patch_skybell(), _patch_skybell_devices(), _patch_setup_entry(): + skybell_mock.async_initialize.side_effect = None - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={CONF_PASSWORD: PASSWORD}, - ) - assert result["type"] == FlowResultType.ABORT - assert result["reason"] == "reauth_successful" + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_PASSWORD: PASSWORD}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "reauth_successful" From f48843acae6eb0f7c02a46d306b9d13d527e6c9c Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Tue, 29 Nov 2022 21:44:05 +0100 Subject: [PATCH 10/10] Fix sorting that broke after rebase --- tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 0d7858a0e3eafd..4dc988db8fcbd9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,8 +5,8 @@ from collections.abc import AsyncGenerator, Callable, Generator from contextlib import asynccontextmanager import functools -import itertools import gc +import itertools from json import JSONDecoder, loads import logging import sqlite3