diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index a05f2aa8e3f..ea21b7b5eba 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -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, ) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 51a8f1f1982..40364b7b367 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -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 diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index 70f08477a72..b1b2027c65d 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -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: diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index dc06b9d94c8..00ad580693e 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -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."""