diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 1824bb9e2d6..4cc2c1975e8 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -427,11 +427,12 @@ class _ScriptRun: delay = delay.total_seconds() self._changed() + trace_set_result(delay=delay, done=False) try: async with async_timeout.timeout(delay): await self._stop.wait() except asyncio.TimeoutError: - pass + trace_set_result(delay=delay, done=True) async def _async_wait_template_step(self): """Handle a wait template.""" @@ -443,6 +444,7 @@ class _ScriptRun: self._step_log("wait template", timeout) self._variables["wait"] = {"remaining": timeout, "completed": False} + trace_set_result(wait=self._variables["wait"]) wait_template = self._action[CONF_WAIT_TEMPLATE] wait_template.hass = self._hass @@ -455,10 +457,9 @@ class _ScriptRun: @callback def async_script_wait(entity_id, from_s, to_s): """Handle script after template condition is true.""" - self._variables["wait"] = { - "remaining": to_context.remaining if to_context else timeout, - "completed": True, - } + wait_var = self._variables["wait"] + wait_var["remaining"] = to_context.remaining if to_context else timeout + wait_var["completed"] = True done.set() to_context = None @@ -475,10 +476,11 @@ class _ScriptRun: async with async_timeout.timeout(timeout) as to_context: await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED) except asyncio.TimeoutError as ex: + 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 _StopScript from ex - self._variables["wait"]["remaining"] = 0.0 finally: for task in tasks: task.cancel() @@ -539,6 +541,7 @@ class _ScriptRun: else: limit = SERVICE_CALL_LIMIT + trace_set_result(params=params, running_script=running_script, limit=limit) service_task = self._hass.async_create_task( self._hass.services.async_call( **params, @@ -567,6 +570,7 @@ class _ScriptRun: async def _async_scene_step(self): """Activate the scene specified in the action.""" self._step_log("activate scene") + trace_set_result(scene=self._action[CONF_SCENE]) await self._hass.services.async_call( scene.DOMAIN, SERVICE_TURN_ON, @@ -592,6 +596,7 @@ class _ScriptRun: "Error rendering event data template: %s", ex, level=logging.ERROR ) + trace_set_result(event=self._action[CONF_EVENT], event_data=event_data) self._hass.bus.async_fire( self._action[CONF_EVENT], event_data, context=self._context ) @@ -748,14 +753,14 @@ class _ScriptRun: variables = {**self._variables} self._variables["wait"] = {"remaining": timeout, "trigger": None} + trace_set_result(wait=self._variables["wait"]) done = asyncio.Event() async def async_done(variables, context=None): - self._variables["wait"] = { - "remaining": to_context.remaining if to_context else timeout, - "trigger": variables["trigger"], - } + wait_var = self._variables["wait"] + wait_var["remaining"] = to_context.remaining if to_context else timeout + wait_var["trigger"] = variables["trigger"] done.set() def log_cb(level, msg, **kwargs): @@ -782,10 +787,11 @@ class _ScriptRun: async with async_timeout.timeout(timeout) as to_context: await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED) except asyncio.TimeoutError as ex: + 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 _StopScript from ex - self._variables["wait"]["remaining"] = 0.0 finally: for task in tasks: task.cancel() diff --git a/tests/components/automation/test_websocket_api.py b/tests/components/automation/test_websocket_api.py index ab69e89416a..6f630c27470 100644 --- a/tests/components/automation/test_websocket_api.py +++ b/tests/components/automation/test_websocket_api.py @@ -50,6 +50,18 @@ async def test_get_automation_trace(hass, hass_ws_client): "action": {"event": "another_event"}, } + sun_action = { + "limit": 10, + "params": { + "domain": "test", + "service": "automation", + "service_data": {}, + "target": {}, + }, + "running_script": False, + } + moon_action = {"event": "another_event", "event_data": {}} + assert await async_setup_component( hass, "automation", @@ -94,7 +106,7 @@ async def test_get_automation_trace(hass, hass_ws_client): assert len(trace["action_trace"]) == 1 assert len(trace["action_trace"]["action/0"]) == 1 assert trace["action_trace"]["action/0"][0]["error"] - assert "result" not in trace["action_trace"]["action/0"][0] + assert trace["action_trace"]["action/0"][0]["result"] == sun_action assert trace["condition_trace"] == {} assert trace["config"] == sun_config assert trace["context"] @@ -133,7 +145,7 @@ async def test_get_automation_trace(hass, hass_ws_client): assert len(trace["action_trace"]) == 1 assert len(trace["action_trace"]["action/0"]) == 1 assert "error" not in trace["action_trace"]["action/0"][0] - assert "result" not in trace["action_trace"]["action/0"][0] + assert trace["action_trace"]["action/0"][0]["result"] == moon_action assert len(trace["condition_trace"]) == 1 assert len(trace["condition_trace"]["condition/0"]) == 1 assert trace["condition_trace"]["condition/0"][0]["result"] == {"result": True} @@ -212,7 +224,7 @@ async def test_get_automation_trace(hass, hass_ws_client): assert len(trace["action_trace"]) == 1 assert len(trace["action_trace"]["action/0"]) == 1 assert "error" not in trace["action_trace"]["action/0"][0] - assert "result" not in trace["action_trace"]["action/0"][0] + assert trace["action_trace"]["action/0"][0]["result"] == moon_action assert len(trace["condition_trace"]) == 1 assert len(trace["condition_trace"]["condition/0"]) == 1 assert trace["condition_trace"]["condition/0"][0]["result"] == {"result": True} diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index 8a93d769775..917fc64b0e7 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -16,7 +16,8 @@ import voluptuous as vol from homeassistant import exceptions import homeassistant.components.scene as scene from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_ON -from homeassistant.core import Context, CoreState, callback +from homeassistant.core import SERVICE_CALL_LIMIT, Context, CoreState, callback +from homeassistant.exceptions import ConditionError, ServiceNotFound from homeassistant.helpers import config_validation as cv, script, trace from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.setup import async_setup_component @@ -31,18 +32,59 @@ from tests.common import ( ENTITY_ID = "script.test" +@pytest.fixture(autouse=True) +def prepare_tracing(): + """Prepare tracing.""" + trace.trace_get() + + +def compare_trigger_item(actual_trigger, expected_trigger): + """Compare trigger data description.""" + assert actual_trigger["description"] == expected_trigger["description"] + + +def compare_result_item(key, actual, expected): + """Compare an item in the result dict.""" + if key == "wait" and (expected.get("trigger") is not None): + assert "trigger" in actual + expected_trigger = expected.pop("trigger") + actual_trigger = actual.pop("trigger") + compare_trigger_item(actual_trigger, expected_trigger) + + assert actual == expected + + def assert_element(trace_element, expected_element, path): """Assert a trace element is as expected. Note: Unused variable 'path' is passed to get helpful errors from pytest. """ - for result_key, result in expected_element.get("result", {}).items(): + expected_result = expected_element.get("result", {}) + + # Check that every item in expected_element is present and equal in trace_element + # The redundant set operation gives helpful errors from pytest + assert not set(expected_result) - set(trace_element._result or {}) + for result_key, result in expected_result.items(): + compare_result_item(result_key, trace_element._result[result_key], result) assert trace_element._result[result_key] == result + + # Check for unexpected items in trace_element + assert not set(trace_element._result or {}) - set(expected_result) + if "error_type" in expected_element: assert isinstance(trace_element._error, expected_element["error_type"]) else: assert trace_element._error is None + # Don't check variables when script starts + if trace_element.path == "0": + return + + if "variables" in expected_element: + assert expected_element["variables"] == trace_element._variables + else: + assert not trace_element._variables + def assert_action_trace(expected): """Assert a trace condition sequence is as expected.""" @@ -82,9 +124,6 @@ async def test_firing_event_basic(hass, caplog): {"alias": alias, "event": event, "event_data": {"hello": "world"}} ) - # Prepare tracing - trace.trace_get() - script_obj = script.Script( hass, sequence, @@ -102,9 +141,12 @@ async def test_firing_event_basic(hass, caplog): assert ".test_name:" in caplog.text assert "Test Name: Running test script" in caplog.text assert f"Executing step {alias}" in caplog.text + assert_action_trace( { - "0": [{}], + "0": [ + {"result": {"event": "test_event", "event_data": {"hello": "world"}}}, + ], } ) @@ -150,6 +192,24 @@ async def test_firing_event_template(hass): "list2": ["yes", "yesyes"], } + assert_action_trace( + { + "0": [ + { + "result": { + "event": "test_event", + "event_data": { + "dict": {1: "yes", 2: "yesyes", 3: "yesyesyes"}, + "dict2": {1: "yes", 2: "yesyes", 3: "yesyesyes"}, + "list": ["yes", "yesyes"], + "list2": ["yes", "yesyes"], + }, + } + } + ], + } + ) + async def test_calling_service_basic(hass, caplog): """Test the calling of a service.""" @@ -170,6 +230,25 @@ async def test_calling_service_basic(hass, caplog): assert calls[0].data.get("hello") == "world" assert f"Executing step {alias}" in caplog.text + assert_action_trace( + { + "0": [ + { + "result": { + "limit": SERVICE_CALL_LIMIT, + "params": { + "domain": "test", + "service": "script", + "service_data": {"hello": "world"}, + "target": {}, + }, + "running_script": False, + } + } + ], + } + ) + async def test_calling_service_template(hass): """Test the calling of a service.""" @@ -204,6 +283,25 @@ async def test_calling_service_template(hass): assert calls[0].context is context assert calls[0].data.get("hello") == "world" + assert_action_trace( + { + "0": [ + { + "result": { + "limit": SERVICE_CALL_LIMIT, + "params": { + "domain": "test", + "service": "script", + "service_data": {"hello": "world"}, + "target": {}, + }, + "running_script": False, + } + } + ], + } + ) + async def test_data_template_with_templated_key(hass): """Test the calling of a service with a data_template with a templated key.""" @@ -222,7 +320,26 @@ async def test_data_template_with_templated_key(hass): assert len(calls) == 1 assert calls[0].context is context - assert "hello" in calls[0].data + assert calls[0].data.get("hello") == "world" + + assert_action_trace( + { + "0": [ + { + "result": { + "limit": SERVICE_CALL_LIMIT, + "params": { + "domain": "test", + "service": "script", + "service_data": {"hello": "world"}, + "target": {}, + }, + "running_script": False, + } + } + ], + } + ) async def test_multiple_runs_no_wait(hass): @@ -315,6 +432,12 @@ async def test_activating_scene(hass, caplog): assert calls[0].data.get(ATTR_ENTITY_ID) == "scene.hello" assert f"Executing step {alias}" in caplog.text + assert_action_trace( + { + "0": [{"result": {"scene": "scene.hello"}}], + } + ) + @pytest.mark.parametrize("count", [1, 3]) async def test_stop_no_wait(hass, count): @@ -390,6 +513,12 @@ async def test_delay_basic(hass): assert not script_obj.is_running assert script_obj.last_action is None + assert_action_trace( + { + "0": [{"result": {"delay": 5.0, "done": True}}], + } + ) + async def test_multiple_runs_delay(hass): """Test multiple runs with delay in script.""" @@ -454,6 +583,12 @@ async def test_delay_template_ok(hass): assert not script_obj.is_running + assert_action_trace( + { + "0": [{"result": {"delay": 5.0, "done": True}}], + } + ) + async def test_delay_template_invalid(hass, caplog): """Test the delay as a template that fails.""" @@ -481,6 +616,13 @@ async def test_delay_template_invalid(hass, caplog): assert not script_obj.is_running assert len(events) == 1 + assert_action_trace( + { + "0": [{"result": {"event": "test_event", "event_data": {}}}], + "1": [{"error_type": script._StopScript}], + } + ) + async def test_delay_template_complex_ok(hass): """Test the delay with a working complex template.""" @@ -501,6 +643,12 @@ async def test_delay_template_complex_ok(hass): assert not script_obj.is_running + assert_action_trace( + { + "0": [{"result": {"delay": 5.0, "done": True}}], + } + ) + async def test_delay_template_complex_invalid(hass, caplog): """Test the delay with a complex template that fails.""" @@ -528,6 +676,13 @@ async def test_delay_template_complex_invalid(hass, caplog): assert not script_obj.is_running assert len(events) == 1 + assert_action_trace( + { + "0": [{"result": {"event": "test_event", "event_data": {}}}], + "1": [{"error_type": script._StopScript}], + } + ) + async def test_cancel_delay(hass): """Test the cancelling while the delay is present.""" @@ -559,6 +714,12 @@ async def test_cancel_delay(hass): assert not script_obj.is_running assert len(events) == 0 + assert_action_trace( + { + "0": [{"result": {"delay": 5.0, "done": False}}], + } + ) + @pytest.mark.parametrize("action_type", ["template", "trigger"]) async def test_wait_basic(hass, action_type): @@ -594,6 +755,28 @@ async def test_wait_basic(hass, action_type): assert not script_obj.is_running assert script_obj.last_action is None + if action_type == "template": + assert_action_trace( + { + "0": [{"result": {"wait": {"completed": True, "remaining": None}}}], + } + ) + else: + assert_action_trace( + { + "0": [ + { + "result": { + "wait": { + "trigger": {"description": "state of switch.test"}, + "remaining": None, + } + } + } + ], + } + ) + async def test_wait_for_trigger_variables(hass): """Test variables are passed to wait_for_trigger action.""" @@ -672,6 +855,19 @@ async def test_wait_basic_times_out(hass, action_type): assert timed_out + if action_type == "template": + assert_action_trace( + { + "0": [{"result": {"wait": {"completed": False, "remaining": None}}}], + } + ) + else: + assert_action_trace( + { + "0": [{"result": {"wait": {"trigger": None, "remaining": None}}}], + } + ) + @pytest.mark.parametrize("action_type", ["template", "trigger"]) async def test_multiple_runs_wait(hass, action_type): @@ -769,6 +965,19 @@ async def test_cancel_wait(hass, action_type): assert not script_obj.is_running assert len(events) == 0 + if action_type == "template": + assert_action_trace( + { + "0": [{"result": {"wait": {"completed": False, "remaining": None}}}], + } + ) + else: + assert_action_trace( + { + "0": [{"result": {"wait": {"trigger": None, "remaining": None}}}], + } + ) + async def test_wait_template_not_schedule(hass): """Test the wait template with correct condition.""" @@ -790,6 +999,19 @@ async def test_wait_template_not_schedule(hass): assert not script_obj.is_running assert len(events) == 2 + assert_action_trace( + { + "0": [{"result": {"event": "test_event", "event_data": {}}}], + "1": [{"result": {"wait": {"completed": True, "remaining": None}}}], + "2": [ + { + "result": {"event": "test_event", "event_data": {}}, + "variables": {"wait": {"completed": True, "remaining": None}}, + } + ], + } + ) + @pytest.mark.parametrize( "timeout_param", [5, "{{ 5 }}", {"seconds": 5}, {"seconds": "{{ 5 }}"}] @@ -839,6 +1061,21 @@ async def test_wait_timeout(hass, caplog, timeout_param, action_type): assert len(events) == 1 assert "(timeout: 0:00:05)" in caplog.text + if action_type == "template": + variable_wait = {"wait": {"completed": False, "remaining": 0.0}} + else: + variable_wait = {"wait": {"trigger": None, "remaining": 0.0}} + expected_trace = { + "0": [{"result": variable_wait}], + "1": [ + { + "result": {"event": "test_event", "event_data": {}}, + "variables": variable_wait, + } + ], + } + assert_action_trace(expected_trace) + @pytest.mark.parametrize( "continue_on_timeout,n_events", [(False, 0), (True, 1), (None, 1)] @@ -884,6 +1121,25 @@ async def test_wait_continue_on_timeout( assert not script_obj.is_running assert len(events) == n_events + if action_type == "template": + variable_wait = {"wait": {"completed": False, "remaining": 0.0}} + else: + variable_wait = {"wait": {"trigger": None, "remaining": 0.0}} + expected_trace = { + "0": [{"result": variable_wait}], + } + if continue_on_timeout is False: + expected_trace["0"][0]["result"]["timeout"] = True + expected_trace["0"][0]["error_type"] = script._StopScript + else: + expected_trace["1"] = [ + { + "result": {"event": "test_event", "event_data": {}}, + "variables": variable_wait, + } + ] + assert_action_trace(expected_trace) + async def test_wait_template_variables_in(hass): """Test the wait template with input variables.""" @@ -908,6 +1164,12 @@ async def test_wait_template_variables_in(hass): assert not script_obj.is_running + assert_action_trace( + { + "0": [{"result": {"wait": {"completed": True, "remaining": None}}}], + } + ) + async def test_wait_template_with_utcnow(hass): """Test the wait template with utcnow.""" @@ -933,6 +1195,12 @@ async def test_wait_template_with_utcnow(hass): await hass.async_block_till_done() assert not script_obj.is_running + assert_action_trace( + { + "0": [{"result": {"wait": {"completed": True, "remaining": None}}}], + } + ) + async def test_wait_template_with_utcnow_no_match(hass): """Test the wait template with utcnow that does not match.""" @@ -963,6 +1231,12 @@ async def test_wait_template_with_utcnow_no_match(hass): assert timed_out + assert_action_trace( + { + "0": [{"result": {"wait": {"completed": False, "remaining": None}}}], + } + ) + @pytest.mark.parametrize("mode", ["no_timeout", "timeout_finish", "timeout_not_finish"]) @pytest.mark.parametrize("action_type", ["template", "trigger"]) @@ -1056,6 +1330,12 @@ async def test_wait_for_trigger_bad(hass, caplog): assert "Unknown error while setting up trigger" in caplog.text + assert_action_trace( + { + "0": [{"result": {"wait": {"trigger": None, "remaining": None}}}], + } + ) + async def test_wait_for_trigger_generated_exception(hass, caplog): """Test bad wait_for_trigger.""" @@ -1082,6 +1362,12 @@ async def test_wait_for_trigger_generated_exception(hass, caplog): assert "ValueError" in caplog.text assert "something bad" in caplog.text + assert_action_trace( + { + "0": [{"result": {"wait": {"trigger": None, "remaining": None}}}], + } + ) + async def test_condition_warning(hass, caplog): """Test warning on condition.""" @@ -1112,6 +1398,15 @@ async def test_condition_warning(hass, caplog): assert len(events) == 1 + assert_action_trace( + { + "0": [{"result": {"event": "test_event", "event_data": {}}}], + "1": [{"error_type": script._StopScript, "result": {"result": False}}], + "1/condition": [{"error_type": ConditionError}], + "1/condition/entity_id/0": [{"error_type": ConditionError}], + } + ) + async def test_condition_basic(hass, caplog): """Test if we can use conditions in a script.""" @@ -1139,6 +1434,15 @@ async def test_condition_basic(hass, caplog): caplog.clear() assert len(events) == 2 + assert_action_trace( + { + "0": [{"result": {"event": "test_event", "event_data": {}}}], + "1": [{"result": {"result": True}}], + "1/condition": [{"result": {"result": True}}], + "2": [{"result": {"event": "test_event", "event_data": {}}}], + } + ) + hass.states.async_set("test.entity", "goodbye") await script_obj.async_run(context=Context()) @@ -1147,6 +1451,14 @@ async def test_condition_basic(hass, caplog): assert f"Test condition {alias}: False" in caplog.text assert len(events) == 3 + assert_action_trace( + { + "0": [{"result": {"event": "test_event", "event_data": {}}}], + "1": [{"error_type": script._StopScript, "result": {"result": False}}], + "1/condition": [{"result": {"result": False}}], + } + ) + @patch("homeassistant.helpers.script.condition.async_from_config") async def test_condition_created_once(async_from_config, hass): @@ -1219,9 +1531,6 @@ async def test_repeat_count(hass, caplog, count): } ) - # Prepare tracing - trace.trace_get() - script_obj = script.Script(hass, sequence, "Test Name", "test_domain") await script_obj.async_run(context=Context()) @@ -1233,10 +1542,31 @@ async def test_repeat_count(hass, caplog, count): assert event.data.get("index") == index + 1 assert event.data.get("last") == (index == count - 1) assert caplog.text.count(f"Repeating {alias}") == count + first_index = max(1, count - script.ACTION_TRACE_NODE_MAX_LEN + 1) + last_index = count + 1 assert_action_trace( { "0": [{}], - "0/repeat/sequence/0": [{}] * min(count, script.ACTION_TRACE_NODE_MAX_LEN), + "0/repeat/sequence/0": [ + { + "result": { + "event": "test_event", + "event_data": { + "first": index == 1, + "index": index, + "last": index == count, + }, + }, + "variables": { + "repeat": { + "first": index == 1, + "index": index, + "last": index == count, + } + }, + } + for index in range(first_index, last_index) + ], } ) @@ -1281,6 +1611,26 @@ async def test_repeat_condition_warning(hass, caplog, condition): assert len(events) == count + expected_trace = {"0": [{}]} + if condition == "until": + expected_trace["0/repeat/sequence/0"] = [ + { + "result": {"event": "test_event", "event_data": {}}, + "variables": {"repeat": {"first": True, "index": 1}}, + } + ] + expected_trace["0/repeat"] = [ + { + "result": {"result": None}, + "variables": {"repeat": {"first": True, "index": 1}}, + } + ] + expected_trace[f"0/repeat/{condition}/0"] = [{"error_type": ConditionError}] + expected_trace[f"0/repeat/{condition}/0/entity_id/0"] = [ + {"error_type": ConditionError} + ] + assert_action_trace(expected_trace) + @pytest.mark.parametrize("condition", ["while", "until"]) @pytest.mark.parametrize("direct_template", [False, True]) @@ -1364,15 +1714,14 @@ async def test_repeat_var_in_condition(hass, condition): sequence = {"repeat": {"sequence": {"event": event}}} if condition == "while": - sequence["repeat"]["while"] = { - "condition": "template", - "value_template": "{{ repeat.index <= 2 }}", - } + value_template = "{{ repeat.index <= 2 }}" else: - sequence["repeat"]["until"] = { - "condition": "template", - "value_template": "{{ repeat.index == 2 }}", - } + value_template = "{{ repeat.index == 2 }}" + sequence["repeat"][condition] = { + "condition": "template", + "value_template": value_template, + } + script_obj = script.Script( hass, cv.SCRIPT_SCHEMA(sequence), "Test Name", "test_domain" ) @@ -1385,6 +1734,63 @@ async def test_repeat_var_in_condition(hass, condition): assert len(events) == 2 + if condition == "while": + expected_trace = { + "0": [{}], + "0/repeat": [ + { + "result": {"result": True}, + "variables": {"repeat": {"first": True, "index": 1}}, + }, + { + "result": {"result": True}, + "variables": {"repeat": {"first": False, "index": 2}}, + }, + { + "result": {"result": False}, + "variables": {"repeat": {"first": False, "index": 3}}, + }, + ], + "0/repeat/while/0": [ + {"result": {"result": True}}, + {"result": {"result": True}}, + {"result": {"result": False}}, + ], + "0/repeat/sequence/0": [ + {"result": {"event": "test_event", "event_data": {}}} + ] + * 2, + } + else: + expected_trace = { + "0": [{}], + "0/repeat/sequence/0": [ + { + "result": {"event": "test_event", "event_data": {}}, + "variables": {"repeat": {"first": True, "index": 1}}, + }, + { + "result": {"event": "test_event", "event_data": {}}, + "variables": {"repeat": {"first": False, "index": 2}}, + }, + ], + "0/repeat": [ + { + "result": {"result": False}, + "variables": {"repeat": {"first": True, "index": 1}}, + }, + { + "result": {"result": True}, + "variables": {"repeat": {"first": False, "index": 2}}, + }, + ], + "0/repeat/until/0": [ + {"result": {"result": False}}, + {"result": {"result": True}}, + ], + } + assert_action_trace(expected_trace) + @pytest.mark.parametrize( "variables,first_last,inside_x", @@ -1486,6 +1892,49 @@ async def test_repeat_nested(hass, variables, first_last, inside_x): "x": result[3], } + event_data1 = {"repeat": None, "x": inside_x} + event_data2 = [ + {"first": True, "index": 1, "last": False, "x": inside_x}, + {"first": False, "index": 2, "last": True, "x": inside_x}, + ] + variable_repeat = [ + {"repeat": {"first": True, "index": 1, "last": False}}, + {"repeat": {"first": False, "index": 2, "last": True}}, + ] + expected_trace = { + "0": [{"result": {"event": "test_event", "event_data": event_data1}}], + "1": [{}], + "1/repeat/sequence/0": [ + { + "result": {"event": "test_event", "event_data": event_data2[0]}, + "variables": variable_repeat[0], + }, + { + "result": {"event": "test_event", "event_data": event_data2[1]}, + "variables": variable_repeat[1], + }, + ], + "1/repeat/sequence/1": [{}, {}], + "1/repeat/sequence/1/repeat/sequence/0": [ + {"result": {"event": "test_event", "event_data": event_data2[0]}}, + { + "result": {"event": "test_event", "event_data": event_data2[1]}, + "variables": variable_repeat[1], + }, + { + "result": {"event": "test_event", "event_data": event_data2[0]}, + "variables": variable_repeat[0], + }, + {"result": {"event": "test_event", "event_data": event_data2[1]}}, + ], + "1/repeat/sequence/2": [ + {"result": {"event": "test_event", "event_data": event_data2[0]}}, + {"result": {"event": "test_event", "event_data": event_data2[1]}}, + ], + "2": [{"result": {"event": "test_event", "event_data": event_data1}}], + } + assert_action_trace(expected_trace) + async def test_choose_warning(hass, caplog): """Test warning on choose.""" @@ -1578,9 +2027,6 @@ async def test_choose(hass, caplog, var, result): } ) - # Prepare tracing - trace.trace_get() - script_obj = script.Script(hass, sequence, "Test Name", "test_domain") await script_obj.async_run(MappingProxyType({"var": var}), Context()) @@ -1593,19 +2039,29 @@ async def test_choose(hass, caplog, var, result): expected_choice = "default" assert f"{alias}: {expected_choice}: Executing step {aliases[var]}" in caplog.text - expected_trace = {"0": [{}]} - if var >= 1: - expected_trace["0/choose/0"] = [{}] - expected_trace["0/choose/0/conditions/0"] = [{}] - if var >= 2: - expected_trace["0/choose/1"] = [{}] - expected_trace["0/choose/1/conditions/0"] = [{}] - if var == 1: - expected_trace["0/choose/0/sequence/0"] = [{}] - if var == 2: - expected_trace["0/choose/1/sequence/0"] = [{}] + expected_choice = var - 1 if var == 3: - expected_trace["0/default/sequence/0"] = [{}] + expected_choice = "default" + + expected_trace = {"0": [{"result": {"choice": expected_choice}}]} + if var >= 1: + expected_trace["0/choose/0"] = [{"result": {"result": var == 1}}] + expected_trace["0/choose/0/conditions/0"] = [{"result": {"result": var == 1}}] + if var >= 2: + expected_trace["0/choose/1"] = [{"result": {"result": var == 2}}] + expected_trace["0/choose/1/conditions/0"] = [{"result": {"result": var == 2}}] + if var == 1: + expected_trace["0/choose/0/sequence/0"] = [ + {"result": {"event": "test_event", "event_data": {"choice": "first"}}} + ] + if var == 2: + expected_trace["0/choose/1/sequence/0"] = [ + {"result": {"event": "test_event", "event_data": {"choice": "second"}}} + ] + if var == 3: + expected_trace["0/default/sequence/0"] = [ + {"result": {"event": "test_event", "event_data": {"choice": "default"}}} + ] assert_action_trace(expected_trace) @@ -1668,6 +2124,25 @@ async def test_propagate_error_service_not_found(hass): assert len(events) == 0 assert not script_obj.is_running + expected_trace = { + "0": [ + { + "error_type": ServiceNotFound, + "result": { + "limit": 10, + "params": { + "domain": "test", + "service": "script", + "service_data": {}, + "target": {}, + }, + "running_script": False, + }, + } + ], + } + assert_action_trace(expected_trace) + async def test_propagate_error_invalid_service_data(hass): """Test that a script aborts when we send invalid service data.""" @@ -1686,6 +2161,25 @@ async def test_propagate_error_invalid_service_data(hass): assert len(calls) == 0 assert not script_obj.is_running + expected_trace = { + "0": [ + { + "error_type": vol.MultipleInvalid, + "result": { + "limit": 10, + "params": { + "domain": "test", + "service": "script", + "service_data": {"text": 1}, + "target": {}, + }, + "running_script": False, + }, + } + ], + } + assert_action_trace(expected_trace) + async def test_propagate_error_service_exception(hass): """Test that a script aborts when a service throws an exception.""" @@ -1708,6 +2202,25 @@ async def test_propagate_error_service_exception(hass): assert len(events) == 0 assert not script_obj.is_running + expected_trace = { + "0": [ + { + "error_type": ValueError, + "result": { + "limit": 10, + "params": { + "domain": "test", + "service": "script", + "service_data": {}, + "target": {}, + }, + "running_script": False, + }, + } + ], + } + assert_action_trace(expected_trace) + async def test_referenced_entities(hass): """Test referenced entities.""" @@ -2151,6 +2664,11 @@ async def test_shutdown_at(hass, caplog): assert not script_obj.is_running assert "Stopping scripts running at shutdown: test script" in caplog.text + expected_trace = { + "0": [{"result": {"delay": 120.0, "done": False}}], + } + assert_action_trace(expected_trace) + async def test_shutdown_after(hass, caplog): """Test stopping scripts at shutdown.""" @@ -2182,6 +2700,11 @@ async def test_shutdown_after(hass, caplog): in caplog.text ) + expected_trace = { + "0": [{"result": {"delay": 120.0, "done": False}}], + } + assert_action_trace(expected_trace) + async def test_update_logger(hass, caplog): """Test updating logger.""" @@ -2240,6 +2763,26 @@ async def test_set_variable(hass, caplog): assert mock_calls[0].data["value"] == "value" assert f"Executing step {alias}" in caplog.text + expected_trace = { + "0": [{}], + "1": [ + { + "result": { + "limit": SERVICE_CALL_LIMIT, + "params": { + "domain": "test", + "service": "script", + "service_data": {"value": "value"}, + "target": {}, + }, + "running_script": False, + }, + "variables": {"variable": "value"}, + } + ], + } + assert_action_trace(expected_trace) + async def test_set_redefines_variable(hass, caplog): """Test setting variables based on their current value.""" @@ -2261,6 +2804,42 @@ async def test_set_redefines_variable(hass, caplog): assert mock_calls[0].data["value"] == 1 assert mock_calls[1].data["value"] == 2 + expected_trace = { + "0": [{}], + "1": [ + { + "result": { + "limit": SERVICE_CALL_LIMIT, + "params": { + "domain": "test", + "service": "script", + "service_data": {"value": 1}, + "target": {}, + }, + "running_script": False, + }, + "variables": {"variable": "1"}, + } + ], + "2": [{}], + "3": [ + { + "result": { + "limit": SERVICE_CALL_LIMIT, + "params": { + "domain": "test", + "service": "script", + "service_data": {"value": 2}, + "target": {}, + }, + "running_script": False, + }, + "variables": {"variable": 2}, + } + ], + } + assert_action_trace(expected_trace) + async def test_validate_action_config(hass): """Validate action config."""