Fix recursive limit in find_next_time_expression_time (#58914)

* Fix recursive limit in find_next_time_expression_time

* Add test case

* Update test_event.py

Co-authored-by: Erik Montnemery <erik@montnemery.com>
This commit is contained in:
J. Nick Koston 2021-11-01 18:11:17 -05:00 committed by GitHub
parent 63c9cfdbc8
commit 1cd9be7538
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 156 additions and 89 deletions

View File

@ -245,6 +245,16 @@ def _dst_offset_diff(dattim: dt.datetime) -> dt.timedelta:
return (dattim + delta).utcoffset() - (dattim - delta).utcoffset() # type: ignore[operator]
def _lower_bound(arr: list[int], cmp: int) -> int | None:
"""Return the first value in arr greater or equal to cmp.
Return None if no such value exists.
"""
if (left := bisect.bisect_left(arr, cmp)) == len(arr):
return None
return arr[left]
def find_next_time_expression_time(
now: dt.datetime, # pylint: disable=redefined-outer-name
seconds: list[int],
@ -263,15 +273,7 @@ def find_next_time_expression_time(
if not seconds or not minutes or not hours:
raise ValueError("Cannot find a next time: Time expression never matches!")
def _lower_bound(arr: list[int], cmp: int) -> int | None:
"""Return the first value in arr greater or equal to cmp.
Return None if no such value exists.
"""
if (left := bisect.bisect_left(arr, cmp)) == len(arr):
return None
return arr[left]
while True:
# Reset microseconds and fold; fold (for ambiguous DST times) will be handled later
result = now.replace(microsecond=0, fold=0)
@ -325,9 +327,8 @@ def find_next_time_expression_time(
# We solve this edge case by just iterating one second until the result exists
# (max. 3600 operations, which should be fine for an edge case that happens once a year)
return find_next_time_expression_time(
result + dt.timedelta(seconds=1), seconds, minutes, hours
)
now += dt.timedelta(seconds=1)
continue
now_is_ambiguous = _datetime_ambiguous(now)
result_is_ambiguous = _datetime_ambiguous(result)

View File

@ -3452,6 +3452,72 @@ async def test_periodic_task_entering_dst(hass):
unsub()
async def test_periodic_task_entering_dst_2(hass):
"""Test periodic task behavior when entering dst.
This tests a task firing every second in the range 0..58 (not *:*:59)
"""
timezone = dt_util.get_time_zone("Europe/Vienna")
dt_util.set_default_time_zone(timezone)
specific_runs = []
# DST starts early morning March 27th 2022
yy = 2022
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
assert (
time_that_will_not_match_right_away.utcoffset()
!= (time_that_will_not_match_right_away + timedelta(hours=2)).utcoffset()
)
with patch(
"homeassistant.util.dt.utcnow", return_value=time_that_will_not_match_right_away
):
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()
assert len(specific_runs) == 0
async_fire_time_changed(
hass, datetime(yy, mm, dd, 3, 0, 0, 999999, tzinfo=timezone)
)
await hass.async_block_till_done()
assert len(specific_runs) == 1
async_fire_time_changed(
hass, datetime(yy, mm, dd, 3, 0, 1, 999999, tzinfo=timezone)
)
await hass.async_block_till_done()
assert len(specific_runs) == 2
async_fire_time_changed(
hass, datetime(yy, mm, dd + 1, 1, 59, 59, 999999, tzinfo=timezone)
)
await hass.async_block_till_done()
assert len(specific_runs) == 3
async_fire_time_changed(
hass, datetime(yy, mm, dd + 1, 2, 0, 0, 999999, tzinfo=timezone)
)
await hass.async_block_till_done()
assert len(specific_runs) == 4
unsub()
async def test_periodic_task_leaving_dst(hass):
"""Test periodic task behavior when leaving dst."""
timezone = dt_util.get_time_zone("Europe/Vienna")