Use freezegun in DST tests (#58939)

This commit is contained in:
Erik Montnemery 2021-11-02 18:11:39 +01:00 committed by GitHub
parent 2df1ba2346
commit 30f7bc0f18
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 166 additions and 162 deletions

View file

@ -9,6 +9,7 @@
-r requirements_test_pre_commit.txt -r requirements_test_pre_commit.txt
codecov==2.1.12 codecov==2.1.12
coverage==6.1.1 coverage==6.1.1
freezegun==1.1.0
jsonpickle==1.4.1 jsonpickle==1.4.1
mock-open==1.4.0 mock-open==1.4.0
mypy==0.910 mypy==0.910
@ -18,6 +19,7 @@ pipdeptree==2.1.0
pylint-strict-informational==0.1 pylint-strict-informational==0.1
pytest-aiohttp==0.3.0 pytest-aiohttp==0.3.0
pytest-cov==2.12.1 pytest-cov==2.12.1
pytest-freezegun==0.4.2
pytest-socket==0.4.1 pytest-socket==0.4.1
pytest-test-groups==1.0.3 pytest-test-groups==1.0.3
pytest-sugar==0.9.4 pytest-sugar==0.9.4

View file

@ -371,9 +371,12 @@ fire_mqtt_message = threadsafe_callback_factory(async_fire_mqtt_message)
@ha.callback @ha.callback
def async_fire_time_changed( def async_fire_time_changed(
hass: HomeAssistant, datetime_: datetime, fire_all: bool = False hass: HomeAssistant, datetime_: datetime = None, fire_all: bool = False
) -> None: ) -> None:
"""Fire a time changes event.""" """Fire a time changed event."""
if datetime_ is None:
datetime_ = date_util.utcnow()
hass.bus.async_fire(EVENT_TIME_CHANGED, {"now": date_util.as_utc(datetime_)}) hass.bus.async_fire(EVENT_TIME_CHANGED, {"now": date_util.as_utc(datetime_)})
for task in list(hass.loop._scheduled): for task in list(hass.loop._scheduled):

View file

@ -9,6 +9,7 @@ import threading
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
from aiohttp.test_utils import make_mocked_request from aiohttp.test_utils import make_mocked_request
import freezegun
import multidict import multidict
import pytest import pytest
import pytest_socket import pytest_socket
@ -63,15 +64,24 @@ def pytest_configure(config):
def pytest_runtest_setup(): def pytest_runtest_setup():
"""Throw if tests attempt to open sockets. """Prepare pytest_socket and freezegun.
pytest_socket:
Throw if tests attempt to open sockets.
allow_unix_socket is set to True because it's needed by asyncio. allow_unix_socket is set to True because it's needed by asyncio.
Important: socket_allow_hosts must be called before disable_socket, otherwise all Important: socket_allow_hosts must be called before disable_socket, otherwise all
destinations will be allowed. destinations will be allowed.
freezegun:
Modified to include https://github.com/spulec/freezegun/pull/424
""" """
pytest_socket.socket_allow_hosts(["127.0.0.1"]) pytest_socket.socket_allow_hosts(["127.0.0.1"])
disable_socket(allow_unix_socket=True) disable_socket(allow_unix_socket=True)
freezegun.api.datetime_to_fakedatetime = ha_datetime_to_fakedatetime
freezegun.api.FakeDatetime = HAFakeDatetime
@pytest.fixture @pytest.fixture
def socket_disabled(pytestconfig): def socket_disabled(pytestconfig):
@ -126,6 +136,43 @@ def disable_socket(allow_unix_socket=False):
socket.socket = GuardedSocket socket.socket = GuardedSocket
def ha_datetime_to_fakedatetime(datetime):
"""Convert datetime to FakeDatetime.
Modified to include https://github.com/spulec/freezegun/pull/424.
"""
return freezegun.api.FakeDatetime(
datetime.year,
datetime.month,
datetime.day,
datetime.hour,
datetime.minute,
datetime.second,
datetime.microsecond,
datetime.tzinfo,
fold=datetime.fold,
)
class HAFakeDatetime(freezegun.api.FakeDatetime):
"""Modified to include https://github.com/spulec/freezegun/pull/424."""
@classmethod
def now(cls, tz=None):
"""Return frozen now."""
now = cls._time_to_freeze() or freezegun.api.real_datetime.now()
if tz:
result = tz.fromutc(now.replace(tzinfo=tz))
else:
result = now
# Add the _tz_offset only if it's non-zero to preserve fold
if cls._tz_offset():
result += cls._tz_offset()
return ha_datetime_to_fakedatetime(result)
def check_real(func): def check_real(func):
"""Force a function to require a keyword _test_real to be passed in.""" """Force a function to require a keyword _test_real to be passed in."""

View file

@ -1,7 +1,7 @@
"""Test event helpers.""" """Test event helpers."""
# pylint: disable=protected-access # pylint: disable=protected-access
import asyncio import asyncio
from datetime import datetime, timedelta from datetime import date, datetime, timedelta
from unittest.mock import patch from unittest.mock import patch
from astral import LocationInfo from astral import LocationInfo
@ -3393,66 +3393,56 @@ async def test_periodic_task_duplicate_time(hass):
unsub() unsub()
async def test_periodic_task_entering_dst(hass): # DST starts early morning March 28th 2021
@pytest.mark.freeze_time("2021-03-28 01:28:00+01:00")
async def test_periodic_task_entering_dst(hass, freezer):
"""Test periodic task behavior when entering dst.""" """Test periodic task behavior when entering dst."""
timezone = dt_util.get_time_zone("Europe/Vienna") timezone = dt_util.get_time_zone("Europe/Vienna")
dt_util.set_default_time_zone(timezone) dt_util.set_default_time_zone(timezone)
specific_runs = [] specific_runs = []
# DST starts early morning March 27th 2022 today = date.today().isoformat()
yy = 2022 tomorrow = (date.today() + timedelta(days=1)).isoformat()
mm = 3
dd = 27
# There's no 2022-03-27 02:30, the event should not fire until 2022-03-28 02:30
time_that_will_not_match_right_away = datetime(
yy, mm, dd, 1, 28, 0, tzinfo=timezone, fold=0
)
# Make sure we enter DST during the test # Make sure we enter DST during the test
assert ( now_local = dt_util.now()
time_that_will_not_match_right_away.utcoffset() assert now_local.utcoffset() != (now_local + timedelta(hours=2)).utcoffset()
!= (time_that_will_not_match_right_away + timedelta(hours=2)).utcoffset()
unsub = async_track_time_change(
hass,
callback(lambda x: specific_runs.append(x)),
hour=2,
minute=30,
second=0,
) )
with patch( freezer.move_to(f"{today} 01:50:00.999999+01:00")
"homeassistant.util.dt.utcnow", return_value=time_that_will_not_match_right_away async_fire_time_changed(hass)
):
unsub = async_track_time_change(
hass,
callback(lambda x: specific_runs.append(x)),
hour=2,
minute=30,
second=0,
)
async_fire_time_changed(
hass, datetime(yy, mm, dd, 1, 50, 0, 999999, tzinfo=timezone)
)
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(specific_runs) == 0 assert len(specific_runs) == 0
async_fire_time_changed( # There was no 02:30 today, the event should not fire until tomorrow
hass, datetime(yy, mm, dd, 3, 50, 0, 999999, tzinfo=timezone) freezer.move_to(f"{today} 03:50:00.999999+02:00")
) async_fire_time_changed(hass)
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(specific_runs) == 0 assert len(specific_runs) == 0
async_fire_time_changed( freezer.move_to(f"{tomorrow} 01:50:00.999999+02:00")
hass, datetime(yy, mm, dd + 1, 1, 50, 0, 999999, tzinfo=timezone) async_fire_time_changed(hass)
)
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(specific_runs) == 0 assert len(specific_runs) == 0
async_fire_time_changed( freezer.move_to(f"{tomorrow} 02:50:00.999999+02:00")
hass, datetime(yy, mm, dd + 1, 2, 50, 0, 999999, tzinfo=timezone) async_fire_time_changed(hass)
)
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(specific_runs) == 1 assert len(specific_runs) == 1
unsub() unsub()
async def test_periodic_task_entering_dst_2(hass): # DST starts early morning March 28th 2021
@pytest.mark.freeze_time("2021-03-28 01:59:59+01:00")
async def test_periodic_task_entering_dst_2(hass, freezer):
"""Test periodic task behavior when entering dst. """Test periodic task behavior when entering dst.
This tests a task firing every second in the range 0..58 (not *:*:59) This tests a task firing every second in the range 0..58 (not *:*:59)
@ -3461,220 +3451,182 @@ async def test_periodic_task_entering_dst_2(hass):
dt_util.set_default_time_zone(timezone) dt_util.set_default_time_zone(timezone)
specific_runs = [] specific_runs = []
# DST starts early morning March 27th 2022 today = date.today().isoformat()
yy = 2022 tomorrow = (date.today() + timedelta(days=1)).isoformat()
mm = 3
dd = 27
# There's no 2022-03-27 02:00:00, the event should not fire until 2022-03-28 03:00:00
time_that_will_not_match_right_away = datetime(
yy, mm, dd, 1, 59, 59, tzinfo=timezone, fold=0
)
# Make sure we enter DST during the test # Make sure we enter DST during the test
assert ( now_local = dt_util.now()
time_that_will_not_match_right_away.utcoffset() assert now_local.utcoffset() != (now_local + timedelta(hours=2)).utcoffset()
!= (time_that_will_not_match_right_away + timedelta(hours=2)).utcoffset()
unsub = async_track_time_change(
hass,
callback(lambda x: specific_runs.append(x)),
second=list(range(59)),
) )
with patch( freezer.move_to(f"{today} 01:59:59.999999+01:00")
"homeassistant.util.dt.utcnow", return_value=time_that_will_not_match_right_away async_fire_time_changed(hass)
):
unsub = async_track_time_change(
hass,
callback(lambda x: specific_runs.append(x)),
second=list(range(59)),
)
async_fire_time_changed(
hass, datetime(yy, mm, dd, 1, 59, 59, 999999, tzinfo=timezone)
)
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(specific_runs) == 0 assert len(specific_runs) == 0
async_fire_time_changed( freezer.move_to(f"{today} 03:00:00.999999+02:00")
hass, datetime(yy, mm, dd, 3, 0, 0, 999999, tzinfo=timezone) async_fire_time_changed(hass)
)
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(specific_runs) == 1 assert len(specific_runs) == 1
async_fire_time_changed( freezer.move_to(f"{today} 03:00:01.999999+02:00")
hass, datetime(yy, mm, dd, 3, 0, 1, 999999, tzinfo=timezone) async_fire_time_changed(hass)
)
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(specific_runs) == 2 assert len(specific_runs) == 2
async_fire_time_changed( freezer.move_to(f"{tomorrow} 01:59:59.999999+02:00")
hass, datetime(yy, mm, dd + 1, 1, 59, 59, 999999, tzinfo=timezone) async_fire_time_changed(hass)
)
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(specific_runs) == 3 assert len(specific_runs) == 3
async_fire_time_changed( freezer.move_to(f"{tomorrow} 02:00:00.999999+02:00")
hass, datetime(yy, mm, dd + 1, 2, 0, 0, 999999, tzinfo=timezone) async_fire_time_changed(hass)
)
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(specific_runs) == 4 assert len(specific_runs) == 4
unsub() unsub()
async def test_periodic_task_leaving_dst(hass): # DST ends early morning October 31st 2021
@pytest.mark.freeze_time("2021-10-31 02:28:00+02:00")
async def test_periodic_task_leaving_dst(hass, freezer):
"""Test periodic task behavior when leaving dst.""" """Test periodic task behavior when leaving dst."""
timezone = dt_util.get_time_zone("Europe/Vienna") timezone = dt_util.get_time_zone("Europe/Vienna")
dt_util.set_default_time_zone(timezone) dt_util.set_default_time_zone(timezone)
specific_runs = [] specific_runs = []
# DST ends early morning Ocotber 30th 2022 today = date.today().isoformat()
yy = 2022 tomorrow = (date.today() + timedelta(days=1)).isoformat()
mm = 10
dd = 30
time_that_will_not_match_right_away = datetime(
yy, mm, dd, 2, 28, 0, tzinfo=timezone, fold=0
)
# Make sure we leave DST during the test # Make sure we leave DST during the test
assert ( now_local = dt_util.now()
time_that_will_not_match_right_away.utcoffset() assert now_local.utcoffset() != (now_local + timedelta(hours=1)).utcoffset()
!= time_that_will_not_match_right_away.replace(fold=1).utcoffset()
)
with patch( unsub = async_track_time_change(
"homeassistant.util.dt.utcnow", return_value=time_that_will_not_match_right_away hass,
): callback(lambda x: specific_runs.append(x)),
unsub = async_track_time_change( hour=2,
hass, minute=30,
callback(lambda x: specific_runs.append(x)), second=0,
hour=2, )
minute=30,
second=0,
)
# The task should not fire yet # The task should not fire yet
async_fire_time_changed( freezer.move_to(f"{today} 02:28:00.999999+02:00")
hass, datetime(yy, mm, dd, 2, 28, 0, 999999, tzinfo=timezone, fold=0) async_fire_time_changed(hass)
) assert dt_util.now().fold == 0
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(specific_runs) == 0 assert len(specific_runs) == 0
# The task should fire # The task should fire
async_fire_time_changed( freezer.move_to(f"{today} 02:30:00.999999+02:00")
hass, datetime(yy, mm, dd, 2, 30, 0, 999999, tzinfo=timezone, fold=0) async_fire_time_changed(hass)
) assert dt_util.now().fold == 0
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(specific_runs) == 1 assert len(specific_runs) == 1
# The task should not fire again # The task should not fire again
async_fire_time_changed( freezer.move_to(f"{today} 02:55:00.999999+02:00")
hass, datetime(yy, mm, dd, 2, 55, 0, 999999, tzinfo=timezone, fold=0) async_fire_time_changed(hass)
) assert dt_util.now().fold == 0
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(specific_runs) == 1 assert len(specific_runs) == 1
# DST has ended, the task should not fire yet # DST has ended, the task should not fire yet
async_fire_time_changed( freezer.move_to(f"{today} 02:15:00.999999+01:00")
hass, async_fire_time_changed(hass)
datetime(yy, mm, dd, 2, 15, 0, 999999, tzinfo=timezone, fold=1), assert dt_util.now().fold == 1 # DST has ended
)
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(specific_runs) == 1 assert len(specific_runs) == 1
# The task should fire # The task should fire
async_fire_time_changed( freezer.move_to(f"{today} 02:45:00.999999+01:00")
hass, async_fire_time_changed(hass)
datetime(yy, mm, dd, 2, 45, 0, 999999, tzinfo=timezone, fold=1), assert dt_util.now().fold == 1
)
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(specific_runs) == 2 assert len(specific_runs) == 2
# The task should not fire again # The task should not fire again
async_fire_time_changed( freezer.move_to(f"{today} 02:55:00.999999+01:00")
hass, async_fire_time_changed(hass)
datetime(yy, mm, dd, 2, 55, 0, 999999, tzinfo=timezone, fold=1), assert dt_util.now().fold == 1
)
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(specific_runs) == 2 assert len(specific_runs) == 2
# The task should fire again the next day # The task should fire again the next day
async_fire_time_changed( freezer.move_to(f"{tomorrow} 02:55:00.999999+01:00")
hass, datetime(yy, mm, dd + 1, 2, 55, 0, 999999, tzinfo=timezone, fold=1) async_fire_time_changed(hass)
) assert dt_util.now().fold == 0
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(specific_runs) == 3 assert len(specific_runs) == 3
unsub() unsub()
async def test_periodic_task_leaving_dst_2(hass): # DST ends early morning October 31st 2021
@pytest.mark.freeze_time("2021-10-31 02:28:00+02:00")
async def test_periodic_task_leaving_dst_2(hass, freezer):
"""Test periodic task behavior when leaving dst.""" """Test periodic task behavior when leaving dst."""
timezone = dt_util.get_time_zone("Europe/Vienna") timezone = dt_util.get_time_zone("Europe/Vienna")
dt_util.set_default_time_zone(timezone) dt_util.set_default_time_zone(timezone)
specific_runs = [] specific_runs = []
# DST ends early morning Ocotber 30th 2022 today = date.today().isoformat()
yy = 2022
mm = 10
dd = 30
time_that_will_not_match_right_away = datetime(
yy, mm, dd, 2, 28, 0, tzinfo=timezone, fold=0
)
# Make sure we leave DST during the test # Make sure we leave DST during the test
assert ( now_local = dt_util.now()
time_that_will_not_match_right_away.utcoffset() assert now_local.utcoffset() != (now_local + timedelta(hours=1)).utcoffset()
!= time_that_will_not_match_right_away.replace(fold=1).utcoffset()
)
with patch( unsub = async_track_time_change(
"homeassistant.util.dt.utcnow", return_value=time_that_will_not_match_right_away hass,
): callback(lambda x: specific_runs.append(x)),
unsub = async_track_time_change( minute=30,
hass, second=0,
callback(lambda x: specific_runs.append(x)), )
minute=30,
second=0,
)
# The task should not fire yet # The task should not fire yet
async_fire_time_changed( freezer.move_to(f"{today} 02:28:00.999999+02:00")
hass, datetime(yy, mm, dd, 2, 28, 0, 999999, tzinfo=timezone, fold=0) async_fire_time_changed(hass)
) assert dt_util.now().fold == 0
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(specific_runs) == 0 assert len(specific_runs) == 0
# The task should fire # The task should fire
async_fire_time_changed( freezer.move_to(f"{today} 02:55:00.999999+02:00")
hass, datetime(yy, mm, dd, 2, 55, 0, 999999, tzinfo=timezone, fold=0) async_fire_time_changed(hass)
) assert dt_util.now().fold == 0
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(specific_runs) == 1 assert len(specific_runs) == 1
# DST has ended, the task should not fire yet # DST has ended, the task should not fire yet
async_fire_time_changed( freezer.move_to(f"{today} 02:15:00.999999+01:00")
hass, datetime(yy, mm, dd, 2, 15, 0, 999999, tzinfo=timezone, fold=1) async_fire_time_changed(hass)
) assert dt_util.now().fold == 1
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(specific_runs) == 1 assert len(specific_runs) == 1
# The task should fire # The task should fire
async_fire_time_changed( freezer.move_to(f"{today} 02:45:00.999999+01:00")
hass, datetime(yy, mm, dd, 2, 45, 0, 999999, tzinfo=timezone, fold=1) async_fire_time_changed(hass)
) assert dt_util.now().fold == 1
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(specific_runs) == 2 assert len(specific_runs) == 2
# The task should not fire again # The task should not fire again
async_fire_time_changed( freezer.move_to(f"{today} 02:55:00.999999+01:00")
hass, async_fire_time_changed(hass)
datetime(yy, mm, dd, 2, 55, 0, 999999, tzinfo=timezone, fold=1), assert dt_util.now().fold == 1
)
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(specific_runs) == 2 assert len(specific_runs) == 2
# The task should fire again the next hour # The task should fire again the next hour
async_fire_time_changed( freezer.move_to(f"{today} 03:55:00.999999+01:00")
hass, datetime(yy, mm, dd, 3, 55, 0, 999999, tzinfo=timezone, fold=0) async_fire_time_changed(hass)
) assert dt_util.now().fold == 0
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(specific_runs) == 3 assert len(specific_runs) == 3