Make WS command render_template not give up if initial render raises (#99808)

This commit is contained in:
Erik Montnemery 2023-09-08 21:02:06 +02:00 committed by GitHub
parent b317e04cf1
commit 1654ef7759
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 281 additions and 74 deletions

View File

@ -542,9 +542,8 @@ async def handle_render_template(
timed_out = await template_obj.async_render_will_timeout(
timeout, variables, strict=msg["strict"], log_fn=log_fn
)
except TemplateError as ex:
connection.send_error(msg["id"], const.ERR_TEMPLATE_ERROR, str(ex))
return
except TemplateError:
timed_out = False
if timed_out:
connection.send_error(
@ -583,7 +582,6 @@ async def handle_render_template(
hass,
[TrackTemplate(template_obj, variables)],
_template_listener,
raise_on_template_error=True,
strict=msg["strict"],
log_fn=log_fn,
)

View File

@ -917,7 +917,6 @@ class TrackTemplateResultInfo:
def async_setup(
self,
raise_on_template_error: bool,
strict: bool = False,
log_fn: Callable[[int, str], None] | None = None,
) -> None:
@ -955,8 +954,6 @@ class TrackTemplateResultInfo:
)
if info.exception:
if raise_on_template_error:
raise info.exception
if not log_fn:
_LOGGER.error(
"Error while processing template: %s",
@ -1239,7 +1236,6 @@ def async_track_template_result(
hass: HomeAssistant,
track_templates: Sequence[TrackTemplate],
action: TrackTemplateResultListener,
raise_on_template_error: bool = False,
strict: bool = False,
log_fn: Callable[[int, str], None] | None = None,
has_super_template: bool = False,
@ -1266,11 +1262,6 @@ def async_track_template_result(
An iterable of TrackTemplate.
action
Callable to call with results.
raise_on_template_error
When set to True, if there is an exception
processing the template during setup, the system
will raise the exception instead of setting up
tracking.
strict
When set to True, raise on undefined variables.
log_fn
@ -1286,7 +1277,7 @@ def async_track_template_result(
"""
tracker = TrackTemplateResultInfo(hass, track_templates, action, has_super_template)
tracker.async_setup(raise_on_template_error, strict=strict, log_fn=log_fn)
tracker.async_setup(strict=strict, log_fn=log_fn)
return tracker

View File

@ -1234,27 +1234,27 @@ EMPTY_LISTENERS = {"all": False, "entities": [], "domains": [], "time": False}
ERR_MSG = {"type": "result", "success": False}
VARIABLE_ERROR_UNDEFINED_FUNC = {
EVENT_UNDEFINED_FUNC_1 = {
"error": "'my_unknown_func' is undefined",
"level": "ERROR",
}
TEMPLATE_ERROR_UNDEFINED_FUNC = {
"code": "template_error",
"message": "UndefinedError: 'my_unknown_func' is undefined",
EVENT_UNDEFINED_FUNC_2 = {
"error": "UndefinedError: 'my_unknown_func' is undefined",
"level": "ERROR",
}
VARIABLE_WARNING_UNDEFINED_VAR = {
EVENT_UNDEFINED_VAR_WARN = {
"error": "'my_unknown_var' is undefined",
"level": "WARNING",
}
TEMPLATE_ERROR_UNDEFINED_VAR = {
"code": "template_error",
"message": "UndefinedError: 'my_unknown_var' is undefined",
EVENT_UNDEFINED_VAR_ERR = {
"error": "UndefinedError: 'my_unknown_var' is undefined",
"level": "ERROR",
}
TEMPLATE_ERROR_UNDEFINED_FILTER = {
"code": "template_error",
"message": "TemplateAssertionError: No filter named 'unknown_filter'.",
EVENT_UNDEFINED_FILTER = {
"error": "TemplateAssertionError: No filter named 'unknown_filter'.",
"level": "ERROR",
}
@ -1264,16 +1264,19 @@ TEMPLATE_ERROR_UNDEFINED_FILTER = {
(
"{{ my_unknown_func() + 1 }}",
[
{"type": "event", "event": VARIABLE_ERROR_UNDEFINED_FUNC},
ERR_MSG | {"error": TEMPLATE_ERROR_UNDEFINED_FUNC},
{"type": "event", "event": EVENT_UNDEFINED_FUNC_1},
{"type": "event", "event": EVENT_UNDEFINED_FUNC_2},
{"type": "result", "success": True, "result": None},
{"type": "event", "event": EVENT_UNDEFINED_FUNC_1},
{"type": "event", "event": EVENT_UNDEFINED_FUNC_2},
],
),
(
"{{ my_unknown_var }}",
[
{"type": "event", "event": VARIABLE_WARNING_UNDEFINED_VAR},
{"type": "event", "event": EVENT_UNDEFINED_VAR_WARN},
{"type": "result", "success": True, "result": None},
{"type": "event", "event": VARIABLE_WARNING_UNDEFINED_VAR},
{"type": "event", "event": EVENT_UNDEFINED_VAR_WARN},
{
"type": "event",
"event": {"result": "", "listeners": EMPTY_LISTENERS},
@ -1282,11 +1285,19 @@ TEMPLATE_ERROR_UNDEFINED_FILTER = {
),
(
"{{ my_unknown_var + 1 }}",
[ERR_MSG | {"error": TEMPLATE_ERROR_UNDEFINED_VAR}],
[
{"type": "event", "event": EVENT_UNDEFINED_VAR_ERR},
{"type": "result", "success": True, "result": None},
{"type": "event", "event": EVENT_UNDEFINED_VAR_ERR},
],
),
(
"{{ now() | unknown_filter }}",
[ERR_MSG | {"error": TEMPLATE_ERROR_UNDEFINED_FILTER}],
[
{"type": "event", "event": EVENT_UNDEFINED_FILTER},
{"type": "result", "success": True, "result": None},
{"type": "event", "event": EVENT_UNDEFINED_FILTER},
],
),
],
)
@ -1325,16 +1336,20 @@ async def test_render_template_with_error(
(
"{{ my_unknown_func() + 1 }}",
[
{"type": "event", "event": VARIABLE_ERROR_UNDEFINED_FUNC},
ERR_MSG | {"error": TEMPLATE_ERROR_UNDEFINED_FUNC},
{"type": "event", "event": EVENT_UNDEFINED_FUNC_1},
{"type": "event", "event": EVENT_UNDEFINED_FUNC_2},
{"type": "result", "success": True, "result": None},
{"type": "event", "event": EVENT_UNDEFINED_FUNC_1},
{"type": "event", "event": EVENT_UNDEFINED_FUNC_2},
{"type": "event", "event": EVENT_UNDEFINED_FUNC_1},
],
),
(
"{{ my_unknown_var }}",
[
{"type": "event", "event": VARIABLE_WARNING_UNDEFINED_VAR},
{"type": "event", "event": EVENT_UNDEFINED_VAR_WARN},
{"type": "result", "success": True, "result": None},
{"type": "event", "event": VARIABLE_WARNING_UNDEFINED_VAR},
{"type": "event", "event": EVENT_UNDEFINED_VAR_WARN},
{
"type": "event",
"event": {"result": "", "listeners": EMPTY_LISTENERS},
@ -1343,11 +1358,19 @@ async def test_render_template_with_error(
),
(
"{{ my_unknown_var + 1 }}",
[ERR_MSG | {"error": TEMPLATE_ERROR_UNDEFINED_VAR}],
[
{"type": "event", "event": EVENT_UNDEFINED_VAR_ERR},
{"type": "result", "success": True, "result": None},
{"type": "event", "event": EVENT_UNDEFINED_VAR_ERR},
],
),
(
"{{ now() | unknown_filter }}",
[ERR_MSG | {"error": TEMPLATE_ERROR_UNDEFINED_FILTER}],
[
{"type": "event", "event": EVENT_UNDEFINED_FILTER},
{"type": "result", "success": True, "result": None},
{"type": "event", "event": EVENT_UNDEFINED_FILTER},
],
),
],
)
@ -1386,19 +1409,35 @@ async def test_render_template_with_timeout_and_error(
[
(
"{{ my_unknown_func() + 1 }}",
[ERR_MSG | {"error": TEMPLATE_ERROR_UNDEFINED_FUNC}],
[
{"type": "event", "event": EVENT_UNDEFINED_FUNC_2},
{"type": "result", "success": True, "result": None},
{"type": "event", "event": EVENT_UNDEFINED_FUNC_2},
],
),
(
"{{ my_unknown_var }}",
[ERR_MSG | {"error": TEMPLATE_ERROR_UNDEFINED_VAR}],
[
{"type": "event", "event": EVENT_UNDEFINED_VAR_ERR},
{"type": "result", "success": True, "result": None},
{"type": "event", "event": EVENT_UNDEFINED_VAR_ERR},
],
),
(
"{{ my_unknown_var + 1 }}",
[ERR_MSG | {"error": TEMPLATE_ERROR_UNDEFINED_VAR}],
[
{"type": "event", "event": EVENT_UNDEFINED_VAR_ERR},
{"type": "result", "success": True, "result": None},
{"type": "event", "event": EVENT_UNDEFINED_VAR_ERR},
],
),
(
"{{ now() | unknown_filter }}",
[ERR_MSG | {"error": TEMPLATE_ERROR_UNDEFINED_FILTER}],
[
{"type": "event", "event": EVENT_UNDEFINED_FILTER},
{"type": "result", "success": True, "result": None},
{"type": "event", "event": EVENT_UNDEFINED_FILTER},
],
),
],
)
@ -1409,7 +1448,73 @@ async def test_render_template_strict_with_timeout_and_error(
template: str,
expected_events: list[dict[str, str]],
) -> None:
"""Test a template with an error with a timeout."""
"""Test a template with an error with a timeout.
In this test report_errors is enabled.
"""
caplog.set_level(logging.INFO)
await websocket_client.send_json(
{
"id": 5,
"type": "render_template",
"template": template,
"timeout": 5,
"strict": True,
"report_errors": True,
}
)
for expected_event in expected_events:
msg = await websocket_client.receive_json()
assert msg["id"] == 5
for key, value in expected_event.items():
assert msg[key] == value
assert "Template variable error" not in caplog.text
assert "Template variable warning" not in caplog.text
assert "TemplateError" not in caplog.text
@pytest.mark.parametrize(
("template", "expected_events"),
[
(
"{{ my_unknown_func() + 1 }}",
[
{"type": "result", "success": True, "result": None},
],
),
(
"{{ my_unknown_var }}",
[
{"type": "result", "success": True, "result": None},
],
),
(
"{{ my_unknown_var + 1 }}",
[
{"type": "result", "success": True, "result": None},
],
),
(
"{{ now() | unknown_filter }}",
[
{"type": "result", "success": True, "result": None},
],
),
],
)
async def test_render_template_strict_with_timeout_and_error_2(
hass: HomeAssistant,
websocket_client,
caplog: pytest.LogCaptureFixture,
template: str,
expected_events: list[dict[str, str]],
) -> None:
"""Test a template with an error with a timeout.
In this test report_errors is disabled.
"""
caplog.set_level(logging.INFO)
await websocket_client.send_json(
{
@ -1427,30 +1532,164 @@ async def test_render_template_strict_with_timeout_and_error(
for key, value in expected_event.items():
assert msg[key] == value
assert "Template variable error" not in caplog.text
assert "Template variable warning" not in caplog.text
assert "TemplateError" not in caplog.text
assert "TemplateError" in caplog.text
@pytest.mark.parametrize(
("template", "expected_events_1", "expected_events_2"),
[
(
"{{ now() | random }}",
[
{
"type": "event",
"event": {
"error": "TypeError: object of type 'datetime.datetime' has no len()",
"level": "ERROR",
},
},
{"type": "result", "success": True, "result": None},
{
"type": "event",
"event": {
"error": "TypeError: object of type 'datetime.datetime' has no len()",
"level": "ERROR",
},
},
],
[],
),
(
"{{ float(states.sensor.foo.state) + 1 }}",
[
{
"type": "event",
"event": {
"error": "UndefinedError: 'None' has no attribute 'state'",
"level": "ERROR",
},
},
{"type": "result", "success": True, "result": None},
{
"type": "event",
"event": {
"error": "UndefinedError: 'None' has no attribute 'state'",
"level": "ERROR",
},
},
],
[
{
"type": "event",
"event": {
"result": 3.0,
"listeners": EMPTY_LISTENERS | {"entities": ["sensor.foo"]},
},
},
],
),
],
)
async def test_render_template_error_in_template_code(
hass: HomeAssistant, websocket_client, caplog: pytest.LogCaptureFixture
hass: HomeAssistant,
websocket_client,
caplog: pytest.LogCaptureFixture,
template: str,
expected_events_1: list[dict[str, str]],
expected_events_2: list[dict[str, str]],
) -> None:
"""Test a template that will throw in template.py."""
"""Test a template that will throw in template.py.
In this test report_errors is enabled.
"""
await websocket_client.send_json(
{"id": 5, "type": "render_template", "template": "{{ now() | random }}"}
{
"id": 5,
"type": "render_template",
"template": template,
"report_errors": True,
}
)
msg = await websocket_client.receive_json()
assert msg["id"] == 5
assert msg["type"] == const.TYPE_RESULT
assert not msg["success"]
assert msg["error"]["code"] == const.ERR_TEMPLATE_ERROR
for expected_event in expected_events_1:
msg = await websocket_client.receive_json()
assert msg["id"] == 5
for key, value in expected_event.items():
assert msg[key] == value
hass.states.async_set("sensor.foo", "2")
for expected_event in expected_events_2:
msg = await websocket_client.receive_json()
assert msg["id"] == 5
for key, value in expected_event.items():
assert msg[key] == value
assert "Template variable error" not in caplog.text
assert "Template variable warning" not in caplog.text
assert "TemplateError" not in caplog.text
@pytest.mark.parametrize(
("template", "expected_events_1", "expected_events_2"),
[
(
"{{ now() | random }}",
[
{"type": "result", "success": True, "result": None},
],
[],
),
(
"{{ float(states.sensor.foo.state) + 1 }}",
[
{"type": "result", "success": True, "result": None},
],
[
{
"type": "event",
"event": {
"result": 3.0,
"listeners": EMPTY_LISTENERS | {"entities": ["sensor.foo"]},
},
},
],
),
],
)
async def test_render_template_error_in_template_code_2(
hass: HomeAssistant,
websocket_client,
caplog: pytest.LogCaptureFixture,
template: str,
expected_events_1: list[dict[str, str]],
expected_events_2: list[dict[str, str]],
) -> None:
"""Test a template that will throw in template.py.
In this test report_errors is disabled.
"""
await websocket_client.send_json(
{"id": 5, "type": "render_template", "template": template}
)
for expected_event in expected_events_1:
msg = await websocket_client.receive_json()
assert msg["id"] == 5
for key, value in expected_event.items():
assert msg[key] == value
hass.states.async_set("sensor.foo", "2")
for expected_event in expected_events_2:
msg = await websocket_client.receive_json()
assert msg["id"] == 5
for key, value in expected_event.items():
assert msg[key] == value
assert "TemplateError" in caplog.text
async def test_render_template_with_delayed_error(
hass: HomeAssistant, websocket_client, caplog: pytest.LogCaptureFixture
) -> None:

View File

@ -3239,27 +3239,6 @@ async def test_async_track_template_result_multiple_templates_mixing_domain(
]
async def test_async_track_template_result_raise_on_template_error(
hass: HomeAssistant,
) -> None:
"""Test that we raise as soon as we encounter a failed template."""
with pytest.raises(TemplateError):
async_track_template_result(
hass,
[
TrackTemplate(
Template(
"{{ states.switch | function_that_does_not_exist | list }}"
),
None,
),
],
ha.callback(lambda event, updates: None),
raise_on_template_error=True,
)
async def test_track_template_with_time(hass: HomeAssistant) -> None:
"""Test tracking template with time."""