Allow specifying a custom log function for template render (#99572)

* Allow specifying a custom log function for template render

* Bypass template cache when reporting errors + fix tests

* Send errors as events

* Fix logic for creating new TemplateEnvironment

* Add strict mode back

* Only send error events if report_errors is True

* Force test of websocket_api only

* Debug test

* Run pytest with higher verbosity

* Timeout after 1 minute, enable syslog output

* Adjust timeout

* Add debug logs

* Fix unsafe call to WebSocketHandler._send_message

* Remove debug code

* Improve test coverage

* Revert accidental change

* Include severity in error events

* Remove redundant information from error events
This commit is contained in:
Erik Montnemery 2023-09-06 10:03:35 +02:00 committed by GitHub
parent f41b045244
commit 48f7924e9e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 374 additions and 84 deletions

View file

@ -5,6 +5,7 @@ from collections.abc import Callable
import datetime as dt import datetime as dt
from functools import lru_cache, partial from functools import lru_cache, partial
import json import json
import logging
from typing import Any, cast from typing import Any, cast
import voluptuous as vol import voluptuous as vol
@ -505,6 +506,7 @@ def _cached_template(template_str: str, hass: HomeAssistant) -> template.Templat
vol.Optional("variables"): dict, vol.Optional("variables"): dict,
vol.Optional("timeout"): vol.Coerce(float), vol.Optional("timeout"): vol.Coerce(float),
vol.Optional("strict", default=False): bool, vol.Optional("strict", default=False): bool,
vol.Optional("report_errors", default=False): bool,
} }
) )
@decorators.async_response @decorators.async_response
@ -513,14 +515,32 @@ async def handle_render_template(
) -> None: ) -> None:
"""Handle render_template command.""" """Handle render_template command."""
template_str = msg["template"] template_str = msg["template"]
template_obj = _cached_template(template_str, hass) report_errors: bool = msg["report_errors"]
if report_errors:
template_obj = template.Template(template_str, hass)
else:
template_obj = _cached_template(template_str, hass)
variables = msg.get("variables") variables = msg.get("variables")
timeout = msg.get("timeout") timeout = msg.get("timeout")
@callback
def _error_listener(level: int, template_error: str) -> None:
connection.send_message(
messages.event_message(
msg["id"],
{"error": template_error, "level": logging.getLevelName(level)},
)
)
@callback
def _thread_safe_error_listener(level: int, template_error: str) -> None:
hass.loop.call_soon_threadsafe(_error_listener, level, template_error)
if timeout: if timeout:
try: try:
log_fn = _thread_safe_error_listener if report_errors else None
timed_out = await template_obj.async_render_will_timeout( timed_out = await template_obj.async_render_will_timeout(
timeout, variables, strict=msg["strict"] timeout, variables, strict=msg["strict"], log_fn=log_fn
) )
except TemplateError as ex: except TemplateError as ex:
connection.send_error(msg["id"], const.ERR_TEMPLATE_ERROR, str(ex)) connection.send_error(msg["id"], const.ERR_TEMPLATE_ERROR, str(ex))
@ -542,7 +562,11 @@ async def handle_render_template(
track_template_result = updates.pop() track_template_result = updates.pop()
result = track_template_result.result result = track_template_result.result
if isinstance(result, TemplateError): if isinstance(result, TemplateError):
connection.send_error(msg["id"], const.ERR_TEMPLATE_ERROR, str(result)) if not report_errors:
return
connection.send_message(
messages.event_message(msg["id"], {"error": str(result)})
)
return return
connection.send_message( connection.send_message(
@ -552,12 +576,14 @@ async def handle_render_template(
) )
try: try:
log_fn = _error_listener if report_errors else None
info = async_track_template_result( info = async_track_template_result(
hass, hass,
[TrackTemplate(template_obj, variables)], [TrackTemplate(template_obj, variables)],
_template_listener, _template_listener,
raise_on_template_error=True, raise_on_template_error=True,
strict=msg["strict"], strict=msg["strict"],
log_fn=log_fn,
) )
except TemplateError as ex: except TemplateError as ex:
connection.send_error(msg["id"], const.ERR_TEMPLATE_ERROR, str(ex)) connection.send_error(msg["id"], const.ERR_TEMPLATE_ERROR, str(ex))

View file

@ -915,7 +915,12 @@ class TrackTemplateResultInfo:
"""Return the representation.""" """Return the representation."""
return f"<TrackTemplateResultInfo {self._info}>" return f"<TrackTemplateResultInfo {self._info}>"
def async_setup(self, raise_on_template_error: bool, strict: bool = False) -> None: def async_setup(
self,
raise_on_template_error: bool,
strict: bool = False,
log_fn: Callable[[int, str], None] | None = None,
) -> None:
"""Activation of template tracking.""" """Activation of template tracking."""
block_render = False block_render = False
super_template = self._track_templates[0] if self._has_super_template else None super_template = self._track_templates[0] if self._has_super_template else None
@ -925,7 +930,7 @@ class TrackTemplateResultInfo:
template = super_template.template template = super_template.template
variables = super_template.variables variables = super_template.variables
self._info[template] = info = template.async_render_to_info( self._info[template] = info = template.async_render_to_info(
variables, strict=strict variables, strict=strict, log_fn=log_fn
) )
# If the super template did not render to True, don't update other templates # If the super template did not render to True, don't update other templates
@ -946,7 +951,7 @@ class TrackTemplateResultInfo:
template = track_template_.template template = track_template_.template
variables = track_template_.variables variables = track_template_.variables
self._info[template] = info = template.async_render_to_info( self._info[template] = info = template.async_render_to_info(
variables, strict=strict variables, strict=strict, log_fn=log_fn
) )
if info.exception: if info.exception:
@ -1233,6 +1238,7 @@ def async_track_template_result(
action: TrackTemplateResultListener, action: TrackTemplateResultListener,
raise_on_template_error: bool = False, raise_on_template_error: bool = False,
strict: bool = False, strict: bool = False,
log_fn: Callable[[int, str], None] | None = None,
has_super_template: bool = False, has_super_template: bool = False,
) -> TrackTemplateResultInfo: ) -> TrackTemplateResultInfo:
"""Add a listener that fires when the result of a template changes. """Add a listener that fires when the result of a template changes.
@ -1264,6 +1270,9 @@ def async_track_template_result(
tracking. tracking.
strict strict
When set to True, raise on undefined variables. When set to True, raise on undefined variables.
log_fn
If not None, template error messages will logging by calling log_fn
instead of the normal logging facility.
has_super_template has_super_template
When set to True, the first template will block rendering of other When set to True, the first template will block rendering of other
templates if it doesn't render as True. templates if it doesn't render as True.
@ -1274,7 +1283,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) tracker.async_setup(raise_on_template_error, strict=strict, log_fn=log_fn)
return tracker return tracker

View file

@ -458,6 +458,7 @@ class Template:
"_exc_info", "_exc_info",
"_limited", "_limited",
"_strict", "_strict",
"_log_fn",
"_hash_cache", "_hash_cache",
"_renders", "_renders",
) )
@ -475,6 +476,7 @@ class Template:
self._exc_info: sys._OptExcInfo | None = None self._exc_info: sys._OptExcInfo | None = None
self._limited: bool | None = None self._limited: bool | None = None
self._strict: bool | None = None self._strict: bool | None = None
self._log_fn: Callable[[int, str], None] | None = None
self._hash_cache: int = hash(self.template) self._hash_cache: int = hash(self.template)
self._renders: int = 0 self._renders: int = 0
@ -482,6 +484,11 @@ class Template:
def _env(self) -> TemplateEnvironment: def _env(self) -> TemplateEnvironment:
if self.hass is None: if self.hass is None:
return _NO_HASS_ENV return _NO_HASS_ENV
# Bypass cache if a custom log function is specified
if self._log_fn is not None:
return TemplateEnvironment(
self.hass, self._limited, self._strict, self._log_fn
)
if self._limited: if self._limited:
wanted_env = _ENVIRONMENT_LIMITED wanted_env = _ENVIRONMENT_LIMITED
elif self._strict: elif self._strict:
@ -491,9 +498,7 @@ class Template:
ret: TemplateEnvironment | None = self.hass.data.get(wanted_env) ret: TemplateEnvironment | None = self.hass.data.get(wanted_env)
if ret is None: if ret is None:
ret = self.hass.data[wanted_env] = TemplateEnvironment( ret = self.hass.data[wanted_env] = TemplateEnvironment(
self.hass, self.hass, self._limited, self._strict, self._log_fn
self._limited,
self._strict,
) )
return ret return ret
@ -537,6 +542,7 @@ class Template:
parse_result: bool = True, parse_result: bool = True,
limited: bool = False, limited: bool = False,
strict: bool = False, strict: bool = False,
log_fn: Callable[[int, str], None] | None = None,
**kwargs: Any, **kwargs: Any,
) -> Any: ) -> Any:
"""Render given template. """Render given template.
@ -553,7 +559,7 @@ class Template:
return self.template return self.template
return self._parse_result(self.template) return self._parse_result(self.template)
compiled = self._compiled or self._ensure_compiled(limited, strict) compiled = self._compiled or self._ensure_compiled(limited, strict, log_fn)
if variables is not None: if variables is not None:
kwargs.update(variables) kwargs.update(variables)
@ -608,6 +614,7 @@ class Template:
timeout: float, timeout: float,
variables: TemplateVarsType = None, variables: TemplateVarsType = None,
strict: bool = False, strict: bool = False,
log_fn: Callable[[int, str], None] | None = None,
**kwargs: Any, **kwargs: Any,
) -> bool: ) -> bool:
"""Check to see if rendering a template will timeout during render. """Check to see if rendering a template will timeout during render.
@ -628,7 +635,7 @@ class Template:
if self.is_static: if self.is_static:
return False return False
compiled = self._compiled or self._ensure_compiled(strict=strict) compiled = self._compiled or self._ensure_compiled(strict=strict, log_fn=log_fn)
if variables is not None: if variables is not None:
kwargs.update(variables) kwargs.update(variables)
@ -664,7 +671,11 @@ class Template:
@callback @callback
def async_render_to_info( def async_render_to_info(
self, variables: TemplateVarsType = None, strict: bool = False, **kwargs: Any self,
variables: TemplateVarsType = None,
strict: bool = False,
log_fn: Callable[[int, str], None] | None = None,
**kwargs: Any,
) -> RenderInfo: ) -> RenderInfo:
"""Render the template and collect an entity filter.""" """Render the template and collect an entity filter."""
self._renders += 1 self._renders += 1
@ -680,7 +691,9 @@ class Template:
token = _render_info.set(render_info) token = _render_info.set(render_info)
try: try:
render_info._result = self.async_render(variables, strict=strict, **kwargs) render_info._result = self.async_render(
variables, strict=strict, log_fn=log_fn, **kwargs
)
except TemplateError as ex: except TemplateError as ex:
render_info.exception = ex render_info.exception = ex
finally: finally:
@ -743,7 +756,10 @@ class Template:
return value if error_value is _SENTINEL else error_value return value if error_value is _SENTINEL else error_value
def _ensure_compiled( def _ensure_compiled(
self, limited: bool = False, strict: bool = False self,
limited: bool = False,
strict: bool = False,
log_fn: Callable[[int, str], None] | None = None,
) -> jinja2.Template: ) -> jinja2.Template:
"""Bind a template to a specific hass instance.""" """Bind a template to a specific hass instance."""
self.ensure_valid() self.ensure_valid()
@ -756,10 +772,14 @@ class Template:
self._strict is None or self._strict == strict self._strict is None or self._strict == strict
), "can't change between strict and non strict template" ), "can't change between strict and non strict template"
assert not (strict and limited), "can't combine strict and limited template" assert not (strict and limited), "can't combine strict and limited template"
assert (
self._log_fn is None or self._log_fn == log_fn
), "can't change custom log function"
assert self._compiled_code is not None, "template code was not compiled" assert self._compiled_code is not None, "template code was not compiled"
self._limited = limited self._limited = limited
self._strict = strict self._strict = strict
self._log_fn = log_fn
env = self._env env = self._env
self._compiled = jinja2.Template.from_code( self._compiled = jinja2.Template.from_code(
@ -2178,45 +2198,56 @@ def _render_with_context(
return template.render(**kwargs) return template.render(**kwargs)
class LoggingUndefined(jinja2.Undefined): def make_logging_undefined(
strict: bool | None, log_fn: Callable[[int, str], None] | None
) -> type[jinja2.Undefined]:
"""Log on undefined variables.""" """Log on undefined variables."""
def _log_message(self) -> None: if strict:
return jinja2.StrictUndefined
def _log_with_logger(level: int, msg: str) -> None:
template, action = template_cv.get() or ("", "rendering or compiling") template, action = template_cv.get() or ("", "rendering or compiling")
_LOGGER.warning( _LOGGER.log(
"Template variable warning: %s when %s '%s'", level,
self._undefined_message, "Template variable %s: %s when %s '%s'",
logging.getLevelName(level).lower(),
msg,
action, action,
template, template,
) )
def _fail_with_undefined_error(self, *args, **kwargs): _log_fn = log_fn or _log_with_logger
try:
return super()._fail_with_undefined_error(*args, **kwargs)
except self._undefined_exception as ex:
template, action = template_cv.get() or ("", "rendering or compiling")
_LOGGER.error(
"Template variable error: %s when %s '%s'",
self._undefined_message,
action,
template,
)
raise ex
def __str__(self) -> str: class LoggingUndefined(jinja2.Undefined):
"""Log undefined __str___.""" """Log on undefined variables."""
self._log_message()
return super().__str__()
def __iter__(self): def _log_message(self) -> None:
"""Log undefined __iter___.""" _log_fn(logging.WARNING, self._undefined_message)
self._log_message()
return super().__iter__()
def __bool__(self) -> bool: def _fail_with_undefined_error(self, *args, **kwargs):
"""Log undefined __bool___.""" try:
self._log_message() return super()._fail_with_undefined_error(*args, **kwargs)
return super().__bool__() except self._undefined_exception as ex:
_log_fn(logging.ERROR, self._undefined_message)
raise ex
def __str__(self) -> str:
"""Log undefined __str___."""
self._log_message()
return super().__str__()
def __iter__(self):
"""Log undefined __iter___."""
self._log_message()
return super().__iter__()
def __bool__(self) -> bool:
"""Log undefined __bool___."""
self._log_message()
return super().__bool__()
return LoggingUndefined
async def async_load_custom_templates(hass: HomeAssistant) -> None: async def async_load_custom_templates(hass: HomeAssistant) -> None:
@ -2281,14 +2312,10 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment):
hass: HomeAssistant | None, hass: HomeAssistant | None,
limited: bool | None = False, limited: bool | None = False,
strict: bool | None = False, strict: bool | None = False,
log_fn: Callable[[int, str], None] | None = None,
) -> None: ) -> None:
"""Initialise template environment.""" """Initialise template environment."""
undefined: type[LoggingUndefined] | type[jinja2.StrictUndefined] super().__init__(undefined=make_logging_undefined(strict, log_fn))
if not strict:
undefined = LoggingUndefined
else:
undefined = jinja2.StrictUndefined
super().__init__(undefined=undefined)
self.hass = hass self.hass = hass
self.template_cache: weakref.WeakValueDictionary[ self.template_cache: weakref.WeakValueDictionary[
str | jinja2.nodes.Template, CodeType | str | None str | jinja2.nodes.Template, CodeType | str | None

View file

@ -2,6 +2,7 @@
import asyncio import asyncio
from copy import deepcopy from copy import deepcopy
import datetime import datetime
import logging
from unittest.mock import ANY, AsyncMock, Mock, patch from unittest.mock import ANY, AsyncMock, Mock, patch
import pytest import pytest
@ -33,7 +34,11 @@ from tests.common import (
async_mock_service, async_mock_service,
mock_platform, mock_platform,
) )
from tests.typing import ClientSessionGenerator, WebSocketGenerator from tests.typing import (
ClientSessionGenerator,
MockHAClientWebSocket,
WebSocketGenerator,
)
STATE_KEY_SHORT_NAMES = { STATE_KEY_SHORT_NAMES = {
"entity_id": "e", "entity_id": "e",
@ -1225,46 +1230,187 @@ async def test_render_template_manual_entity_ids_no_longer_needed(
} }
EMPTY_LISTENERS = {"all": False, "entities": [], "domains": [], "time": False}
ERR_MSG = {"type": "result", "success": False}
VARIABLE_ERROR_UNDEFINED_FUNC = {
"error": "'my_unknown_func' is undefined",
"level": "ERROR",
}
TEMPLATE_ERROR_UNDEFINED_FUNC = {
"code": "template_error",
"message": "UndefinedError: 'my_unknown_func' is undefined",
}
VARIABLE_WARNING_UNDEFINED_VAR = {
"error": "'my_unknown_var' is undefined",
"level": "WARNING",
}
TEMPLATE_ERROR_UNDEFINED_VAR = {
"code": "template_error",
"message": "UndefinedError: 'my_unknown_var' is undefined",
}
TEMPLATE_ERROR_UNDEFINED_FILTER = {
"code": "template_error",
"message": "TemplateAssertionError: No filter named 'unknown_filter'.",
}
@pytest.mark.parametrize( @pytest.mark.parametrize(
"template", ("template", "expected_events"),
[ [
"{{ my_unknown_func() + 1 }}", (
"{{ my_unknown_var }}", "{{ my_unknown_func() + 1 }}",
"{{ my_unknown_var + 1 }}", [
"{{ now() | unknown_filter }}", {"type": "event", "event": VARIABLE_ERROR_UNDEFINED_FUNC},
ERR_MSG | {"error": TEMPLATE_ERROR_UNDEFINED_FUNC},
],
),
(
"{{ my_unknown_var }}",
[
{"type": "event", "event": VARIABLE_WARNING_UNDEFINED_VAR},
{"type": "result", "success": True, "result": None},
{"type": "event", "event": VARIABLE_WARNING_UNDEFINED_VAR},
{
"type": "event",
"event": {"result": "", "listeners": EMPTY_LISTENERS},
},
],
),
(
"{{ my_unknown_var + 1 }}",
[ERR_MSG | {"error": TEMPLATE_ERROR_UNDEFINED_VAR}],
),
(
"{{ now() | unknown_filter }}",
[ERR_MSG | {"error": TEMPLATE_ERROR_UNDEFINED_FILTER}],
),
], ],
) )
async def test_render_template_with_error( async def test_render_template_with_error(
hass: HomeAssistant, websocket_client, caplog: pytest.LogCaptureFixture, template hass: HomeAssistant,
websocket_client: MockHAClientWebSocket,
caplog: pytest.LogCaptureFixture,
template: str,
expected_events: list[dict[str, str]],
) -> None: ) -> None:
"""Test a template with an error.""" """Test a template with an error."""
caplog.set_level(logging.INFO)
await websocket_client.send_json( await websocket_client.send_json(
{"id": 5, "type": "render_template", "template": template, "strict": True} {
"id": 5,
"type": "render_template",
"template": template,
"report_errors": True,
}
) )
msg = await websocket_client.receive_json() for expected_event in expected_events:
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
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 "TemplateError" not in caplog.text assert "TemplateError" not in caplog.text
@pytest.mark.parametrize( @pytest.mark.parametrize(
"template", ("template", "expected_events"),
[ [
"{{ my_unknown_func() + 1 }}", (
"{{ my_unknown_var }}", "{{ my_unknown_func() + 1 }}",
"{{ my_unknown_var + 1 }}", [
"{{ now() | unknown_filter }}", {"type": "event", "event": VARIABLE_ERROR_UNDEFINED_FUNC},
ERR_MSG | {"error": TEMPLATE_ERROR_UNDEFINED_FUNC},
],
),
(
"{{ my_unknown_var }}",
[
{"type": "event", "event": VARIABLE_WARNING_UNDEFINED_VAR},
{"type": "result", "success": True, "result": None},
{"type": "event", "event": VARIABLE_WARNING_UNDEFINED_VAR},
{
"type": "event",
"event": {"result": "", "listeners": EMPTY_LISTENERS},
},
],
),
(
"{{ my_unknown_var + 1 }}",
[ERR_MSG | {"error": TEMPLATE_ERROR_UNDEFINED_VAR}],
),
(
"{{ now() | unknown_filter }}",
[ERR_MSG | {"error": TEMPLATE_ERROR_UNDEFINED_FILTER}],
),
], ],
) )
async def test_render_template_with_timeout_and_error( async def test_render_template_with_timeout_and_error(
hass: HomeAssistant, websocket_client, caplog: pytest.LogCaptureFixture, template hass: HomeAssistant,
websocket_client,
caplog: pytest.LogCaptureFixture,
template: 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."""
caplog.set_level(logging.INFO)
await websocket_client.send_json(
{
"id": 5,
"type": "render_template",
"template": template,
"timeout": 5,
"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 }}",
[ERR_MSG | {"error": TEMPLATE_ERROR_UNDEFINED_FUNC}],
),
(
"{{ my_unknown_var }}",
[ERR_MSG | {"error": TEMPLATE_ERROR_UNDEFINED_VAR}],
),
(
"{{ my_unknown_var + 1 }}",
[ERR_MSG | {"error": TEMPLATE_ERROR_UNDEFINED_VAR}],
),
(
"{{ now() | unknown_filter }}",
[ERR_MSG | {"error": TEMPLATE_ERROR_UNDEFINED_FILTER}],
),
],
)
async def test_render_template_strict_with_timeout_and_error(
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."""
caplog.set_level(logging.INFO)
await websocket_client.send_json( await websocket_client.send_json(
{ {
"id": 5, "id": 5,
@ -1275,13 +1421,14 @@ async def test_render_template_with_timeout_and_error(
} }
) )
msg = await websocket_client.receive_json() for expected_event in expected_events:
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
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 "TemplateError" not in caplog.text assert "TemplateError" not in caplog.text
@ -1299,13 +1446,19 @@ async def test_render_template_error_in_template_code(
assert not msg["success"] assert not msg["success"]
assert msg["error"]["code"] == const.ERR_TEMPLATE_ERROR assert msg["error"]["code"] == const.ERR_TEMPLATE_ERROR
assert "Template variable error" not in caplog.text
assert "Template variable warning" not in caplog.text
assert "TemplateError" not in caplog.text assert "TemplateError" not 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:
"""Test a template with an error that only happens after a state change.""" """Test a template with an error that only happens after a state change.
In this test report_errors is enabled.
"""
caplog.set_level(logging.INFO)
hass.states.async_set("sensor.test", "on") hass.states.async_set("sensor.test", "on")
await hass.async_block_till_done() await hass.async_block_till_done()
@ -1318,12 +1471,16 @@ async def test_render_template_with_delayed_error(
""" """
await websocket_client.send_json( await websocket_client.send_json(
{"id": 5, "type": "render_template", "template": template_str} {
"id": 5,
"type": "render_template",
"template": template_str,
"report_errors": True,
}
) )
await hass.async_block_till_done() await hass.async_block_till_done()
msg = await websocket_client.receive_json() msg = await websocket_client.receive_json()
assert msg["id"] == 5 assert msg["id"] == 5
assert msg["type"] == const.TYPE_RESULT assert msg["type"] == const.TYPE_RESULT
assert msg["success"] assert msg["success"]
@ -1347,13 +1504,74 @@ async def test_render_template_with_delayed_error(
msg = await websocket_client.receive_json() msg = await websocket_client.receive_json()
assert msg["id"] == 5 assert msg["id"] == 5
assert msg["type"] == const.TYPE_RESULT assert msg["type"] == "event"
assert not msg["success"] event = msg["event"]
assert msg["error"]["code"] == const.ERR_TEMPLATE_ERROR assert event["error"] == "'None' has no attribute 'state'"
msg = await websocket_client.receive_json()
assert msg["id"] == 5
assert msg["type"] == "event"
event = msg["event"]
assert event == {"error": "UndefinedError: 'explode' is undefined"}
assert "Template variable error" not in caplog.text
assert "Template variable warning" not in caplog.text
assert "TemplateError" not in caplog.text assert "TemplateError" not in caplog.text
async def test_render_template_with_delayed_error_2(
hass: HomeAssistant, websocket_client, caplog: pytest.LogCaptureFixture
) -> None:
"""Test a template with an error that only happens after a state change.
In this test report_errors is disabled.
"""
hass.states.async_set("sensor.test", "on")
await hass.async_block_till_done()
template_str = """
{% if states.sensor.test.state %}
on
{% else %}
{{ explode + 1 }}
{% endif %}
"""
await websocket_client.send_json(
{
"id": 5,
"type": "render_template",
"template": template_str,
"report_errors": False,
}
)
await hass.async_block_till_done()
msg = await websocket_client.receive_json()
assert msg["id"] == 5
assert msg["type"] == const.TYPE_RESULT
assert msg["success"]
hass.states.async_remove("sensor.test")
await hass.async_block_till_done()
msg = await websocket_client.receive_json()
assert msg["id"] == 5
assert msg["type"] == "event"
event = msg["event"]
assert event == {
"result": "on",
"listeners": {
"all": False,
"domains": [],
"entities": ["sensor.test"],
"time": False,
},
}
assert "Template variable warning" in caplog.text
async def test_render_template_with_timeout( async def test_render_template_with_timeout(
hass: HomeAssistant, websocket_client, caplog: pytest.LogCaptureFixture hass: HomeAssistant, websocket_client, caplog: pytest.LogCaptureFixture
) -> None: ) -> None:

View file

@ -4466,15 +4466,25 @@ async def test_parse_result(hass: HomeAssistant) -> None:
assert template.Template(tpl, hass).async_render() == result assert template.Template(tpl, hass).async_render() == result
async def test_undefined_variable( @pytest.mark.parametrize(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture "template_string",
[
"{{ no_such_variable }}",
"{{ no_such_variable and True }}",
"{{ no_such_variable | join(', ') }}",
],
)
async def test_undefined_symbol_warnings(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
template_string: str,
) -> None: ) -> None:
"""Test a warning is logged on undefined variables.""" """Test a warning is logged on undefined variables."""
tpl = template.Template("{{ no_such_variable }}", hass) tpl = template.Template(template_string, hass)
assert tpl.async_render() == "" assert tpl.async_render() == ""
assert ( assert (
"Template variable warning: 'no_such_variable' is undefined when rendering " "Template variable warning: 'no_such_variable' is undefined when rendering "
"'{{ no_such_variable }}'" in caplog.text f"'{template_string}'" in caplog.text
) )