Skip to content

Commit

Permalink
Ensure scripts with timeouts of zero timeout immediately (#115830)
Browse files Browse the repository at this point in the history
  • Loading branch information
bdraco authored and frenck committed Apr 23, 2024
1 parent 6464218 commit 32f82d4
Show file tree
Hide file tree
Showing 2 changed files with 198 additions and 5 deletions.
25 changes: 20 additions & 5 deletions homeassistant/helpers/script.py
Original file line number Diff line number Diff line change
Expand Up @@ -656,6 +656,12 @@ async def _async_wait_template_step(self):
# check if condition already okay
if condition.async_template(self._hass, wait_template, self._variables, False):
self._variables["wait"]["completed"] = True
self._changed()
return

if timeout == 0:
self._changed()
self._async_handle_timeout()
return

futures, timeout_handle, timeout_future = self._async_futures_with_timeout(
Expand Down Expand Up @@ -1085,6 +1091,11 @@ async def _async_wait_for_trigger_step(self):
self._variables["wait"] = {"remaining": timeout, "trigger": None}
trace_set_result(wait=self._variables["wait"])

if timeout == 0:
self._changed()
self._async_handle_timeout()
return

futures, timeout_handle, timeout_future = self._async_futures_with_timeout(
timeout
)
Expand Down Expand Up @@ -1115,6 +1126,14 @@ def log_cb(level, msg, **kwargs):
futures, timeout_handle, timeout_future, remove_triggers
)

def _async_handle_timeout(self) -> None:
"""Handle timeout."""
self._variables["wait"]["remaining"] = 0.0
if not self._action.get(CONF_CONTINUE_ON_TIMEOUT, True):
self._log(_TIMEOUT_MSG)
trace_set_result(wait=self._variables["wait"], timeout=True)
raise _AbortScript from TimeoutError()

async def _async_wait_with_optional_timeout(
self,
futures: list[asyncio.Future[None]],
Expand All @@ -1125,11 +1144,7 @@ async def _async_wait_with_optional_timeout(
try:
await asyncio.wait(futures, return_when=asyncio.FIRST_COMPLETED)
if timeout_future and timeout_future.done():
self._variables["wait"]["remaining"] = 0.0
if not self._action.get(CONF_CONTINUE_ON_TIMEOUT, True):
self._log(_TIMEOUT_MSG)
trace_set_result(wait=self._variables["wait"], timeout=True)
raise _AbortScript from TimeoutError()
self._async_handle_timeout()
finally:
if timeout_future and not timeout_future.done() and timeout_handle:
timeout_handle.cancel()
Expand Down
178 changes: 178 additions & 0 deletions tests/helpers/test_script.py
Original file line number Diff line number Diff line change
Expand Up @@ -1311,6 +1311,184 @@ async def test_wait_timeout(
assert_action_trace(expected_trace)


@pytest.mark.parametrize(
"timeout_param", [0, "{{ 0 }}", {"minutes": 0}, {"minutes": "{{ 0 }}"}]
)
async def test_wait_trigger_with_zero_timeout(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture, timeout_param: int | str
) -> None:
"""Test the wait trigger with zero timeout option."""
event = "test_event"
events = async_capture_events(hass, event)
action = {
"wait_for_trigger": {
"platform": "state",
"entity_id": "switch.test",
"to": "off",
}
}
action["timeout"] = timeout_param
action["continue_on_timeout"] = True
sequence = cv.SCRIPT_SCHEMA([action, {"event": event}])
sequence = await script.async_validate_actions_config(hass, sequence)
script_obj = script.Script(hass, sequence, "Test Name", "test_domain")
wait_started_flag = async_watch_for_action(script_obj, "wait")
hass.states.async_set("switch.test", "on")
hass.async_create_task(script_obj.async_run(context=Context()))

try:
await asyncio.wait_for(wait_started_flag.wait(), 1)
except (AssertionError, TimeoutError):
await script_obj.async_stop()
raise

assert not script_obj.is_running
assert len(events) == 1
assert "(timeout: 0:00:00)" in caplog.text

variable_wait = {"wait": {"trigger": None, "remaining": 0.0}}
expected_trace = {
"0": [
{
"result": variable_wait,
"variables": variable_wait,
}
],
"1": [{"result": {"event": "test_event", "event_data": {}}}],
}
assert_action_trace(expected_trace)


@pytest.mark.parametrize(
"timeout_param", [0, "{{ 0 }}", {"minutes": 0}, {"minutes": "{{ 0 }}"}]
)
async def test_wait_trigger_matches_with_zero_timeout(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture, timeout_param: int | str
) -> None:
"""Test the wait trigger that matches with zero timeout option."""
event = "test_event"
events = async_capture_events(hass, event)
action = {
"wait_for_trigger": {
"platform": "state",
"entity_id": "switch.test",
"to": "off",
}
}
action["timeout"] = timeout_param
action["continue_on_timeout"] = True
sequence = cv.SCRIPT_SCHEMA([action, {"event": event}])
sequence = await script.async_validate_actions_config(hass, sequence)
script_obj = script.Script(hass, sequence, "Test Name", "test_domain")
wait_started_flag = async_watch_for_action(script_obj, "wait")
hass.states.async_set("switch.test", "off")
hass.async_create_task(script_obj.async_run(context=Context()))

try:
await asyncio.wait_for(wait_started_flag.wait(), 1)
except (AssertionError, TimeoutError):
await script_obj.async_stop()
raise

assert not script_obj.is_running
assert len(events) == 1
assert "(timeout: 0:00:00)" in caplog.text

variable_wait = {"wait": {"trigger": None, "remaining": 0.0}}
expected_trace = {
"0": [
{
"result": variable_wait,
"variables": variable_wait,
}
],
"1": [{"result": {"event": "test_event", "event_data": {}}}],
}
assert_action_trace(expected_trace)


@pytest.mark.parametrize(
"timeout_param", [0, "{{ 0 }}", {"minutes": 0}, {"minutes": "{{ 0 }}"}]
)
async def test_wait_template_with_zero_timeout(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture, timeout_param: int | str
) -> None:
"""Test the wait template with zero timeout option."""
event = "test_event"
events = async_capture_events(hass, event)
action = {"wait_template": "{{ states.switch.test.state == 'off' }}"}
action["timeout"] = timeout_param
action["continue_on_timeout"] = True
sequence = cv.SCRIPT_SCHEMA([action, {"event": event}])
sequence = await script.async_validate_actions_config(hass, sequence)
script_obj = script.Script(hass, sequence, "Test Name", "test_domain")
wait_started_flag = async_watch_for_action(script_obj, "wait")
hass.states.async_set("switch.test", "on")
hass.async_create_task(script_obj.async_run(context=Context()))

try:
await asyncio.wait_for(wait_started_flag.wait(), 1)
except (AssertionError, TimeoutError):
await script_obj.async_stop()
raise

assert not script_obj.is_running
assert len(events) == 1
assert "(timeout: 0:00:00)" in caplog.text
variable_wait = {"wait": {"completed": False, "remaining": 0.0}}
expected_trace = {
"0": [
{
"result": variable_wait,
"variables": variable_wait,
}
],
"1": [{"result": {"event": "test_event", "event_data": {}}}],
}
assert_action_trace(expected_trace)


@pytest.mark.parametrize(
"timeout_param", [0, "{{ 0 }}", {"minutes": 0}, {"minutes": "{{ 0 }}"}]
)
async def test_wait_template_matches_with_zero_timeout(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture, timeout_param: int | str
) -> None:
"""Test the wait template that matches with zero timeout option."""
event = "test_event"
events = async_capture_events(hass, event)
action = {"wait_template": "{{ states.switch.test.state == 'off' }}"}
action["timeout"] = timeout_param
action["continue_on_timeout"] = True
sequence = cv.SCRIPT_SCHEMA([action, {"event": event}])
sequence = await script.async_validate_actions_config(hass, sequence)
script_obj = script.Script(hass, sequence, "Test Name", "test_domain")
wait_started_flag = async_watch_for_action(script_obj, "wait")
hass.states.async_set("switch.test", "off")
hass.async_create_task(script_obj.async_run(context=Context()))

try:
await asyncio.wait_for(wait_started_flag.wait(), 1)
except (AssertionError, TimeoutError):
await script_obj.async_stop()
raise

assert not script_obj.is_running
assert len(events) == 1
assert "(timeout: 0:00:00)" in caplog.text
variable_wait = {"wait": {"completed": True, "remaining": 0.0}}
expected_trace = {
"0": [
{
"result": variable_wait,
"variables": variable_wait,
}
],
"1": [{"result": {"event": "test_event", "event_data": {}}}],
}
assert_action_trace(expected_trace)


@pytest.mark.parametrize(
("continue_on_timeout", "n_events"), [(False, 0), (True, 1), (None, 1)]
)
Expand Down

0 comments on commit 32f82d4

Please sign in to comment.