mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 11:17:21 +00:00
Fix debouncer not scheduling timer when wrapped function raises (#94689)
* Fix debouncer not scheduling timer when callback function raises * test coro as well * preen
This commit is contained in:
parent
34b725bb99
commit
26be0fab78
@ -90,11 +90,11 @@ class Debouncer(Generic[_R_co]):
|
|||||||
if self._timer_task:
|
if self._timer_task:
|
||||||
return
|
return
|
||||||
|
|
||||||
task = self.hass.async_run_hass_job(self._job)
|
try:
|
||||||
if task:
|
if task := self.hass.async_run_hass_job(self._job):
|
||||||
await task
|
await task
|
||||||
|
finally:
|
||||||
self._schedule_timer()
|
self._schedule_timer()
|
||||||
|
|
||||||
async def _handle_timer_finish(self) -> None:
|
async def _handle_timer_finish(self) -> None:
|
||||||
"""Handle a finished timer."""
|
"""Handle a finished timer."""
|
||||||
@ -112,14 +112,13 @@ class Debouncer(Generic[_R_co]):
|
|||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
task = self.hass.async_run_hass_job(self._job)
|
if task := self.hass.async_run_hass_job(self._job):
|
||||||
if task:
|
|
||||||
await task
|
await task
|
||||||
except Exception: # pylint: disable=broad-except
|
except Exception: # pylint: disable=broad-except
|
||||||
self.logger.exception("Unexpected exception from %s", self.function)
|
self.logger.exception("Unexpected exception from %s", self.function)
|
||||||
|
finally:
|
||||||
# Schedule a new timer to prevent new runs during cooldown
|
# Schedule a new timer to prevent new runs during cooldown
|
||||||
self._schedule_timer()
|
self._schedule_timer()
|
||||||
|
|
||||||
async def async_shutdown(self) -> None:
|
async def async_shutdown(self) -> None:
|
||||||
"""Cancel any scheduled call, and prevent new runs."""
|
"""Cancel any scheduled call, and prevent new runs."""
|
||||||
|
@ -6,7 +6,7 @@ from unittest.mock import AsyncMock
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers import debounce
|
from homeassistant.helpers import debounce
|
||||||
from homeassistant.util.dt import utcnow
|
from homeassistant.util.dt import utcnow
|
||||||
|
|
||||||
@ -69,6 +69,133 @@ async def test_immediate_works(hass: HomeAssistant) -> None:
|
|||||||
assert debouncer._job.target == debouncer.function
|
assert debouncer._job.target == debouncer.function
|
||||||
|
|
||||||
|
|
||||||
|
async def test_immediate_works_with_passed_callback_function_raises(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
) -> None:
|
||||||
|
"""Test immediate works with a callback function that raises."""
|
||||||
|
calls = []
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _append_and_raise() -> None:
|
||||||
|
calls.append(None)
|
||||||
|
raise RuntimeError("forced_raise")
|
||||||
|
|
||||||
|
debouncer = debounce.Debouncer(
|
||||||
|
hass,
|
||||||
|
_LOGGER,
|
||||||
|
cooldown=0.01,
|
||||||
|
immediate=True,
|
||||||
|
function=_append_and_raise,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Call when nothing happening
|
||||||
|
with pytest.raises(RuntimeError, match="forced_raise"):
|
||||||
|
await debouncer.async_call()
|
||||||
|
assert len(calls) == 1
|
||||||
|
assert debouncer._timer_task is not None
|
||||||
|
assert debouncer._execute_at_end_of_timer is False
|
||||||
|
assert debouncer._job.target == debouncer.function
|
||||||
|
|
||||||
|
# Call when cooldown active setting execute at end to True
|
||||||
|
await debouncer.async_call()
|
||||||
|
assert len(calls) == 1
|
||||||
|
assert debouncer._timer_task is not None
|
||||||
|
assert debouncer._execute_at_end_of_timer is True
|
||||||
|
assert debouncer._job.target == debouncer.function
|
||||||
|
|
||||||
|
# Canceling debounce in cooldown
|
||||||
|
debouncer.async_cancel()
|
||||||
|
assert debouncer._timer_task is None
|
||||||
|
assert debouncer._execute_at_end_of_timer is False
|
||||||
|
assert debouncer._job.target == debouncer.function
|
||||||
|
|
||||||
|
before_job = debouncer._job
|
||||||
|
|
||||||
|
# Call and let timer run out
|
||||||
|
with pytest.raises(RuntimeError, match="forced_raise"):
|
||||||
|
await debouncer.async_call()
|
||||||
|
assert len(calls) == 2
|
||||||
|
async_fire_time_changed(hass, utcnow() + timedelta(seconds=1))
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(calls) == 2
|
||||||
|
assert debouncer._timer_task is None
|
||||||
|
assert debouncer._execute_at_end_of_timer is False
|
||||||
|
assert debouncer._job.target == debouncer.function
|
||||||
|
assert debouncer._job == before_job
|
||||||
|
|
||||||
|
# Test calling doesn't execute/cooldown if currently executing.
|
||||||
|
await debouncer._execute_lock.acquire()
|
||||||
|
await debouncer.async_call()
|
||||||
|
assert len(calls) == 2
|
||||||
|
assert debouncer._timer_task is None
|
||||||
|
assert debouncer._execute_at_end_of_timer is False
|
||||||
|
debouncer._execute_lock.release()
|
||||||
|
assert debouncer._job.target == debouncer.function
|
||||||
|
|
||||||
|
|
||||||
|
async def test_immediate_works_with_passed_coroutine_raises(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
) -> None:
|
||||||
|
"""Test immediate works with a coroutine that raises."""
|
||||||
|
calls = []
|
||||||
|
|
||||||
|
async def _append_and_raise() -> None:
|
||||||
|
calls.append(None)
|
||||||
|
raise RuntimeError("forced_raise")
|
||||||
|
|
||||||
|
debouncer = debounce.Debouncer(
|
||||||
|
hass,
|
||||||
|
_LOGGER,
|
||||||
|
cooldown=0.01,
|
||||||
|
immediate=True,
|
||||||
|
function=_append_and_raise,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Call when nothing happening
|
||||||
|
with pytest.raises(RuntimeError, match="forced_raise"):
|
||||||
|
await debouncer.async_call()
|
||||||
|
assert len(calls) == 1
|
||||||
|
assert debouncer._timer_task is not None
|
||||||
|
assert debouncer._execute_at_end_of_timer is False
|
||||||
|
assert debouncer._job.target == debouncer.function
|
||||||
|
|
||||||
|
# Call when cooldown active setting execute at end to True
|
||||||
|
await debouncer.async_call()
|
||||||
|
assert len(calls) == 1
|
||||||
|
assert debouncer._timer_task is not None
|
||||||
|
assert debouncer._execute_at_end_of_timer is True
|
||||||
|
assert debouncer._job.target == debouncer.function
|
||||||
|
|
||||||
|
# Canceling debounce in cooldown
|
||||||
|
debouncer.async_cancel()
|
||||||
|
assert debouncer._timer_task is None
|
||||||
|
assert debouncer._execute_at_end_of_timer is False
|
||||||
|
assert debouncer._job.target == debouncer.function
|
||||||
|
|
||||||
|
before_job = debouncer._job
|
||||||
|
|
||||||
|
# Call and let timer run out
|
||||||
|
with pytest.raises(RuntimeError, match="forced_raise"):
|
||||||
|
await debouncer.async_call()
|
||||||
|
assert len(calls) == 2
|
||||||
|
async_fire_time_changed(hass, utcnow() + timedelta(seconds=1))
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(calls) == 2
|
||||||
|
assert debouncer._timer_task is None
|
||||||
|
assert debouncer._execute_at_end_of_timer is False
|
||||||
|
assert debouncer._job.target == debouncer.function
|
||||||
|
assert debouncer._job == before_job
|
||||||
|
|
||||||
|
# Test calling doesn't execute/cooldown if currently executing.
|
||||||
|
await debouncer._execute_lock.acquire()
|
||||||
|
await debouncer.async_call()
|
||||||
|
assert len(calls) == 2
|
||||||
|
assert debouncer._timer_task is None
|
||||||
|
assert debouncer._execute_at_end_of_timer is False
|
||||||
|
debouncer._execute_lock.release()
|
||||||
|
assert debouncer._job.target == debouncer.function
|
||||||
|
|
||||||
|
|
||||||
async def test_not_immediate_works(hass: HomeAssistant) -> None:
|
async def test_not_immediate_works(hass: HomeAssistant) -> None:
|
||||||
"""Test immediate works."""
|
"""Test immediate works."""
|
||||||
calls = []
|
calls = []
|
||||||
|
Loading…
x
Reference in New Issue
Block a user