Files
core/tests/helpers/template/test_render_info.py
T

244 lines
7.4 KiB
Python

"""Test template render information tracking for Home Assistant."""
from collections.abc import Callable
from typing import Any
import pytest
from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.exceptions import TemplateError
from homeassistant.helpers import template
from homeassistant.helpers.template.render_info import (
ALL_STATES_RATE_LIMIT,
DOMAIN_STATES_RATE_LIMIT,
RenderInfo,
_false,
_true,
render_info_cv,
)
from homeassistant.setup import async_setup_component
@pytest.fixture
def template_obj(hass: HomeAssistant) -> template.Template:
"""Template object for test_render_info."""
return template.Template("{{ 1 + 1 }}", hass)
def test_render_info_initialization(template_obj: template.Template) -> None:
"""Test RenderInfo initialization."""
info = RenderInfo(template_obj)
assert info.template is template_obj
assert info.collecting is True
assert info._result is None
assert info.is_static is False
assert info.exception is None
assert info.all_states is False
assert info.all_states_lifecycle is False
assert info.domains == set()
assert info.domains_lifecycle == set()
assert info.entities == set()
assert info.rate_limit is None
assert info.has_time is False
assert info.filter_lifecycle is _true
assert info.filter is _true
def test_render_info_repr(template_obj: template.Template) -> None:
"""Test RenderInfo representation."""
info = RenderInfo(template_obj)
info.domains.add("sensor")
info.entities.add("sensor.test")
repr_str = repr(info)
assert "RenderInfo" in repr_str
assert "domains={'sensor'}" in repr_str
assert "entities={'sensor.test'}" in repr_str
def test_render_info_result(template_obj: template.Template) -> None:
"""Test RenderInfo result property."""
info = RenderInfo(template_obj)
# Test with no result set - should return None cast as str
assert info.result() is None
# Test with result set
info._result = "test_result"
assert info.result() == "test_result"
# Test with exception
info.exception = TemplateError("Test error")
with pytest.raises(TemplateError, match="Test error"):
info.result()
def test_render_info_filter_domains_and_entities(
template_obj: template.Template,
) -> None:
"""Test RenderInfo entity and domain filtering."""
info = RenderInfo(template_obj)
# Add domain and entity
info.domains.add("sensor")
info.entities.add("light.test")
# Should match domain
assert info._filter_domains_and_entities("sensor.temperature") is True
# Should match entity
assert info._filter_domains_and_entities("light.test") is True
# Should not match
assert info._filter_domains_and_entities("switch.kitchen") is False
def test_render_info_filter_entities(template_obj: template.Template) -> None:
"""Test RenderInfo entity-only filtering."""
info = RenderInfo(template_obj)
info.entities.add("sensor.test")
assert info._filter_entities("sensor.test") is True
assert info._filter_entities("sensor.other") is False
def test_render_info_filter_lifecycle_domains(template_obj: template.Template) -> None:
"""Test RenderInfo domain lifecycle filtering."""
info = RenderInfo(template_obj)
info.domains_lifecycle.add("sensor")
assert info._filter_lifecycle_domains("sensor.test") is True
assert info._filter_lifecycle_domains("light.test") is False
def test_render_info_freeze_static(template_obj: template.Template) -> None:
"""Test RenderInfo static freezing."""
info = RenderInfo(template_obj)
info.domains.add("sensor")
info.entities.add("sensor.test")
info.all_states = True
info._freeze_static()
assert info.is_static is True
assert info.all_states is False
assert isinstance(info.domains, frozenset)
assert isinstance(info.entities, frozenset)
def test_render_info_freeze(template_obj: template.Template) -> None:
"""Test RenderInfo freezing with rate limits."""
info = RenderInfo(template_obj)
# Test all_states rate limit
info.all_states = True
info._freeze()
assert info.rate_limit == ALL_STATES_RATE_LIMIT
# Test domain rate limit
info = RenderInfo(template_obj)
info.domains.add("sensor")
info._freeze()
assert info.rate_limit == DOMAIN_STATES_RATE_LIMIT
# Test exception rate limit
info = RenderInfo(template_obj)
info.exception = TemplateError("Test")
info._freeze()
assert info.rate_limit == ALL_STATES_RATE_LIMIT
def test_render_info_freeze_filters(template_obj: template.Template) -> None:
"""Test RenderInfo filter assignment during freeze."""
# Test lifecycle filter assignment
info = RenderInfo(template_obj)
info.domains_lifecycle.add("sensor")
info._freeze()
assert info.filter_lifecycle == info._filter_lifecycle_domains
# Test no lifecycle domains
info = RenderInfo(template_obj)
info._freeze()
assert info.filter_lifecycle is _false
# Test domain and entity filter
info = RenderInfo(template_obj)
info.domains.add("sensor")
info._freeze()
assert info.filter == info._filter_domains_and_entities
# Test entity-only filter
info = RenderInfo(template_obj)
info.entities.add("sensor.test")
info._freeze()
assert info.filter == info._filter_entities
# Test no domains or entities
info = RenderInfo(template_obj)
info._freeze()
assert info.filter is _false
def test_render_info_context_var(template_obj: template.Template) -> None:
"""Test render_info_cv context variable."""
# Should start as None
assert render_info_cv.get() is None
# Test setting and getting
info = RenderInfo(template_obj)
render_info_cv.set(info)
assert render_info_cv.get() is info
# Reset for other tests
render_info_cv.set(None)
assert render_info_cv.get() is None
@pytest.mark.parametrize(
"render",
[
pytest.param(
lambda tpl: tpl.async_render_to_info().result(), id="render_to_info"
),
pytest.param(lambda tpl: tpl.async_render(), id="render"),
],
)
async def test_render_info_does_not_escape_into_scheduled_work(
hass: HomeAssistant,
render: Callable[[template.Template], Any],
) -> None:
"""Test the render in flight does not escape into work scheduled during it.
An error logged while a template renders makes `system_log` fire an event,
and firing hands the dispatch to the event loop together with a copy of the
context of the render. Renders made from that copy used to collect into the
render that had already finished, or raise "RenderInfo already set", and the
error logged for that fired another event carrying the same copy, so the
loop sustained itself.
"""
assert await async_setup_component(
hass, "system_log", {"system_log": {"fire_event": True}}
)
hass.states.async_set("sensor.test", "ok")
renders: list[str] = []
@callback
def _render_on_event(event: Event) -> None:
renders.append(render(template.Template("{{ states('sensor.test') }}", hass)))
hass.bus.async_listen("system_log_event", _render_on_event)
# Calling an undefined variable logs an error from inside the render
with pytest.raises(TemplateError):
template.Template(
"{{ nope.startswith('x') }}", hass
).async_render_to_info().result()
await hass.async_block_till_done()
assert renders == ["ok"]