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 Bram Kragten
parent 19c2bbcd93
commit f753f5af6e
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( timed_out = await template_obj.async_render_will_timeout(
timeout, variables, strict=msg["strict"], log_fn=log_fn timeout, variables, strict=msg["strict"], log_fn=log_fn
) )
except TemplateError as ex: except TemplateError:
connection.send_error(msg["id"], const.ERR_TEMPLATE_ERROR, str(ex)) timed_out = False
return
if timed_out: if timed_out:
connection.send_error( connection.send_error(
@ -583,7 +582,6 @@ async def handle_render_template(
hass, hass,
[TrackTemplate(template_obj, variables)], [TrackTemplate(template_obj, variables)],
_template_listener, _template_listener,
raise_on_template_error=True,
strict=msg["strict"], strict=msg["strict"],
log_fn=log_fn, log_fn=log_fn,
) )

View File

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

View File

@ -1234,27 +1234,27 @@ EMPTY_LISTENERS = {"all": False, "entities": [], "domains": [], "time": False}
ERR_MSG = {"type": "result", "success": False} ERR_MSG = {"type": "result", "success": False}
VARIABLE_ERROR_UNDEFINED_FUNC = { EVENT_UNDEFINED_FUNC_1 = {
"error": "'my_unknown_func' is undefined", "error": "'my_unknown_func' is undefined",
"level": "ERROR", "level": "ERROR",
} }
TEMPLATE_ERROR_UNDEFINED_FUNC = { EVENT_UNDEFINED_FUNC_2 = {
"code": "template_error", "error": "UndefinedError: 'my_unknown_func' is undefined",
"message": "UndefinedError: 'my_unknown_func' is undefined", "level": "ERROR",
} }
VARIABLE_WARNING_UNDEFINED_VAR = { EVENT_UNDEFINED_VAR_WARN = {
"error": "'my_unknown_var' is undefined", "error": "'my_unknown_var' is undefined",
"level": "WARNING", "level": "WARNING",
} }
TEMPLATE_ERROR_UNDEFINED_VAR = { EVENT_UNDEFINED_VAR_ERR = {
"code": "template_error", "error": "UndefinedError: 'my_unknown_var' is undefined",
"message": "UndefinedError: 'my_unknown_var' is undefined", "level": "ERROR",
} }
TEMPLATE_ERROR_UNDEFINED_FILTER = { EVENT_UNDEFINED_FILTER = {
"code": "template_error", "error": "TemplateAssertionError: No filter named 'unknown_filter'.",
"message": "TemplateAssertionError: No filter named 'unknown_filter'.", "level": "ERROR",
} }
@ -1264,16 +1264,19 @@ TEMPLATE_ERROR_UNDEFINED_FILTER = {
( (
"{{ my_unknown_func() + 1 }}", "{{ my_unknown_func() + 1 }}",
[ [
{"type": "event", "event": VARIABLE_ERROR_UNDEFINED_FUNC}, {"type": "event", "event": EVENT_UNDEFINED_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_1},
{"type": "event", "event": EVENT_UNDEFINED_FUNC_2},
], ],
), ),
( (
"{{ my_unknown_var }}", "{{ my_unknown_var }}",
[ [
{"type": "event", "event": VARIABLE_WARNING_UNDEFINED_VAR}, {"type": "event", "event": EVENT_UNDEFINED_VAR_WARN},
{"type": "result", "success": True, "result": None}, {"type": "result", "success": True, "result": None},
{"type": "event", "event": VARIABLE_WARNING_UNDEFINED_VAR}, {"type": "event", "event": EVENT_UNDEFINED_VAR_WARN},
{ {
"type": "event", "type": "event",
"event": {"result": "", "listeners": EMPTY_LISTENERS}, "event": {"result": "", "listeners": EMPTY_LISTENERS},
@ -1282,11 +1285,19 @@ TEMPLATE_ERROR_UNDEFINED_FILTER = {
), ),
( (
"{{ my_unknown_var + 1 }}", "{{ 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 }}", "{{ 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 }}", "{{ my_unknown_func() + 1 }}",
[ [
{"type": "event", "event": VARIABLE_ERROR_UNDEFINED_FUNC}, {"type": "event", "event": EVENT_UNDEFINED_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_1},
{"type": "event", "event": EVENT_UNDEFINED_FUNC_2},
{"type": "event", "event": EVENT_UNDEFINED_FUNC_1},
], ],
), ),
( (
"{{ my_unknown_var }}", "{{ my_unknown_var }}",
[ [
{"type": "event", "event": VARIABLE_WARNING_UNDEFINED_VAR}, {"type": "event", "event": EVENT_UNDEFINED_VAR_WARN},
{"type": "result", "success": True, "result": None}, {"type": "result", "success": True, "result": None},
{"type": "event", "event": VARIABLE_WARNING_UNDEFINED_VAR}, {"type": "event", "event": EVENT_UNDEFINED_VAR_WARN},
{ {
"type": "event", "type": "event",
"event": {"result": "", "listeners": EMPTY_LISTENERS}, "event": {"result": "", "listeners": EMPTY_LISTENERS},
@ -1343,11 +1358,19 @@ async def test_render_template_with_error(
), ),
( (
"{{ my_unknown_var + 1 }}", "{{ 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 }}", "{{ 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 }}", "{{ 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 }}", "{{ 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 }}", "{{ 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 }}", "{{ 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, template: str,
expected_events: list[dict[str, str]], expected_events: list[dict[str, str]],
) -> None: ) -> 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) caplog.set_level(logging.INFO)
await websocket_client.send_json( 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(): for key, value in expected_event.items():
assert msg[key] == value assert msg[key] == value
assert "Template variable error" not in caplog.text assert "TemplateError" 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": "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( 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: ) -> 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( 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() for expected_event in expected_events_1:
assert msg["id"] == 5 msg = await websocket_client.receive_json()
assert msg["type"] == const.TYPE_RESULT assert msg["id"] == 5
assert not msg["success"] for key, value in expected_event.items():
assert msg["error"]["code"] == const.ERR_TEMPLATE_ERROR 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 error" not in caplog.text
assert "Template variable warning" not in caplog.text assert "Template variable warning" not in caplog.text
assert "TemplateError" 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( async def test_render_template_with_delayed_error(
hass: HomeAssistant, websocket_client, caplog: pytest.LogCaptureFixture hass: HomeAssistant, websocket_client, caplog: pytest.LogCaptureFixture
) -> None: ) -> 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: async def test_track_template_with_time(hass: HomeAssistant) -> None:
"""Test tracking template with time.""" """Test tracking template with time."""