From 1931600eac9d62b00efa2897acb735bfafb73851 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 3 May 2022 10:36:58 +0200 Subject: [PATCH] Isolate parallel subscripts (#71233) --- homeassistant/helpers/script.py | 18 ++-- tests/helpers/test_script.py | 146 +++++++++++++++++++++++++++++++- 2 files changed, 157 insertions(+), 7 deletions(-) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index d988b0edd81..3d43e03ddce 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -1133,13 +1133,14 @@ class Script: domain: str, *, # Used in "Running " log message - running_description: str | None = None, change_listener: Callable[..., Any] | None = None, - script_mode: str = DEFAULT_SCRIPT_MODE, - max_runs: int = DEFAULT_MAX, - max_exceeded: str = DEFAULT_MAX_EXCEEDED, - logger: logging.Logger | None = None, + copy_variables: bool = False, log_exceptions: bool = True, + logger: logging.Logger | None = None, + max_exceeded: str = DEFAULT_MAX_EXCEEDED, + max_runs: int = DEFAULT_MAX, + running_description: str | None = None, + script_mode: str = DEFAULT_SCRIPT_MODE, top_level: bool = True, variables: ScriptVariables | None = None, ) -> None: @@ -1192,6 +1193,7 @@ class Script: self._variables_dynamic = template.is_complex(variables) if self._variables_dynamic: template.attach(hass, variables) + self._copy_variables_on_run = copy_variables @property def change_listener(self) -> Callable[..., Any] | None: @@ -1454,7 +1456,10 @@ class Script: variables["context"] = context else: - variables = cast(dict, run_variables) + if self._copy_variables_on_run: + variables = cast(dict, copy(run_variables)) + else: + variables = cast(dict, run_variables) # Prevent non-allowed recursive calls which will cause deadlocks when we try to # stop (restart) or wait for (queued) our own script run. @@ -1671,6 +1676,7 @@ class Script: max_runs=self.max_runs, logger=self._logger, top_level=False, + copy_variables=True, ) parallel_script.change_listener = partial( self._chain_change_listener, parallel_script diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index b9c968838c9..4791dd84cc4 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -3027,7 +3027,7 @@ async def test_parallel(hass: HomeAssistant, caplog: pytest.LogCaptureFixture) - ], "0/parallel/1/sequence/0": [ { - "variables": {"wait": {"remaining": None}}, + "variables": {}, "result": { "event": "test_event", "event_data": {"hello": "from action 2", "what": "world"}, @@ -3047,6 +3047,150 @@ async def test_parallel(hass: HomeAssistant, caplog: pytest.LogCaptureFixture) - assert_action_trace(expected_trace) +async def test_parallel_loop( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test parallel loops do not affect each other.""" + events_loop1 = async_capture_events(hass, "loop1") + events_loop2 = async_capture_events(hass, "loop2") + hass.states.async_set("switch.trigger", "off") + + sequence = cv.SCRIPT_SCHEMA( + { + "parallel": [ + { + "alias": "Loop1", + "sequence": [ + { + "repeat": { + "for_each": ["loop1_a", "loop1_b", "loop1_c"], + "sequence": [ + { + "event": "loop1", + "event_data": {"hello1": "{{ repeat.item }}"}, + } + ], + }, + }, + ], + }, + { + "alias": "Loop2", + "sequence": [ + { + "repeat": { + "for_each": ["loop2_a", "loop2_b", "loop2_c"], + "sequence": [ + { + "event": "loop2", + "event_data": {"hello2": "{{ repeat.item }}"}, + } + ], + }, + }, + ], + }, + ] + } + ) + + script_obj = script.Script(hass, sequence, "Test Name", "test_domain") + + hass.async_create_task( + script_obj.async_run(MappingProxyType({"what": "world"}), Context()) + ) + await hass.async_block_till_done() + + assert len(events_loop1) == 3 + assert events_loop1[0].data["hello1"] == "loop1_a" + assert events_loop1[1].data["hello1"] == "loop1_b" + assert events_loop1[2].data["hello1"] == "loop1_c" + assert events_loop2[0].data["hello2"] == "loop2_a" + assert events_loop2[1].data["hello2"] == "loop2_b" + assert events_loop2[2].data["hello2"] == "loop2_c" + + expected_trace = { + "0": [{"result": {}}], + "0/parallel/0/sequence/0": [{"result": {}}], + "0/parallel/1/sequence/0": [ + { + "result": {}, + } + ], + "0/parallel/0/sequence/0/repeat/sequence/0": [ + { + "variables": { + "repeat": { + "first": True, + "index": 1, + "last": False, + "item": "loop1_a", + } + }, + "result": {"event": "loop1", "event_data": {"hello1": "loop1_a"}}, + }, + { + "variables": { + "repeat": { + "first": False, + "index": 2, + "last": False, + "item": "loop1_b", + } + }, + "result": {"event": "loop1", "event_data": {"hello1": "loop1_b"}}, + }, + { + "variables": { + "repeat": { + "first": False, + "index": 3, + "last": True, + "item": "loop1_c", + } + }, + "result": {"event": "loop1", "event_data": {"hello1": "loop1_c"}}, + }, + ], + "0/parallel/1/sequence/0/repeat/sequence/0": [ + { + "variables": { + "repeat": { + "first": True, + "index": 1, + "last": False, + "item": "loop2_a", + } + }, + "result": {"event": "loop2", "event_data": {"hello2": "loop2_a"}}, + }, + { + "variables": { + "repeat": { + "first": False, + "index": 2, + "last": False, + "item": "loop2_b", + } + }, + "result": {"event": "loop2", "event_data": {"hello2": "loop2_b"}}, + }, + { + "variables": { + "repeat": { + "first": False, + "index": 3, + "last": True, + "item": "loop2_c", + } + }, + "result": {"event": "loop2", "event_data": {"hello2": "loop2_c"}}, + }, + ], + } + assert_action_trace(expected_trace) + + async def test_parallel_error( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: