From f6c2fb088c8f0c9f9d358e6c81025778dc36129a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 2 May 2022 14:59:58 +0200 Subject: [PATCH] Stop script if sub-script stops or aborts (#71195) --- homeassistant/helpers/script.py | 10 ++++-- tests/helpers/test_script.py | 61 ++++++++++++++++++++++++++++++++- 2 files changed, 68 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 70f73f065ab..d988b0edd81 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -395,8 +395,14 @@ class _ScriptRun: script_execution_set("finished") except _StopScript: script_execution_set("finished") + # Let the _StopScript bubble up if this is a sub-script + if not self._script.top_level: + raise except _AbortScript: script_execution_set("aborted") + # Let the _AbortScript bubble up if this is a sub-script + if not self._script.top_level: + raise except Exception: script_execution_set("error") raise @@ -1143,7 +1149,7 @@ class Script: hass.bus.async_listen_once( EVENT_HOMEASSISTANT_STOP, partial(_async_stop_scripts_at_shutdown, hass) ) - self._top_level = top_level + self.top_level = top_level if top_level: all_scripts.append( {"instance": self, "started_before_shutdown": not hass.is_stopping} @@ -1431,7 +1437,7 @@ class Script: # If this is a top level Script then make a copy of the variables in case they # are read-only, but more importantly, so as not to leak any variables created # during the run back to the caller. - if self._top_level: + if self.top_level: if self.variables: try: variables = self.variables.async_render( diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index fb3b021daec..b9c968838c9 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -94,7 +94,7 @@ def assert_element(trace_element, expected_element, path): # Check for unexpected items in trace_element assert not set(trace_element._result or {}) - set(expected_result) - if "error_type" in expected_element: + if "error_type" in expected_element and expected_element["error_type"] is not None: assert isinstance(trace_element._error, expected_element["error_type"]) else: assert trace_element._error is None @@ -4485,6 +4485,65 @@ async def test_stop_action(hass, caplog): ) +@pytest.mark.parametrize( + "error,error_type,logmsg,script_execution", + ( + (True, script._AbortScript, "Error", "aborted"), + (False, None, "Stop", "finished"), + ), +) +async def test_stop_action_subscript( + hass, caplog, error, error_type, logmsg, script_execution +): + """Test if automation stops on calling the stop action from a sub-script.""" + event = "test_event" + events = async_capture_events(hass, event) + + alias = "stop step" + sequence = cv.SCRIPT_SCHEMA( + [ + {"event": event}, + { + "if": { + "alias": "if condition", + "condition": "template", + "value_template": "{{ 1 == 1 }}", + }, + "then": { + "alias": alias, + "stop": "In the name of love", + "error": error, + }, + }, + {"event": event}, + ] + ) + script_obj = script.Script(hass, sequence, "Test Name", "test_domain") + + await script_obj.async_run(context=Context()) + await hass.async_block_till_done() + + assert f"{logmsg} script sequence: In the name of love" in caplog.text + caplog.clear() + assert len(events) == 1 + + assert_action_trace( + { + "0": [{"result": {"event": "test_event", "event_data": {}}}], + "1": [{"error_type": error_type, "result": {"choice": "then"}}], + "1/if": [{"result": {"result": True}}], + "1/if/condition/0": [{"result": {"result": True, "entities": []}}], + "1/then/0": [ + { + "error_type": error_type, + "result": {"stop": "In the name of love", "error": error}, + } + ], + }, + expected_script_execution=script_execution, + ) + + async def test_stop_action_with_error(hass, caplog): """Test if automation fails on calling the error action.""" event = "test_event"