Skip to content

Commit

Permalink
Bump version
Browse files Browse the repository at this point in the history
  • Loading branch information
MatthewFlamm committed Dec 1, 2022
1 parent dcba456 commit f96a21c
Show file tree
Hide file tree
Showing 9 changed files with 158 additions and 35 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# pytest-homeassistant-custom-component

![HA core version](https://img.shields.io/static/v1?label=HA+core+version&message=2022.11.4&labelColor=blue)
![HA core version](https://img.shields.io/static/v1?label=HA+core+version&message=2022.12.0b0&labelColor=blue)

[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/MatthewFlamm/pytest-homeassistant-custom-component)

Expand Down
2 changes: 1 addition & 1 deletion ha_version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
2022.11.4
2022.12.0b0
54 changes: 49 additions & 5 deletions pytest_homeassistant_custom_component/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
from aiohttp.test_utils import unused_port as get_test_instance_port # noqa: F401
import voluptuous as vol

from homeassistant import auth, config_entries, core as ha, loader
from homeassistant import auth, bootstrap, config_entries, core as ha, loader
from homeassistant.auth import (
auth_store,
models as auth_models,
Expand Down Expand Up @@ -165,7 +165,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)
Expand Down Expand Up @@ -294,6 +294,7 @@ async def _await_count_and_log_pending(
hass.config.units = METRIC_SYSTEM
hass.config.media_dirs = {"local": get_test_config_dir("media")}
hass.config.skip_pip = True
hass.config.skip_pip_packages = []

hass.config_entries = config_entries.ConfigEntries(
hass,
Expand All @@ -311,6 +312,7 @@ async def _await_count_and_log_pending(
issue_registry.async_load(hass),
)
await hass.async_block_till_done()
hass.data[bootstrap.DATA_REGISTRIES_LOADED] = None

hass.state = ha.CoreState.running

Expand Down Expand Up @@ -384,17 +386,59 @@ def async_fire_mqtt_message(hass, topic, payload, qos=0, retain=False):
fire_mqtt_message = threadsafe_callback_factory(async_fire_mqtt_message)


@ha.callback
def async_fire_time_changed_exact(
hass: HomeAssistant, datetime_: datetime | None = None, fire_all: bool = False
) -> None:
"""Fire a time changed event at an exact microsecond.
Consider that it is not possible to actually achieve an exact
microsecond in production as the event loop is not precise enough.
If your code relies on this level of precision, consider a different
approach, as this is only for testing.
"""
if datetime_ is None:
utc_datetime = date_util.utcnow()
else:
utc_datetime = date_util.as_utc(datetime_)

_async_fire_time_changed(hass, utc_datetime, fire_all)


@ha.callback
def async_fire_time_changed(
hass: HomeAssistant, datetime_: datetime = None, fire_all: bool = False
hass: HomeAssistant, datetime_: datetime | None = None, fire_all: bool = False
) -> None:
"""Fire a time changed event."""
"""Fire a time changed event.
This function will add up to 0.5 seconds to the time to ensure that
it accounts for the accidental synchronization avoidance code in repeating
listeners.
As asyncio is cooperative, we can't guarantee that the event loop will
run an event at the exact time we want. If you need to fire time changed
for an exact microsecond, use async_fire_time_changed_exact.
"""
if datetime_ is None:
utc_datetime = date_util.utcnow()
else:
utc_datetime = date_util.as_utc(datetime_)
timestamp = date_util.utc_to_timestamp(utc_datetime)

if utc_datetime.microsecond < 500000:
# Allow up to 500000 microseconds to be added to the time
# to handle update_coordinator's and
# async_track_time_interval's
# staggering to avoid thundering herd.
utc_datetime = utc_datetime.replace(microsecond=500000)

_async_fire_time_changed(hass, utc_datetime, fire_all)


@ha.callback
def _async_fire_time_changed(
hass: HomeAssistant, utc_datetime: datetime | None, fire_all: bool
) -> None:
timestamp = date_util.utc_to_timestamp(utc_datetime)
for task in list(hass.loop._scheduled):
if not isinstance(task, asyncio.TimerHandle):
continue
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from dataclasses import dataclass
from datetime import datetime
import time
from typing import Any, cast
from typing import Any, Literal, cast

from sqlalchemy import create_engine
from sqlalchemy.orm.session import Session
Expand Down Expand Up @@ -57,7 +57,7 @@ def do_adhoc_statistics(hass: HomeAssistant, **kwargs: Any) -> None:
"""Trigger an adhoc statistics run."""
if not (start := kwargs.get("start")):
start = statistics.get_start_time()
get_instance(hass).queue_task(StatisticsTask(start))
get_instance(hass).queue_task(StatisticsTask(start, False))


def wait_recording_done(hass: HomeAssistant) -> None:
Expand Down Expand Up @@ -141,3 +141,21 @@ def run_information_with_session(
session.expunge(res)
return cast(RecorderRuns, res)
return res


def statistics_during_period(
hass: HomeAssistant,
start_time: datetime,
end_time: datetime | None = None,
statistic_ids: list[str] | None = None,
period: Literal["5minute", "day", "hour", "week", "month"] = "hour",
units: dict[str, str] | None = None,
types: set[Literal["last_reset", "max", "mean", "min", "state", "sum"]]
| None = None,
) -> dict[str, list[dict[str, Any]]]:
"""Call statistics_during_period with defaults for simpler ..."""
if types is None:
types = {"last_reset", "max", "mean", "min", "state", "sum"}
return statistics.statistics_during_period(
hass, start_time, end_time, statistic_ids, period, units, types
)
4 changes: 2 additions & 2 deletions pytest_homeassistant_custom_component/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@

APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2022
MINOR_VERSION: Final = 11
PATCH_VERSION: Final = "4"
MINOR_VERSION: Final = 12
PATCH_VERSION: Final = "0b0"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0)
91 changes: 75 additions & 16 deletions pytest_homeassistant_custom_component/plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,16 @@
from collections.abc import AsyncGenerator, Callable, Generator
from contextlib import asynccontextmanager
import functools
import gc
import itertools
from json import JSONDecoder, loads
import logging
import sqlite3
import ssl
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
Expand Down Expand Up @@ -191,12 +194,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:
Expand All @@ -207,6 +212,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():
Expand Down Expand Up @@ -281,7 +306,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.
Expand All @@ -292,6 +317,7 @@ def aiohttp_client(
aiohttp_client(server, **kwargs)
aiohttp_client(raw_server, **kwargs)
"""
loop = event_loop
clients = []

async def go(
Expand Down Expand Up @@ -338,9 +364,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
Expand Down Expand Up @@ -385,7 +412,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

Expand All @@ -406,6 +433,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
Expand Down Expand Up @@ -864,6 +892,16 @@ def enable_statistics():
return False


@pytest.fixture
def enable_statistics_table_validation():
"""Fixture to control enabling of recorder's statistics table validation.
To enable statistics table validation, tests can be marked with:
@pytest.mark.parametrize("enable_statistics_table_validation", [True])
"""
return False


@pytest.fixture
def enable_nightly_purge():
"""Fixture to control enabling of recorder's nightly purge job.
Expand Down Expand Up @@ -906,6 +944,7 @@ def hass_recorder(
recorder_db_url,
enable_nightly_purge,
enable_statistics,
enable_statistics_table_validation,
hass_storage,
):
"""Home Assistant fixture with in-memory recorder."""
Expand All @@ -914,6 +953,11 @@ def hass_recorder(
hass = get_test_home_assistant()
nightly = recorder.Recorder.async_nightly_tasks if enable_nightly_purge else None
stats = recorder.Recorder.async_periodic_statistics if enable_statistics else None
stats_validate = (
recorder.statistics.validate_db_schema
if enable_statistics_table_validation
else itertools.repeat(set())
)
with patch(
"homeassistant.components.recorder.Recorder.async_nightly_tasks",
side_effect=nightly,
Expand All @@ -922,6 +966,10 @@ def hass_recorder(
"homeassistant.components.recorder.Recorder.async_periodic_statistics",
side_effect=stats,
autospec=True,
), patch(
"homeassistant.components.recorder.migration.statistics_validate_db_schema",
side_effect=stats_validate,
autospec=True,
):

def setup_recorder(config=None):
Expand Down Expand Up @@ -966,12 +1014,18 @@ async def async_setup_recorder_instance(
hass_fixture_setup,
enable_nightly_purge,
enable_statistics,
enable_statistics_table_validation,
) -> AsyncGenerator[SetupRecorderInstanceT, None]:
"""Yield callable to setup recorder instance."""
assert not hass_fixture_setup

nightly = recorder.Recorder.async_nightly_tasks if enable_nightly_purge else None
stats = recorder.Recorder.async_periodic_statistics if enable_statistics else None
stats_validate = (
recorder.statistics.validate_db_schema
if enable_statistics_table_validation
else itertools.repeat(set())
)
with patch(
"homeassistant.components.recorder.Recorder.async_nightly_tasks",
side_effect=nightly,
Expand All @@ -980,6 +1034,10 @@ async def async_setup_recorder_instance(
"homeassistant.components.recorder.Recorder.async_periodic_statistics",
side_effect=stats,
autospec=True,
), patch(
"homeassistant.components.recorder.migration.statistics_validate_db_schema",
side_effect=stats_validate,
autospec=True,
):

async def async_setup_recorder(
Expand Down Expand Up @@ -1045,18 +1103,19 @@ async def mock_enable_bluetooth(
def mock_bluetooth_adapters():
"""Fixture to mock bluetooth adapters."""
with patch(
"homeassistant.components.bluetooth.util.platform.system", return_value="Linux"
), patch(
"bluetooth_adapters.BlueZDBusObjects", return_value=MagicMock(load=AsyncMock())
), patch(
"bluetooth_adapters.get_bluetooth_adapter_details",
return_value={
"bluetooth_adapters.systems.platform.system", return_value="Linux"
), patch("bluetooth_adapters.systems.linux.LinuxAdapters.refresh"), patch(
"bluetooth_adapters.systems.linux.LinuxAdapters.adapters",
{
"hci0": {
"org.bluez.Adapter1": {
"Address": "00:00:00:00:00:01",
"Name": "BlueZ 4.63",
"Modalias": "usbid:1234",
}
"address": "00:00:00:00:00:01",
"hw_version": "usb:v1D6Bp0246d053F",
"passive_scan": False,
"sw_version": "homeassistant",
"manufacturer": "ACME",
"product": "Bluetooth Adapter 5.0",
"product_id": "aa01",
"vendor_id": "cc01",
},
},
):
Expand Down
7 changes: 4 additions & 3 deletions requirements_dev.txt
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# This file is originally from homeassistant/core and modified by pytest-homeassistant-custom-component.
astroid==2.12.12
astroid==2.12.13
codecov==2.1.12
mypy==0.982
mypy==0.991
pre-commit==2.20.0
pylint==2.15.5
pylint==2.15.7
types-atomicwrites==1.4.1
types-croniter==1.0.0
types-backports==0.1.3
Expand All @@ -13,6 +13,7 @@ types-decorator==0.1.7
types-enum34==0.1.8
types-ipaddress==0.1.5
types-pkg-resources==0.1.3
types-python-dateutil==2.8.19.2
types-python-slugify==0.1.2
types-pytz==2021.1.2
types-PyYAML==5.4.6
Expand Down
Loading

0 comments on commit f96a21c

Please sign in to comment.