Merge condition and action traces (#48461)

This commit is contained in:
Erik Montnemery 2021-03-29 23:06:49 +02:00 committed by GitHub
parent c459789c09
commit 7534b54e4e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 102 additions and 119 deletions

View File

@ -55,7 +55,12 @@ from homeassistant.helpers.script import (
)
from homeassistant.helpers.script_variables import ScriptVariables
from homeassistant.helpers.service import async_register_admin_service
from homeassistant.helpers.trace import trace_get, trace_path
from homeassistant.helpers.trace import (
TraceElement,
trace_append_element,
trace_get,
trace_path,
)
from homeassistant.helpers.trigger import async_initialize_triggers
from homeassistant.helpers.typing import TemplateVarsType
from homeassistant.loader import bind_hass
@ -406,10 +411,16 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
return
else:
variables = run_variables
automation_trace.set_variables(variables)
# Prepare tracing the automation
automation_trace.set_trace(trace_get())
# Prepare tracing the evaluation of the automation's conditions
automation_trace.set_condition_trace(trace_get())
# Set trigger reason
trigger_description = variables.get("trigger", {}).get("description")
automation_trace.set_trigger_description(trigger_description)
# Add initial variables as the trigger step
trace_element = TraceElement(variables, "trigger")
trace_append_element(trace_element)
if (
not skip_condition
@ -422,9 +433,6 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
)
return
# Prepare tracing the execution of the automation's actions
automation_trace.set_action_trace(trace_get())
self.async_set_context(trigger_context)
event_data = {
ATTR_NAME: self._name,

View File

@ -2,11 +2,10 @@
from __future__ import annotations
from contextlib import contextmanager
from typing import Any, Deque
from typing import Any
from homeassistant.components.trace import ActionTrace, async_store_trace
from homeassistant.core import Context
from homeassistant.helpers.trace import TraceElement
# mypy: allow-untyped-calls, allow-untyped-defs
# mypy: no-check-untyped-defs, no-warn-return-any
@ -24,42 +23,16 @@ class AutomationTrace(ActionTrace):
"""Container for automation trace."""
key = ("automation", item_id)
super().__init__(key, config, context)
self._condition_trace: dict[str, Deque[TraceElement]] | None = None
self._trigger_description: str | None = None
def set_condition_trace(self, trace: dict[str, Deque[TraceElement]]) -> None:
"""Set condition trace."""
self._condition_trace = trace
def as_dict(self) -> dict[str, Any]:
"""Return dictionary version of this AutomationTrace."""
result = super().as_dict()
condition_traces = {}
if self._condition_trace:
for key, trace_list in self._condition_trace.items():
condition_traces[key] = [item.as_dict() for item in trace_list]
result["condition_trace"] = condition_traces
return result
def set_trigger_description(self, trigger: str) -> None:
"""Set trigger description."""
self._trigger_description = trigger
def as_short_dict(self) -> dict[str, Any]:
"""Return a brief dictionary version of this AutomationTrace."""
result = super().as_short_dict()
last_condition = None
trigger = None
if self._condition_trace:
last_condition = list(self._condition_trace)[-1]
if self._variables:
trigger = self._variables.get("trigger", {}).get("description")
result["trigger"] = trigger
result["last_condition"] = last_condition
result["trigger"] = self._trigger_description
return result

View File

@ -354,9 +354,8 @@ class ScriptEntity(ToggleEntity):
with trace_script(
self.hass, self.object_id, self._raw_config, context
) as script_trace:
script_trace.set_variables(variables)
# Prepare tracing the execution of the script's sequence
script_trace.set_action_trace(trace_get())
script_trace.set_trace(trace_get())
with trace_path("sequence"):
return await self.script.async_run(variables, context)

View File

@ -50,7 +50,7 @@ class ActionTrace:
context: Context,
):
"""Container for script trace."""
self._action_trace: dict[str, Deque[TraceElement]] | None = None
self._trace: dict[str, Deque[TraceElement]] | None = None
self._config: dict[str, Any] = config
self.context: Context = context
self._error: Exception | None = None
@ -59,23 +59,18 @@ class ActionTrace:
self._timestamp_finish: dt.datetime | None = None
self._timestamp_start: dt.datetime = dt_util.utcnow()
self.key: tuple[str, str] = key
self._variables: dict[str, Any] | None = None
if trace_id_get():
trace_set_child_id(self.key, self.run_id)
trace_id_set((key, self.run_id))
def set_action_trace(self, trace: dict[str, Deque[TraceElement]]) -> None:
"""Set action trace."""
self._action_trace = trace
def set_trace(self, trace: dict[str, Deque[TraceElement]]) -> None:
"""Set trace."""
self._trace = trace
def set_error(self, ex: Exception) -> None:
"""Set error."""
self._error = ex
def set_variables(self, variables: dict[str, Any]) -> None:
"""Set variables."""
self._variables = variables
def finished(self) -> None:
"""Set finish time."""
self._timestamp_finish = dt_util.utcnow()
@ -86,17 +81,16 @@ class ActionTrace:
result = self.as_short_dict()
action_traces = {}
if self._action_trace:
for key, trace_list in self._action_trace.items():
action_traces[key] = [item.as_dict() for item in trace_list]
traces = {}
if self._trace:
for key, trace_list in self._trace.items():
traces[key] = [item.as_dict() for item in trace_list]
result.update(
{
"action_trace": action_traces,
"trace": traces,
"config": self._config,
"context": self.context,
"variables": self._variables,
}
)
if self._error is not None:
@ -106,13 +100,13 @@ class ActionTrace:
def as_short_dict(self) -> dict[str, Any]:
"""Return a brief dictionary version of this ActionTrace."""
last_action = None
last_step = None
if self._action_trace:
last_action = list(self._action_trace)[-1]
if self._trace:
last_step = list(self._trace)[-1]
result = {
"last_action": last_action,
"last_step": last_step,
"run_id": self.run_id,
"state": self._state,
"timestamp": {
@ -124,7 +118,7 @@ class ActionTrace:
}
if self._error is not None:
result["error"] = str(self._error)
if last_action is not None:
result["last_action"] = last_action
if last_step is not None:
result["last_step"] = last_step
return result

View File

@ -110,18 +110,21 @@ async def test_get_trace(hass, hass_ws_client, domain, prefix):
response = await client.receive_json()
assert response["success"]
trace = response["result"]
assert len(trace["action_trace"]) == 1
assert len(trace["action_trace"][f"{prefix}/0"]) == 1
assert trace["action_trace"][f"{prefix}/0"][0]["error"]
assert trace["action_trace"][f"{prefix}/0"][0]["result"] == sun_action
if domain == "automation":
assert len(trace["trace"]) == 2
assert set(trace["trace"]) == {"trigger", f"{prefix}/0"}
else:
assert len(trace["trace"]) == 1
assert set(trace["trace"]) == {f"{prefix}/0"}
assert len(trace["trace"][f"{prefix}/0"]) == 1
assert trace["trace"][f"{prefix}/0"][0]["error"]
assert trace["trace"][f"{prefix}/0"][0]["result"] == sun_action
assert trace["config"] == sun_config
assert trace["context"]
assert trace["error"] == "Unable to find service test.automation"
assert trace["state"] == "stopped"
assert trace["item_id"] == "sun"
assert trace["variables"] is not None
if domain == "automation":
assert trace["condition_trace"] == {}
assert trace["context"]["parent_id"] == context.id
assert trace["trigger"] == "event 'test_event'"
else:
@ -158,21 +161,24 @@ async def test_get_trace(hass, hass_ws_client, domain, prefix):
response = await client.receive_json()
assert response["success"]
trace = response["result"]
assert len(trace["action_trace"]) == 1
assert len(trace["action_trace"][f"{prefix}/0"]) == 1
assert "error" not in trace["action_trace"][f"{prefix}/0"][0]
assert trace["action_trace"][f"{prefix}/0"][0]["result"] == moon_action
if domain == "automation":
assert len(trace["trace"]) == 3
assert set(trace["trace"]) == {"trigger", "condition/0", f"{prefix}/0"}
else:
assert len(trace["trace"]) == 1
assert set(trace["trace"]) == {f"{prefix}/0"}
assert len(trace["trace"][f"{prefix}/0"]) == 1
assert "error" not in trace["trace"][f"{prefix}/0"][0]
assert trace["trace"][f"{prefix}/0"][0]["result"] == moon_action
assert trace["config"] == moon_config
assert trace["context"]
assert "error" not in trace
assert trace["state"] == "stopped"
assert trace["item_id"] == "moon"
assert trace["variables"] is not None
if domain == "automation":
assert len(trace["condition_trace"]) == 1
assert len(trace["condition_trace"]["condition/0"]) == 1
assert trace["condition_trace"]["condition/0"][0]["result"] == {"result": True}
assert len(trace["trace"]["condition/0"]) == 1
assert trace["trace"]["condition/0"][0]["result"] == {"result": True}
assert trace["trigger"] == "event 'test_event2'"
contexts[trace["context"]["id"]] = {
"run_id": trace["run_id"],
@ -211,17 +217,20 @@ async def test_get_trace(hass, hass_ws_client, domain, prefix):
response = await client.receive_json()
assert response["success"]
trace = response["result"]
assert len(trace["action_trace"]) == 0
assert len(trace["condition_trace"]) == 1
assert len(trace["condition_trace"]["condition/0"]) == 1
assert trace["condition_trace"]["condition/0"][0]["result"] == {"result": False}
if domain == "automation":
assert len(trace["trace"]) == 2
assert set(trace["trace"]) == {"trigger", "condition/0"}
else:
assert len(trace["trace"]) == 1
assert set(trace["trace"]) == {f"{prefix}/0"}
assert len(trace["trace"]["condition/0"]) == 1
assert trace["trace"]["condition/0"][0]["result"] == {"result": False}
assert trace["config"] == moon_config
assert trace["context"]
assert "error" not in trace
assert trace["state"] == "stopped"
assert trace["trigger"] == "event 'test_event3'"
assert trace["item_id"] == "moon"
assert trace["variables"]
contexts[trace["context"]["id"]] = {
"run_id": trace["run_id"],
"domain": domain,
@ -251,20 +260,19 @@ async def test_get_trace(hass, hass_ws_client, domain, prefix):
response = await client.receive_json()
assert response["success"]
trace = response["result"]
assert len(trace["action_trace"]) == 1
assert len(trace["action_trace"][f"{prefix}/0"]) == 1
assert "error" not in trace["action_trace"][f"{prefix}/0"][0]
assert trace["action_trace"][f"{prefix}/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}
assert len(trace["trace"]) == 3
assert set(trace["trace"]) == {"trigger", "condition/0", f"{prefix}/0"}
assert len(trace["trace"][f"{prefix}/0"]) == 1
assert "error" not in trace["trace"][f"{prefix}/0"][0]
assert trace["trace"][f"{prefix}/0"][0]["result"] == moon_action
assert len(trace["trace"]["condition/0"]) == 1
assert trace["trace"]["condition/0"][0]["result"] == {"result": True}
assert trace["config"] == moon_config
assert trace["context"]
assert "error" not in trace
assert trace["state"] == "stopped"
assert trace["trigger"] == "event 'test_event2'"
assert trace["item_id"] == "moon"
assert trace["variables"]
contexts[trace["context"]["id"]] = {
"run_id": trace["run_id"],
"domain": domain,
@ -467,23 +475,21 @@ async def test_list_traces(hass, hass_ws_client, domain, prefix):
assert len(_find_traces(response["result"], domain, "moon")) == 3
assert len(_find_traces(response["result"], domain, "sun")) == 1
trace = _find_traces(response["result"], domain, "sun")[0]
assert trace["last_action"] == f"{prefix}/0"
assert trace["last_step"] == f"{prefix}/0"
assert trace["error"] == "Unable to find service test.automation"
assert trace["state"] == "stopped"
assert trace["timestamp"]
assert trace["item_id"] == "sun"
if domain == "automation":
assert trace["last_condition"] is None
assert trace["trigger"] == "event 'test_event'"
trace = _find_traces(response["result"], domain, "moon")[0]
assert trace["last_action"] == f"{prefix}/0"
assert trace["last_step"] == f"{prefix}/0"
assert "error" not in trace
assert trace["state"] == "stopped"
assert trace["timestamp"]
assert trace["item_id"] == "moon"
if domain == "automation":
assert trace["last_condition"] == "condition/0"
assert trace["trigger"] == "event 'test_event2'"
trace = _find_traces(response["result"], domain, "moon")[1]
@ -492,20 +498,18 @@ async def test_list_traces(hass, hass_ws_client, domain, prefix):
assert trace["timestamp"]
assert trace["item_id"] == "moon"
if domain == "automation":
assert trace["last_action"] is None
assert trace["last_condition"] == "condition/0"
assert trace["last_step"] == "condition/0"
assert trace["trigger"] == "event 'test_event3'"
else:
assert trace["last_action"] == f"{prefix}/0"
assert trace["last_step"] == f"{prefix}/0"
trace = _find_traces(response["result"], domain, "moon")[2]
assert trace["last_action"] == f"{prefix}/0"
assert trace["last_step"] == f"{prefix}/0"
assert "error" not in trace
assert trace["state"] == "stopped"
assert trace["timestamp"]
assert trace["item_id"] == "moon"
if domain == "automation":
assert trace["last_condition"] == "condition/0"
assert trace["trigger"] == "event 'test_event2'"
@ -585,9 +589,14 @@ async def test_nested_traces(hass, hass_ws_client, domain, prefix):
response = await client.receive_json()
assert response["success"]
trace = response["result"]
assert len(trace["action_trace"]) == 1
assert len(trace["action_trace"][f"{prefix}/0"]) == 1
child_id = trace["action_trace"][f"{prefix}/0"][0]["child_id"]
if domain == "automation":
assert len(trace["trace"]) == 2
assert set(trace["trace"]) == {"trigger", f"{prefix}/0"}
else:
assert len(trace["trace"]) == 1
assert set(trace["trace"]) == {f"{prefix}/0"}
assert len(trace["trace"][f"{prefix}/0"]) == 1
child_id = trace["trace"][f"{prefix}/0"][0]["child_id"]
assert child_id == {"domain": "script", "item_id": "moon", "run_id": moon_run_id}
@ -603,14 +612,14 @@ async def test_breakpoints(hass, hass_ws_client, domain, prefix):
id += 1
return id
async def assert_last_action(item_id, expected_action, expected_state):
async def assert_last_step(item_id, expected_action, expected_state):
await client.send_json(
{"id": next_id(), "type": "trace/list", "domain": domain}
)
response = await client.receive_json()
assert response["success"]
trace = _find_traces(response["result"], domain, item_id)[-1]
assert trace["last_action"] == expected_action
assert trace["last_step"] == expected_action
assert trace["state"] == expected_state
return trace["run_id"]
@ -704,7 +713,7 @@ async def test_breakpoints(hass, hass_ws_client, domain, prefix):
await hass.services.async_call("script", "sun")
response = await client.receive_json()
run_id = await assert_last_action("sun", f"{prefix}/1", "running")
run_id = await assert_last_step("sun", f"{prefix}/1", "running")
assert response["event"] == {
"domain": domain,
"item_id": "sun",
@ -725,7 +734,7 @@ async def test_breakpoints(hass, hass_ws_client, domain, prefix):
assert response["success"]
response = await client.receive_json()
run_id = await assert_last_action("sun", f"{prefix}/2", "running")
run_id = await assert_last_step("sun", f"{prefix}/2", "running")
assert response["event"] == {
"domain": domain,
"item_id": "sun",
@ -746,7 +755,7 @@ async def test_breakpoints(hass, hass_ws_client, domain, prefix):
assert response["success"]
response = await client.receive_json()
run_id = await assert_last_action("sun", f"{prefix}/5", "running")
run_id = await assert_last_step("sun", f"{prefix}/5", "running")
assert response["event"] == {
"domain": domain,
"item_id": "sun",
@ -766,7 +775,7 @@ async def test_breakpoints(hass, hass_ws_client, domain, prefix):
response = await client.receive_json()
assert response["success"]
await hass.async_block_till_done()
await assert_last_action("sun", f"{prefix}/5", "stopped")
await assert_last_step("sun", f"{prefix}/5", "stopped")
@pytest.mark.parametrize(
@ -781,14 +790,14 @@ async def test_breakpoints_2(hass, hass_ws_client, domain, prefix):
id += 1
return id
async def assert_last_action(item_id, expected_action, expected_state):
async def assert_last_step(item_id, expected_action, expected_state):
await client.send_json(
{"id": next_id(), "type": "trace/list", "domain": domain}
)
response = await client.receive_json()
assert response["success"]
trace = _find_traces(response["result"], domain, item_id)[-1]
assert trace["last_action"] == expected_action
assert trace["last_step"] == expected_action
assert trace["state"] == expected_state
return trace["run_id"]
@ -843,7 +852,7 @@ async def test_breakpoints_2(hass, hass_ws_client, domain, prefix):
await hass.services.async_call("script", "sun")
response = await client.receive_json()
run_id = await assert_last_action("sun", f"{prefix}/1", "running")
run_id = await assert_last_step("sun", f"{prefix}/1", "running")
assert response["event"] == {
"domain": domain,
"item_id": "sun",
@ -858,7 +867,7 @@ async def test_breakpoints_2(hass, hass_ws_client, domain, prefix):
response = await client.receive_json()
assert response["success"]
await hass.async_block_till_done()
await assert_last_action("sun", f"{prefix}/8", "stopped")
await assert_last_step("sun", f"{prefix}/8", "stopped")
# Should not be possible to set breakpoints
await client.send_json(
@ -880,7 +889,7 @@ async def test_breakpoints_2(hass, hass_ws_client, domain, prefix):
await hass.services.async_call("script", "sun")
await hass.async_block_till_done()
new_run_id = await assert_last_action("sun", f"{prefix}/8", "stopped")
new_run_id = await assert_last_step("sun", f"{prefix}/8", "stopped")
assert new_run_id != run_id
@ -896,14 +905,14 @@ async def test_breakpoints_3(hass, hass_ws_client, domain, prefix):
id += 1
return id
async def assert_last_action(item_id, expected_action, expected_state):
async def assert_last_step(item_id, expected_action, expected_state):
await client.send_json(
{"id": next_id(), "type": "trace/list", "domain": domain}
)
response = await client.receive_json()
assert response["success"]
trace = _find_traces(response["result"], domain, item_id)[-1]
assert trace["last_action"] == expected_action
assert trace["last_step"] == expected_action
assert trace["state"] == expected_state
return trace["run_id"]
@ -970,7 +979,7 @@ async def test_breakpoints_3(hass, hass_ws_client, domain, prefix):
await hass.services.async_call("script", "sun")
response = await client.receive_json()
run_id = await assert_last_action("sun", f"{prefix}/1", "running")
run_id = await assert_last_step("sun", f"{prefix}/1", "running")
assert response["event"] == {
"domain": domain,
"item_id": "sun",
@ -991,7 +1000,7 @@ async def test_breakpoints_3(hass, hass_ws_client, domain, prefix):
assert response["success"]
response = await client.receive_json()
run_id = await assert_last_action("sun", f"{prefix}/5", "running")
run_id = await assert_last_step("sun", f"{prefix}/5", "running")
assert response["event"] == {
"domain": domain,
"item_id": "sun",
@ -1011,7 +1020,7 @@ async def test_breakpoints_3(hass, hass_ws_client, domain, prefix):
response = await client.receive_json()
assert response["success"]
await hass.async_block_till_done()
await assert_last_action("sun", f"{prefix}/5", "stopped")
await assert_last_step("sun", f"{prefix}/5", "stopped")
# Clear 1st breakpoint
await client.send_json(
@ -1033,7 +1042,7 @@ async def test_breakpoints_3(hass, hass_ws_client, domain, prefix):
await hass.services.async_call("script", "sun")
response = await client.receive_json()
run_id = await assert_last_action("sun", f"{prefix}/5", "running")
run_id = await assert_last_step("sun", f"{prefix}/5", "running")
assert response["event"] == {
"domain": domain,
"item_id": "sun",