1
0
mirror of https://github.com/home-assistant/core.git synced 2026-05-17 22:10:57 +01:00
Files
core/tests/helpers/template/extensions/test_functional.py
T

522 lines
17 KiB
Python

"""Test functional utility functions for Home Assistant templates."""
from __future__ import annotations
import random
from unittest.mock import MagicMock, patch
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import TemplateError
from homeassistant.helpers import template
from tests.helpers.template.helpers import render
def test_apply(hass: HomeAssistant) -> None:
"""Test apply."""
tpl = """
{%- macro add_foo(arg) -%}
{{arg}}foo
{%- endmacro -%}
{{ ["a", "b", "c"] | map('apply', add_foo) | list }}
"""
assert render(hass, tpl) == ["afoo", "bfoo", "cfoo"]
assert render(
hass, "{{ ['1', '2', '3', '4', '5'] | map('apply', int) | list }}"
) == [1, 2, 3, 4, 5]
def test_apply_macro_with_arguments(hass: HomeAssistant) -> None:
"""Test apply macro with positional, named, and mixed arguments."""
# Test macro with positional arguments
tpl = """
{%- macro add_numbers(a, b, c) -%}
{{ a + b + c }}
{%- endmacro -%}
{{ apply(5, add_numbers, 10, 15) }}
"""
assert render(hass, tpl) == 30
# Test macro with named arguments
tpl = """
{%- macro greet(name, greeting="Hello") -%}
{{ greeting }}, {{ name }}!
{%- endmacro -%}
{{ apply("World", greet, greeting="Hi") }}
"""
assert render(hass, tpl) == "Hi, World!"
# Test macro with mixed arguments
tpl = """
{%- macro format_message(prefix, name, suffix="!") -%}
{{ prefix }} {{ name }}{{ suffix }}
{%- endmacro -%}
{{ apply("Welcome", format_message, "John", suffix="...") }}
"""
assert render(hass, tpl) == "Welcome John..."
def test_as_function(hass: HomeAssistant) -> None:
"""Test as_function."""
tpl = """
{%- macro macro_double(num, returns) -%}
{%- do returns(num * 2) -%}
{%- endmacro -%}
{%- set double = macro_double | as_function -%}
{{ double(5) }}
"""
assert render(hass, tpl) == 10
def test_as_function_no_arguments(hass: HomeAssistant) -> None:
"""Test as_function with no arguments."""
tpl = """
{%- macro macro_get_hello(returns) -%}
{%- do returns("Hello") -%}
{%- endmacro -%}
{%- set get_hello = macro_get_hello | as_function -%}
{{ get_hello() }}
"""
assert render(hass, tpl) == "Hello"
def test_ord(hass: HomeAssistant) -> None:
"""Test the ord filter."""
assert render(hass, '{{ "d" | ord }}') == 100
@patch.object(random, "choice")
def test_random_every_time(test_choice: MagicMock, hass: HomeAssistant) -> None:
"""Ensure the random filter runs every time, not just once."""
tpl = template.Template("{{ [1,2] | random }}", hass)
test_choice.return_value = "foo"
assert tpl.async_render() == "foo"
test_choice.return_value = "bar"
assert tpl.async_render() == "bar"
def test_render_with_possible_json_value_valid_with_is_defined(
hass: HomeAssistant,
) -> None:
"""Render with possible JSON value with known JSON object."""
tpl = template.Template("{{ value_json.hello|is_defined }}", hass)
assert tpl.async_render_with_possible_json_value('{"hello": "world"}') == "world"
def test_render_with_possible_json_value_undefined_json(hass: HomeAssistant) -> None:
"""Render with possible JSON value with unknown JSON object."""
tpl = template.Template("{{ value_json.bye|is_defined }}", hass)
assert (
tpl.async_render_with_possible_json_value('{"hello": "world"}')
== '{"hello": "world"}'
)
def test_render_with_possible_json_value_undefined_json_error_value(
hass: HomeAssistant,
) -> None:
"""Render with possible JSON value with unknown JSON object."""
tpl = template.Template("{{ value_json.bye|is_defined }}", hass)
assert tpl.async_render_with_possible_json_value('{"hello": "world"}', "") == ""
def test_iif(hass: HomeAssistant) -> None:
"""Test the immediate if function/filter."""
result = render(hass, "{{ (1 == 1) | iif }}")
assert result is True
result = render(hass, "{{ (1 == 2) | iif }}")
assert result is False
result = render(hass, "{{ (1 == 1) | iif('yes') }}")
assert result == "yes"
result = render(hass, "{{ (1 == 2) | iif('yes') }}")
assert result is False
result = render(hass, "{{ (1 == 2) | iif('yes', 'no') }}")
assert result == "no"
result = render(hass, "{{ not_exists | default(None) | iif('yes', 'no') }}")
assert result == "no"
result = render(
hass, "{{ not_exists | default(None) | iif('yes', 'no', 'unknown') }}"
)
assert result == "unknown"
result = render(hass, "{{ iif(1 == 1) }}")
assert result is True
result = render(hass, "{{ iif(1 == 2, 'yes', 'no') }}")
assert result == "no"
@pytest.mark.parametrize(
("seq", "value", "expected"),
[
([0], 0, True),
([1], 0, False),
([False], 0, True),
([True], 0, False),
([0], [0], False),
(["toto", 1], "toto", True),
(["toto", 1], "tata", False),
([], 0, False),
([], None, False),
],
)
def test_contains(
hass: HomeAssistant, seq: list, value: object, expected: bool
) -> None:
"""Test contains."""
assert (
render(hass, "{{ seq | contains(value) }}", {"seq": seq, "value": value})
== expected
)
assert (
render(hass, "{{ seq is contains(value) }}", {"seq": seq, "value": value})
== expected
)
@pytest.mark.parametrize(
("service_response"),
[
{
"calendar.sports": {
"events": [
{
"start": "2024-02-27T17:00:00-06:00",
"end": "2024-02-27T18:00:00-06:00",
"summary": "Basketball vs. Rockets",
"description": "",
}
]
},
"calendar.local_furry_events": {"events": []},
"calendar.yap_house_schedules": {
"events": [
{
"start": "2024-02-26T08:00:00-06:00",
"end": "2024-02-26T09:00:00-06:00",
"summary": "Dr. Appt",
"description": "",
},
{
"start": "2024-02-28T20:00:00-06:00",
"end": "2024-02-28T21:00:00-06:00",
"summary": "Bake a cake",
"description": "something good",
},
]
},
},
{
"binary_sensor.workday": {"workday": True},
"binary_sensor.workday2": {"workday": False},
},
{
"weather.smhi_home": {
"forecast": [
{
"datetime": "2024-03-31T16:00:00",
"condition": "cloudy",
"wind_bearing": 79,
"cloud_coverage": 100,
"temperature": 10,
"templow": 4,
"pressure": 998,
"wind_gust_speed": 21.6,
"wind_speed": 11.88,
"precipitation": 0.2,
"humidity": 87,
},
{
"datetime": "2024-04-01T12:00:00",
"condition": "rainy",
"wind_bearing": 17,
"cloud_coverage": 100,
"temperature": 6,
"templow": 1,
"pressure": 999,
"wind_gust_speed": 20.52,
"wind_speed": 8.64,
"precipitation": 2.2,
"humidity": 88,
},
{
"datetime": "2024-04-02T12:00:00",
"condition": "cloudy",
"wind_bearing": 17,
"cloud_coverage": 100,
"temperature": 0,
"templow": -3,
"pressure": 1003,
"wind_gust_speed": 57.24,
"wind_speed": 30.6,
"precipitation": 1.3,
"humidity": 71,
},
]
},
"weather.forecast_home": {
"forecast": [
{
"condition": "cloudy",
"precipitation_probability": 6.6,
"datetime": "2024-03-31T10:00:00+00:00",
"wind_bearing": 71.8,
"temperature": 10.9,
"templow": 6.5,
"wind_gust_speed": 24.1,
"wind_speed": 13.7,
"precipitation": 0,
"humidity": 71,
},
{
"condition": "cloudy",
"precipitation_probability": 8,
"datetime": "2024-04-01T10:00:00+00:00",
"wind_bearing": 350.6,
"temperature": 10.2,
"templow": 3.4,
"wind_gust_speed": 38.2,
"wind_speed": 21.6,
"precipitation": 0,
"humidity": 79,
},
{
"condition": "snowy",
"precipitation_probability": 67.4,
"datetime": "2024-04-02T10:00:00+00:00",
"wind_bearing": 24.5,
"temperature": 3,
"templow": 0,
"wind_gust_speed": 64.8,
"wind_speed": 37.4,
"precipitation": 2.3,
"humidity": 77,
},
]
},
},
{
"vacuum.deebot_n8_plus_1": {
"payloadType": "j",
"resp": {
"body": {
"msg": "ok",
}
},
"header": {
"ver": "0.0.1",
},
},
"vacuum.deebot_n8_plus_2": {
"payloadType": "j",
"resp": {
"body": {
"msg": "ok",
}
},
"header": {
"ver": "0.0.1",
},
},
},
],
ids=["calendar", "workday", "weather", "vacuum"],
)
async def test_merge_response(
hass: HomeAssistant,
service_response: dict,
snapshot: SnapshotAssertion,
) -> None:
"""Test the merge_response function/filter."""
_template = "{{ merge_response(" + str(service_response) + ") }}"
assert service_response == snapshot(name="a_response")
assert render(
hass,
_template,
) == snapshot(name="b_rendered")
async def test_merge_response_with_entity_id_in_response(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
) -> None:
"""Test the merge_response function/filter with empty lists."""
service_response = {
"test.response": {"some_key": True, "entity_id": "test.response"},
"test.response2": {"some_key": False, "entity_id": "test.response2"},
}
_template = "{{ merge_response(" + str(service_response) + ") }}"
with pytest.raises(
TemplateError,
match="ValueError: Response dictionary already contains key 'entity_id'",
):
render(hass, _template)
service_response = {
"test.response": {
"happening": [
{
"start": "2024-02-27T17:00:00-06:00",
"end": "2024-02-27T18:00:00-06:00",
"summary": "Magic day",
"entity_id": "test.response",
}
]
}
}
_template = "{{ merge_response(" + str(service_response) + ") }}"
with pytest.raises(
TemplateError,
match="ValueError: Response dictionary already contains key 'entity_id'",
):
render(hass, _template)
async def test_merge_response_with_empty_response(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
) -> None:
"""Test the merge_response function/filter with empty lists."""
service_response = {
"calendar.sports": {"events": []},
"calendar.local_furry_events": {"events": []},
"calendar.yap_house_schedules": {"events": []},
}
_template = "{{ merge_response(" + str(service_response) + ") }}"
assert service_response == snapshot(name="a_response")
assert render(hass, _template) == snapshot(name="b_rendered")
async def test_response_empty_dict(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
) -> None:
"""Test the merge_response function/filter with empty dict."""
service_response = {}
_template = "{{ merge_response(" + str(service_response) + ") }}"
result = render(hass, _template)
assert result == []
async def test_response_incorrect_value(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
) -> None:
"""Test the merge_response function/filter with incorrect response."""
service_response = "incorrect"
_template = "{{ merge_response(" + str(service_response) + ") }}"
with pytest.raises(TemplateError, match="TypeError: Response is not a dictionary"):
render(hass, _template)
async def test_merge_response_with_incorrect_response(hass: HomeAssistant) -> None:
"""Test the merge_response function/filter with empty response should raise."""
service_response = {"calendar.sports": []}
_template = "{{ merge_response(" + str(service_response) + ") }}"
with pytest.raises(TemplateError, match="TypeError: Response is not a dictionary"):
render(hass, _template)
service_response = {
"binary_sensor.workday": [],
}
_template = "{{ merge_response(" + str(service_response) + ") }}"
with pytest.raises(TemplateError, match="TypeError: Response is not a dictionary"):
render(hass, _template)
async def test_merge_response_not_mutate_original_object(
hass: HomeAssistant, snapshot: SnapshotAssertion
) -> None:
"""Test the merge_response does not mutate original service response value."""
value = '{"calendar.family": {"events": [{"summary": "An event"}]}'
_template = (
"{% set calendar_response = " + value + "} %}"
"{{ merge_response(calendar_response) }}"
# We should be able to merge the same response again
# as the merge is working on a copy of the original object (response)
"{{ merge_response(calendar_response) }}"
)
assert render(hass, _template)
def test_typeof(hass: HomeAssistant) -> None:
"""Test the typeof debug filter/function."""
assert render(hass, "{{ True | typeof }}") == "bool"
assert render(hass, "{{ typeof(True) }}") == "bool"
assert render(hass, "{{ [1, 2, 3] | typeof }}") == "list"
assert render(hass, "{{ typeof([1, 2, 3]) }}") == "list"
assert render(hass, "{{ 1 | typeof }}") == "int"
assert render(hass, "{{ typeof(1) }}") == "int"
assert render(hass, "{{ 1.1 | typeof }}") == "float"
assert render(hass, "{{ typeof(1.1) }}") == "float"
assert render(hass, "{{ None | typeof }}") == "NoneType"
assert render(hass, "{{ typeof(None) }}") == "NoneType"
assert render(hass, "{{ 'Home Assistant' | typeof }}") == "str"
assert render(hass, "{{ typeof('Home Assistant') }}") == "str"
def test_combine(hass: HomeAssistant) -> None:
"""Test combine filter and function."""
assert render(hass, "{{ {'a': 1, 'b': 2} | combine({'b': 3, 'c': 4}) }}") == {
"a": 1,
"b": 3,
"c": 4,
}
assert render(hass, "{{ combine({'a': 1, 'b': 2}, {'b': 3, 'c': 4}) }}") == {
"a": 1,
"b": 3,
"c": 4,
}
assert render(
hass,
"{{ combine({'a': 1, 'b': {'x': 1}}, {'b': {'y': 2}, 'c': 4}, recursive=True) }}",
) == {"a": 1, "b": {"x": 1, "y": 2}, "c": 4}
# Test that recursive=False does not merge nested dictionaries
assert render(
hass,
"{{ combine({'a': 1, 'b': {'x': 1}}, {'b': {'y': 2}, 'c': 4}, recursive=False) }}",
) == {"a": 1, "b": {"y": 2}, "c": 4}
# Test that None values are handled correctly in recursive merge
assert render(
hass,
"{{ combine({'a': 1, 'b': none}, {'b': {'y': 2}, 'c': 4}, recursive=True) }}",
) == {"a": 1, "b": {"y": 2}, "c": 4}
with pytest.raises(
TemplateError, match="combine expected at least 1 argument, got 0"
):
render(hass, "{{ combine() }}")
with pytest.raises(TemplateError, match="combine expected a dict, got str"):
render(hass, "{{ {'a': 1} | combine('not a dict') }}")