"""Test Home Assistant template helper methods.""" from datetime import datetime from unittest.mock import patch from freezegun import freeze_time import pytest import voluptuous as vol from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT from homeassistant.core import HomeAssistant from homeassistant.exceptions import TemplateError from homeassistant.helpers import entity_registry as er, template from homeassistant.helpers.template.render_info import ( ALL_STATES_RATE_LIMIT, DOMAIN_STATES_RATE_LIMIT, ) from homeassistant.util import dt as dt_util from .helpers import assert_result_info, render, render_to_info async def test_template_render_missing_hass(hass: HomeAssistant) -> None: """Test template render when hass is not set.""" hass.states.async_set("sensor.test", "23") template_obj = template.Template("{{ states('sensor.test') }}", None) template.render_info_cv.set(template.RenderInfo(template_obj)) with pytest.raises(RuntimeError, match="hass not set while rendering"): template_obj.async_render_to_info() async def test_template_render_info_collision(hass: HomeAssistant) -> None: """Test template render info collision. This usually means the template is being rendered in the wrong thread. """ hass.states.async_set("sensor.test", "23") template_obj = template.Template("{{ states('sensor.test') }}", None) template_obj.hass = hass template.render_info_cv.set(template.RenderInfo(template_obj)) with pytest.raises(RuntimeError, match="RenderInfo already set while rendering"): template_obj.async_render_to_info() def test_template_equality(hass: HomeAssistant) -> None: """Test template comparison and hashing.""" template_one = template.Template("{{ template_one }}", hass) template_one_1 = template.Template("{{ template_one }}", hass) template_two = template.Template("{{ template_two }}", hass) assert template_one == template_one_1 assert template_one != template_two assert hash(template_one) == hash(template_one_1) assert hash(template_one) != hash(template_two) assert str(template_one_1) == "Template" with pytest.raises(TypeError): template.Template(["{{ template_one }}"], hass) def test_invalid_template(hass: HomeAssistant) -> None: """Invalid template raises error.""" tmpl = template.Template("{{", hass) with pytest.raises(TemplateError): tmpl.ensure_valid() with pytest.raises(TemplateError): tmpl.async_render() info = tmpl.async_render_to_info() with pytest.raises(TemplateError): assert info.result() == "impossible" tmpl = template.Template("{{states(keyword)}}", hass) tmpl.ensure_valid() with pytest.raises(TemplateError): tmpl.async_render() def test_raise_exception_on_error(hass: HomeAssistant) -> None: """Test raising an exception on error.""" with pytest.raises(TemplateError): template.Template("{{ invalid_syntax", hass).ensure_valid() async def test_import(hass: HomeAssistant) -> None: """Test that imports work from the config/custom_templates folder.""" await template.async_load_custom_templates(hass) assert "test.jinja" in template._get_hass_loader(hass).sources assert "inner/inner_test.jinja" in template._get_hass_loader(hass).sources assert ( render( hass, """ {% import 'test.jinja' as t %} {{ t.test_macro() }} {{ t.test_variable }} """, ) == "macro variable" ) assert ( render( hass, """ {% import 'inner/inner_test.jinja' as t %} {{ t.test_macro() }} {{ t.test_variable }} """, ) == "inner macro inner variable" ) with pytest.raises(TemplateError): render( hass, """ {% import 'notfound.jinja' as t %} {{ t.test_macro() }} {{ t.test_variable }} """, ) async def test_import_change(hass: HomeAssistant) -> None: """Test that a change in HassLoader results in updated imports.""" await template.async_load_custom_templates(hass) to_test = template.Template( """ {% import 'test.jinja' as t %} {{ t.test_macro() }} {{ t.test_variable }} """, hass, ) assert to_test.async_render() == "macro variable" template._get_hass_loader(hass).sources = { "test.jinja": """ {% macro test_macro() -%} macro2 {%- endmacro %} {% set test_variable = "variable2" %} """ } assert to_test.async_render() == "macro2 variable2" def test_loop_controls(hass: HomeAssistant) -> None: """Test that loop controls are enabled.""" tpl = """ {%- for v in range(10) %} {%- if v == 1 -%} {%- continue -%} {%- elif v == 3 -%} {%- break -%} {%- endif -%} {{ v }} {%- endfor -%} """ assert render(hass, tpl) == "02" def test_converting_datetime_to_iterable(hass: HomeAssistant) -> None: """Test converting a datetime to an iterable raises an error.""" dt_ = datetime(2020, 1, 1, 0, 0, 0) with pytest.raises(TemplateError): render(hass, "{{ tuple(value) }}", {"value": dt_}) with pytest.raises(TemplateError): render(hass, "{{ set(value) }}", {"value": dt_}) def test_passing_vars_as_keywords(hass: HomeAssistant) -> None: """Test passing variables as keywords.""" assert render(hass, "{{ hello }}", hello=127) == 127 def test_passing_vars_as_vars(hass: HomeAssistant) -> None: """Test passing variables as variables.""" assert render(hass, "{{ hello }}", {"hello": 127}) == 127 def test_passing_vars_as_list(hass: HomeAssistant) -> None: """Test passing variables as list.""" assert template.render_complex( template.Template("{{ hello }}", hass), {"hello": ["foo", "bar"]} ) == ["foo", "bar"] def test_passing_vars_as_list_element(hass: HomeAssistant) -> None: """Test passing variables as list.""" tpl = template.Template("{{ hello[1] }}", hass) assert template.render_complex(tpl, {"hello": ["foo", "bar"]}) == "bar" def test_passing_vars_as_dict_element(hass: HomeAssistant) -> None: """Test passing variables as list.""" tpl = template.Template("{{ hello.foo }}", hass) assert template.render_complex(tpl, {"hello": {"foo": "bar"}}) == "bar" def test_passing_vars_as_dict(hass: HomeAssistant) -> None: """Test passing variables as list.""" tpl = template.Template("{{ hello }}", hass) assert template.render_complex(tpl, {"hello": {"foo": "bar"}}) == {"foo": "bar"} def test_render_with_possible_json_value_with_valid_json(hass: HomeAssistant) -> None: """Render with possible JSON value with valid JSON.""" tpl = template.Template("{{ value_json.hello }}", hass) assert tpl.async_render_with_possible_json_value('{"hello": "world"}') == "world" def test_render_with_possible_json_value_with_invalid_json(hass: HomeAssistant) -> None: """Render with possible JSON value with invalid JSON.""" tpl = template.Template("{{ value_json }}", hass) assert tpl.async_render_with_possible_json_value("{ I AM NOT JSON }") == "" def test_render_with_possible_json_value_with_template_error_value( hass: HomeAssistant, ) -> None: """Render with possible JSON value with template error value.""" tpl = template.Template("{{ non_existing.variable }}", hass) assert tpl.async_render_with_possible_json_value("hello", "-") == "-" def test_render_with_possible_json_value_with_missing_json_value( hass: HomeAssistant, ) -> None: """Render with possible JSON value with unknown JSON object.""" tpl = template.Template("{{ value_json.goodbye }}", hass) assert tpl.async_render_with_possible_json_value('{"hello": "world"}') == "" def test_render_with_possible_json_value_non_string_value(hass: HomeAssistant) -> None: """Render with possible JSON value with non-string value.""" tpl = template.Template( """{{ strptime(value~'+0000', '%Y-%m-%d %H:%M:%S%z') }}""", hass, ) value = datetime(2019, 1, 18, 12, 13, 14) expected = str(value.replace(tzinfo=dt_util.UTC)) assert tpl.async_render_with_possible_json_value(value) == expected def test_render_with_possible_json_value_and_parse_result(hass: HomeAssistant) -> None: """Render with possible JSON value with valid JSON.""" tpl = template.Template("{{ value_json.hello }}", hass) result = tpl.async_render_with_possible_json_value( """{"hello": {"world": "value1"}}""", parse_result=True ) assert isinstance(result, dict) def test_render_with_possible_json_value_and_dont_parse_result( hass: HomeAssistant, ) -> None: """Render with possible JSON value with valid JSON.""" tpl = template.Template("{{ value_json.hello }}", hass) result = tpl.async_render_with_possible_json_value( """{"hello": {"world": "value1"}}""", parse_result=False ) assert isinstance(result, str) @patch( "homeassistant.helpers.template.TemplateEnvironment.is_safe_callable", return_value=True, ) def test_timedelta(mock_is_safe, hass: HomeAssistant) -> None: """Test relative_time method.""" now = datetime.strptime("2000-01-01 10:00:00 +00:00", "%Y-%m-%d %H:%M:%S %z") with freeze_time(now): result = render(hass, "{{timedelta(seconds=120)}}") assert result == "0:02:00" result = render(hass, "{{timedelta(seconds=86400)}}") assert result == "1 day, 0:00:00" result = render(hass, "{{timedelta(days=1, hours=4)}}") assert result == "1 day, 4:00:00" result = render(hass, "{{relative_time(now() - timedelta(seconds=3600))}}") assert result == "1 hour" result = render(hass, "{{relative_time(now() - timedelta(seconds=86400))}}") assert result == "1 day" result = render(hass, "{{relative_time(now() - timedelta(seconds=86401))}}") assert result == "1 day" result = render(hass, "{{relative_time(now() - timedelta(weeks=2, days=1))}}") assert result == "15 days" def test_async_render_to_info_with_branching(hass: HomeAssistant) -> None: """Test async_render_to_info function by domain.""" hass.states.async_set("light.a", "off") hass.states.async_set("light.b", "on") hass.states.async_set("light.c", "off") info = render_to_info( hass, """ {% if states.light.a == "on" %} {{ states.light.b.state }} {% else %} {{ states.light.c.state }} {% endif %} """, ) assert_result_info(info, "off", {"light.a", "light.c"}) assert info.rate_limit is None info = render_to_info( hass, """ {% if states.light.a.state == "off" %} {% set domain = "light" %} {{ states[domain].b.state }} {% endif %} """, ) assert_result_info(info, "on", {"light.a", "light.b"}) assert info.rate_limit is None def test_async_render_to_info_with_complex_branching(hass: HomeAssistant) -> None: """Test async_render_to_info function by domain.""" hass.states.async_set("light.a", "off") hass.states.async_set("light.b", "on") hass.states.async_set("light.c", "off") hass.states.async_set("vacuum.a", "off") hass.states.async_set("device_tracker.a", "off") hass.states.async_set("device_tracker.b", "off") hass.states.async_set("lock.a", "off") hass.states.async_set("sensor.a", "off") hass.states.async_set("binary_sensor.a", "off") info = render_to_info( hass, """ {% set domain = "vacuum" %} {% if states.light.a == "on" %} {{ states.light.b.state }} {% elif states.light.a == "on" %} {{ states.device_tracker }} {% elif states.light.a == "on" %} {{ states[domain] | list }} {% elif states('light.b') == "on" %} {{ states[otherdomain] | sort(attribute='entity_id') | map(attribute='entity_id') | list }} {% elif states.light.a == "on" %} {{ states["nonexist"] | list }} {% else %} else {% endif %} """, {"otherdomain": "sensor"}, ) assert_result_info(info, ["sensor.a"], {"light.a", "light.b"}, {"sensor"}) assert info.rate_limit == DOMAIN_STATES_RATE_LIMIT async def test_async_render_to_info_with_wildcard_matching_entity_id( hass: HomeAssistant, ) -> None: """Test tracking template with a wildcard.""" template_complex_str = r""" {% for state in states.cover %} {% if 'office_' in state.entity_id %} {{ state.entity_id }}={{ state.state }} {% endif %} {% endfor %} """ hass.states.async_set("cover.office_drapes", "closed") hass.states.async_set("cover.office_window", "closed") hass.states.async_set("cover.office_skylight", "open") info = render_to_info(hass, template_complex_str) assert info.domains == {"cover"} assert info.entities == set() assert info.all_states is False assert info.rate_limit == DOMAIN_STATES_RATE_LIMIT async def test_async_render_to_info_with_wildcard_matching_state( hass: HomeAssistant, ) -> None: """Test tracking template with a wildcard.""" template_complex_str = """ {% for state in states %} {% if state.state.startswith('ope') %} {{ state.entity_id }}={{ state.state }} {% endif %} {% endfor %} """ hass.states.async_set("cover.office_drapes", "closed") hass.states.async_set("cover.office_window", "closed") hass.states.async_set("cover.office_skylight", "open") hass.states.async_set("cover.x_skylight", "open") hass.states.async_set("binary_sensor.door", "on") await hass.async_block_till_done() info = render_to_info(hass, template_complex_str) assert not info.domains assert info.entities == set() assert info.all_states is True assert info.rate_limit == ALL_STATES_RATE_LIMIT hass.states.async_set("binary_sensor.door", "off") info = render_to_info(hass, template_complex_str) assert not info.domains assert info.entities == set() assert info.all_states is True assert info.rate_limit == ALL_STATES_RATE_LIMIT template_cover_str = """ {% for state in states.cover %} {% if state.state.startswith('ope') %} {{ state.entity_id }}={{ state.state }} {% endif %} {% endfor %} """ hass.states.async_set("cover.x_skylight", "closed") info = render_to_info(hass, template_cover_str) assert info.domains == {"cover"} assert info.entities == set() assert info.all_states is False assert info.rate_limit == DOMAIN_STATES_RATE_LIMIT def test_nested_async_render_to_info_case(hass: HomeAssistant) -> None: """Test a deeply nested state with async_render_to_info.""" hass.states.async_set("input_select.picker", "vacuum.a") hass.states.async_set("vacuum.a", "off") info = render_to_info( hass, "{{ states[states['input_select.picker'].state].state }}", {} ) assert_result_info(info, "off", {"input_select.picker", "vacuum.a"}) assert info.rate_limit is None def test_result_as_boolean(hass: HomeAssistant) -> None: """Test converting a template result to a boolean.""" assert template.result_as_boolean(True) is True assert template.result_as_boolean(" 1 ") is True assert template.result_as_boolean(" true ") is True assert template.result_as_boolean(" TrUE ") is True assert template.result_as_boolean(" YeS ") is True assert template.result_as_boolean(" On ") is True assert template.result_as_boolean(" Enable ") is True assert template.result_as_boolean(1) is True assert template.result_as_boolean(-1) is True assert template.result_as_boolean(500) is True assert template.result_as_boolean(0.5) is True assert template.result_as_boolean(0.389) is True assert template.result_as_boolean(35) is True assert template.result_as_boolean(False) is False assert template.result_as_boolean(" 0 ") is False assert template.result_as_boolean(" false ") is False assert template.result_as_boolean(" FaLsE ") is False assert template.result_as_boolean(" no ") is False assert template.result_as_boolean(" off ") is False assert template.result_as_boolean(" disable ") is False assert template.result_as_boolean(0) is False assert template.result_as_boolean(0.0) is False assert template.result_as_boolean("0.00") is False assert template.result_as_boolean(None) is False async def test_async_render_to_info_in_conditional(hass: HomeAssistant) -> None: """Test extract entities function with none entities stuff.""" info = render_to_info(hass, '{{ states("sensor.xyz") == "dog" }}') assert_result_info(info, False, ["sensor.xyz"], []) hass.states.async_set("sensor.xyz", "dog") hass.states.async_set("sensor.cow", "True") await hass.async_block_till_done() template_str = """ {% if states("sensor.xyz") == "dog" %} {{ states("sensor.cow") }} {% else %} {{ states("sensor.pig") }} {% endif %} """ info = render_to_info(hass, template_str) assert_result_info(info, True, ["sensor.xyz", "sensor.cow"], []) hass.states.async_set("sensor.xyz", "sheep") hass.states.async_set("sensor.pig", "oink") await hass.async_block_till_done() info = render_to_info(hass, template_str) assert_result_info(info, "oink", ["sensor.xyz", "sensor.pig"], []) def test_jinja_namespace(hass: HomeAssistant) -> None: """Test Jinja's namespace command can be used.""" test_template = template.Template( ( "{% set ns = namespace(a_key='') %}" "{% set ns.a_key = states.sensor.dummy.state %}" "{{ ns.a_key }}" ), hass, ) hass.states.async_set("sensor.dummy", "a value") assert test_template.async_render() == "a value" hass.states.async_set("sensor.dummy", "another value") assert test_template.async_render() == "another value" @pytest.mark.parametrize( ("rounded", "with_unit", "output1_1", "output1_2", "output2_1", "output2_2"), [ (False, False, 23, 23.015, 23, 23.015), (False, True, "23 beers", "23.015 beers", "23 beers", "23.015 beers"), (True, False, 23.0, 23.02, 23, 23.015), (True, True, "23.00 beers", "23.02 beers", "23 beers", "23.015 beers"), ], ) def test_state_with_unit_and_rounding_options( hass: HomeAssistant, entity_registry: er.EntityRegistry, rounded: str, with_unit: str, output1_1, output1_2, output2_1, output2_2, ) -> None: """Test formatting the state rounded and with unit.""" entry = entity_registry.async_get_or_create( "sensor", "test", "very_unique", suggested_object_id="test" ) entity_registry.async_update_entity_options( entry.entity_id, "sensor", { "suggested_display_precision": 2, }, ) assert entry.entity_id == "sensor.test" hass.states.async_set("sensor.test", "23", {ATTR_UNIT_OF_MEASUREMENT: "beers"}) hass.states.async_set("sensor.test2", "23", {ATTR_UNIT_OF_MEASUREMENT: "beers"}) tpl = template.Template( f"{{{{ states('sensor.test', rounded={rounded}, with_unit={with_unit}) }}}}", hass, ) tpl2 = template.Template( f"{{{{ states('sensor.test2', rounded={rounded}, with_unit={with_unit}) }}}}", hass, ) assert tpl.async_render() == output1_1 assert tpl2.async_render() == output2_1 hass.states.async_set("sensor.test", "23.015", {ATTR_UNIT_OF_MEASUREMENT: "beers"}) hass.states.async_set("sensor.test2", "23.015", {ATTR_UNIT_OF_MEASUREMENT: "beers"}) assert tpl.async_render() == output1_2 assert tpl2.async_render() == output2_2 def test_render_complex_handling_non_template_values(hass: HomeAssistant) -> None: """Test that we can render non-template fields.""" assert template.render_complex( {True: 1, False: template.Template("{{ hello }}", hass)}, {"hello": 2} ) == {True: 1, False: 2} async def test_cache_garbage_collection(hass: HomeAssistant) -> None: """Test caching a template.""" template_string = ( "{% set dict = {'foo': 'x&y', 'bar': 42} %} {{ dict | urlencode }}" ) tpl = template.Template( (template_string), hass, ) tpl.ensure_valid() env = tpl._env assert env.template_cache.get(template_string) tpl2 = template.Template( (template_string), hass, ) tpl2.ensure_valid() assert env.template_cache.get(template_string) del tpl assert env.template_cache.get(template_string) del tpl2 assert not env.template_cache.get(template_string) def test_is_template_string() -> None: """Test is template string.""" assert template.is_template_string("{{ x }}") is True assert template.is_template_string("{% if x == 2 %}1{% else %}0{%end if %}") is True assert template.is_template_string("{# a comment #} Hey") is True assert template.is_template_string("1") is False assert template.is_template_string("Some Text") is False async def test_protected_blocked(hass: HomeAssistant) -> None: """Test accessing __getattr__ produces a template error.""" with pytest.raises(TemplateError): render(hass, '{{ states.__getattr__("any") }}') with pytest.raises(TemplateError): render(hass, '{{ states.sensor.__getattr__("any") }}') with pytest.raises(TemplateError): render(hass, '{{ states.sensor.any.__getattr__("any") }}') async def test_demo_template(hass: HomeAssistant) -> None: """Test the demo template works as expected.""" hass.states.async_set( "sun.sun", "above", {"elevation": 50, "next_rising": "2022-05-12T03:00:08.503651+00:00"}, ) for i in range(2): hass.states.async_set(f"sensor.sensor{i}", "on") demo_template_str = """ {## Imitate available variables: ##} {% set my_test_json = { "temperature": 25, "unit": "°C" } %} The temperature is {{ my_test_json.temperature }} {{ my_test_json.unit }}. {% if is_state("sun.sun", "above_horizon") -%} The sun rose {{ relative_time(states.sun.sun.last_changed) }} ago. {%- else -%} The sun will rise at {{ as_timestamp(state_attr("sun.sun", "next_rising")) | timestamp_local }}. {%- endif %} For loop example getting 3 entity values: {% for states in states | slice(3) -%} {% set state = states | first %} {%- if loop.first %}The {% elif loop.last %} and the {% else %}, the {% endif -%} {{ state.name | lower }} is {{state.state_with_unit}} {%- endfor %}. """ result = render(hass, demo_template_str) assert "The temperature is 25" in result assert "is on" in result assert "sensor0" in result assert "sensor1" in result assert "sun" in result async def test_lifecycle(hass: HomeAssistant) -> None: """Test that we limit template render info for lifecycle events.""" hass.states.async_set("sun.sun", "above", {"elevation": 50, "next_rising": "later"}) for i in range(2): hass.states.async_set(f"sensor.sensor{i}", "on") hass.states.async_set("sensor.removed", "off") await hass.async_block_till_done() hass.states.async_set("sun.sun", "below", {"elevation": 60, "next_rising": "later"}) for i in range(2): hass.states.async_set(f"sensor.sensor{i}", "off") hass.states.async_set("sensor.new", "off") hass.states.async_remove("sensor.removed") await hass.async_block_till_done() info = render_to_info(hass, "{{ states | count }}") assert info.all_states is False assert info.all_states_lifecycle is True assert info.rate_limit is None assert info.has_time is False assert info.entities == set() assert info.domains == set() assert info.domains_lifecycle == set() assert info.filter("sun.sun") is False assert info.filter("sensor.sensor1") is False assert info.filter_lifecycle("sensor.new") is True assert info.filter_lifecycle("sensor.removed") is True async def test_template_timeout(hass: HomeAssistant) -> None: """Test to see if a template will timeout.""" for i in range(2): hass.states.async_set(f"sensor.sensor{i}", "on") tmp = template.Template("{{ states | count }}", hass) assert await tmp.async_render_will_timeout(3) is False tmp3 = template.Template("static", hass) assert await tmp3.async_render_will_timeout(3) is False tmp4 = template.Template("{{ var1 }}", hass) assert await tmp4.async_render_will_timeout(3, {"var1": "ok"}) is False slow_template_str = """ {% for var in range(1000) -%} {% for var in range(1000) -%} {{ var }} {%- endfor %} {%- endfor %} """ tmp5 = template.Template(slow_template_str, hass) assert await tmp5.async_render_will_timeout(0.000001) is True async def test_template_timeout_raise(hass: HomeAssistant) -> None: """Test we can raise from.""" tmp2 = template.Template("{{ error_invalid + 1 }}", hass) with pytest.raises(TemplateError): assert await tmp2.async_render_will_timeout(3) is False async def test_lights(hass: HomeAssistant) -> None: """Test we can sort lights.""" tmpl = """ {% set lights_on = states.light |selectattr('state','eq','on') |sort(attribute='entity_id') |map(attribute='name')|list %} {% if lights_on|length == 0 %} No lights on. Sleep well.. {% elif lights_on|length == 1 %} The {{lights_on[0]}} light is on. {% elif lights_on|length == 2 %} The {{lights_on[0]}} and {{lights_on[1]}} lights are on. {% else %} The {{lights_on[:-1]|join(', ')}}, and {{lights_on[-1]}} lights are on. {% endif %} """ states = [] for i in range(10): states.append(f"light.sensor{i}") hass.states.async_set(f"light.sensor{i}", "on") info = render_to_info(hass, tmpl) assert info.entities == set() assert info.domains == {"light"} assert "lights are on" in info.result() for i in range(10): assert f"sensor{i}" in info.result() async def test_template_errors(hass: HomeAssistant) -> None: """Test template rendering wraps exceptions with TemplateError.""" with pytest.raises(TemplateError): render(hass, "{{ now() | rando }}") with pytest.raises(TemplateError): render(hass, "{{ utcnow() | rando }}") with pytest.raises(TemplateError): render(hass, "{{ now() | random }}") with pytest.raises(TemplateError): render(hass, "{{ utcnow() | random }}") async def test_no_result_parsing(hass: HomeAssistant) -> None: """Test if templates results are not parsed.""" hass.states.async_set("sensor.temperature", "12") assert ( render(hass, "{{ states.sensor.temperature.state }}", parse_result=False) == "12" ) assert render(hass, "{{ false }}", parse_result=False) == "False" assert render(hass, "{{ [1, 2, 3] }}", parse_result=False) == "[1, 2, 3]" async def test_is_static_still_ast_evals(hass: HomeAssistant) -> None: """Test is_static still converts to native type.""" tpl = template.Template("[1, 2]", hass) assert tpl.is_static assert tpl.async_render() == [1, 2] async def test_result_wrappers(hass: HomeAssistant) -> None: """Test result wrappers.""" for text, native, orig_type, schema in ( ("[1, 2]", [1, 2], list, vol.Schema([int])), ("{1, 2}", {1, 2}, set, vol.Schema({int})), ("(1, 2)", (1, 2), tuple, vol.ExactSequence([int, int])), ('{"hello": True}', {"hello": True}, dict, vol.Schema({"hello": bool})), ): result = render(hass, text) assert isinstance(result, orig_type) assert isinstance(result, template.ResultWrapper) assert result == native assert result.render_result == text schema(result) # should not raise # Result with render text stringifies to original text assert str(result) == text # Result without render text stringifies same as original type assert str(template.RESULT_WRAPPERS[orig_type](native)) == str( orig_type(native) ) async def test_parse_result(hass: HomeAssistant) -> None: """Test parse result.""" for tpl, result in ( ('{{ "{{}}" }}', "{{}}"), ("not-something", "not-something"), ("2a", "2a"), ("123E5", "123E5"), ("1j", "1j"), ("1e+100", "1e+100"), ("0xface", "0xface"), ("123", 123), ("10", 10), ("123.0", 123.0), (".5", 0.5), ("0.5", 0.5), ("-1", -1), ("-1.0", -1.0), ("+1", 1), ("5.", 5.0), ("123_123_123", "123_123_123"), # ("+48100200300", "+48100200300"), # phone number ("010", "010"), ("0011101.00100001010001", "0011101.00100001010001"), ): assert render(hass, tpl) == result @pytest.mark.parametrize( "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: """Test a warning is logged on undefined variables.""" assert render(hass, template_string) == "" assert ( f"Template variable warning: 'no_such_variable' is" f" undefined when rendering '{template_string}'" in caplog.text ) async def test_render_to_info_with_exception(hass: HomeAssistant) -> None: """Test info is still available if the template has an exception.""" hass.states.async_set("test_domain.object", "dog") info = render_to_info(hass, '{{ states("test_domain.object") | float }}') with pytest.raises(TemplateError, match="no default was specified"): info.result() assert info.all_states is False assert info.entities == {"test_domain.object"} async def test_template_thread_safety_checks(hass: HomeAssistant) -> None: """Test template thread safety checks.""" hass.states.async_set("sensor.test", "23") template_str = "{{ states('sensor.test') }}" template_obj = template.Template(template_str, None) template_obj.hass = hass hass.config.debug = True with pytest.raises( RuntimeError, match="Detected code that calls async_render_to_info from a thread.", ): await hass.async_add_executor_job(template_obj.async_render_to_info) assert template_obj.async_render_to_info().result() == 23 def test_template_output_exceeds_maximum_size(hass: HomeAssistant) -> None: """Test template output exceeds maximum size.""" with pytest.raises(TemplateError): render(hass, "{{ 'a' * 1024 * 257 }}")