Merge remote-tracking branch 'upstream/protect_entity_setters' into integration

# Conflicts:
#	tests/component_tests/binary_sensor/test_binary_sensor.py
#	tests/component_tests/button/test_button.py
#	tests/component_tests/text/test_text.py
#	tests/component_tests/text_sensor/test_text_sensor.py
This commit is contained in:
J. Nick Koston
2026-03-06 11:16:34 -10:00
7 changed files with 62 additions and 101 deletions
@@ -1,16 +1,6 @@
"""Tests for the binary sensor component."""
import re
_INTERNAL_BIT = 1 << 24
def _extract_packed_value(main_cpp, var_name):
"""Extract the third (packed) argument from a configure_entity_ call."""
pattern = rf"{re.escape(var_name)}->configure_entity_\([^,]+,\s*\w+,\s*(\d+)\)"
match = re.search(pattern, main_cpp)
assert match, f"configure_entity_ call not found for {var_name}"
return int(match.group(1))
from tests.component_tests.helpers import INTERNAL_BIT, extract_packed_value
def test_binary_sensor_is_setup(generate_main):
@@ -57,8 +47,8 @@ def test_binary_sensor_config_value_internal_set(generate_main):
)
# Then: bs_1 has internal: true, bs_2 has internal: false
assert _extract_packed_value(main_cpp, "bs_1") & _INTERNAL_BIT != 0
assert _extract_packed_value(main_cpp, "bs_2") & _INTERNAL_BIT == 0
assert extract_packed_value(main_cpp, "bs_1") & INTERNAL_BIT != 0
assert extract_packed_value(main_cpp, "bs_2") & INTERNAL_BIT == 0
def test_binary_sensor_config_value_use_raw_set(generate_main):
+3 -13
View File
@@ -1,16 +1,6 @@
"""Tests for the button component"""
import re
_INTERNAL_BIT = 1 << 24
def _extract_packed_value(main_cpp, var_name):
"""Extract the third (packed) argument from a configure_entity_ call."""
pattern = rf"{re.escape(var_name)}->configure_entity_\([^,]+,\s*\w+,\s*(\d+)\)"
match = re.search(pattern, main_cpp)
assert match, f"configure_entity_ call not found for {var_name}"
return int(match.group(1))
from tests.component_tests.helpers import INTERNAL_BIT, extract_packed_value
def test_button_is_setup(generate_main):
@@ -52,5 +42,5 @@ def test_button_config_value_internal_set(generate_main):
main_cpp = generate_main("tests/component_tests/button/test_button.yaml")
# Then: wol_1 has internal: true, wol_2 has internal: false
assert _extract_packed_value(main_cpp, "wol_1") & _INTERNAL_BIT != 0
assert _extract_packed_value(main_cpp, "wol_2") & _INTERNAL_BIT == 0
assert extract_packed_value(main_cpp, "wol_1") & INTERNAL_BIT != 0
assert extract_packed_value(main_cpp, "wol_2") & INTERNAL_BIT == 0
+15
View File
@@ -0,0 +1,15 @@
"""Shared helpers for component tests."""
from __future__ import annotations
import re
INTERNAL_BIT = 1 << 24
def extract_packed_value(main_cpp: str, var_name: str) -> int:
"""Extract the third (packed) argument from a configure_entity_ call."""
pattern = rf"{re.escape(var_name)}->configure_entity_\([^,]+,\s*\w+,\s*(\d+)\)"
match = re.search(pattern, main_cpp)
assert match, f"configure_entity_ call not found for {var_name}"
return int(match.group(1))
+2 -10
View File
@@ -1,14 +1,6 @@
"""Tests for the sensor component."""
import re
def _extract_packed_value(main_cpp, var_name):
"""Extract the third (packed) argument from a configure_entity_ call."""
pattern = rf"{re.escape(var_name)}->configure_entity_\([^,]+,\s*\w+,\s*(\d+)\)"
match = re.search(pattern, main_cpp)
assert match, f"configure_entity_ call not found for {var_name}"
return int(match.group(1))
from tests.component_tests.helpers import extract_packed_value
def test_sensor_device_class_set(generate_main):
@@ -21,5 +13,5 @@ def test_sensor_device_class_set(generate_main):
main_cpp = generate_main("tests/component_tests/sensor/test_sensor.yaml")
# Then: device_class: voltage means packed value must be non-zero
packed = _extract_packed_value(main_cpp, "s_1")
packed = extract_packed_value(main_cpp, "s_1")
assert packed != 0
+3 -13
View File
@@ -1,16 +1,6 @@
"""Tests for the text component."""
import re
_INTERNAL_BIT = 1 << 24
def _extract_packed_value(main_cpp, var_name):
"""Extract the third (packed) argument from a configure_entity_ call."""
pattern = rf"{re.escape(var_name)}->configure_entity_\([^,]+,\s*\w+,\s*(\d+)\)"
match = re.search(pattern, main_cpp)
assert match, f"configure_entity_ call not found for {var_name}"
return int(match.group(1))
from tests.component_tests.helpers import INTERNAL_BIT, extract_packed_value
def test_text_is_setup(generate_main):
@@ -50,8 +40,8 @@ def test_text_config_value_internal_set(generate_main):
main_cpp = generate_main("tests/component_tests/text/test_text.yaml")
# Then: it_2 has internal: false, it_3 has internal: true
assert _extract_packed_value(main_cpp, "it_2") & _INTERNAL_BIT == 0
assert _extract_packed_value(main_cpp, "it_3") & _INTERNAL_BIT != 0
assert extract_packed_value(main_cpp, "it_2") & INTERNAL_BIT == 0
assert extract_packed_value(main_cpp, "it_3") & INTERNAL_BIT != 0
def test_text_config_value_mode_set(generate_main):
@@ -1,16 +1,6 @@
"""Tests for the text sensor component."""
import re
_INTERNAL_BIT = 1 << 24
def _extract_packed_value(main_cpp, var_name):
"""Extract the third (packed) argument from a configure_entity_ call."""
pattern = rf"{re.escape(var_name)}->configure_entity_\([^,]+,\s*\w+,\s*(\d+)\)"
match = re.search(pattern, main_cpp)
assert match, f"configure_entity_ call not found for {var_name}"
return int(match.group(1))
from tests.component_tests.helpers import INTERNAL_BIT, extract_packed_value
def test_text_sensor_is_setup(generate_main):
@@ -52,8 +42,8 @@ def test_text_sensor_config_value_internal_set(generate_main):
main_cpp = generate_main("tests/component_tests/text_sensor/test_text_sensor.yaml")
# Then: ts_2 has internal: true, ts_3 has internal: false
assert _extract_packed_value(main_cpp, "ts_2") & _INTERNAL_BIT != 0
assert _extract_packed_value(main_cpp, "ts_3") & _INTERNAL_BIT == 0
assert extract_packed_value(main_cpp, "ts_2") & INTERNAL_BIT != 0
assert extract_packed_value(main_cpp, "ts_3") & INTERNAL_BIT == 0
def test_text_sensor_device_class_set(generate_main):
@@ -67,7 +57,7 @@ def test_text_sensor_device_class_set(generate_main):
# Then: ts_2 has device_class: timestamp, ts_3 has device_class: date
# so their packed values must be non-zero
packed_ts_2 = _extract_packed_value(main_cpp, "ts_2")
packed_ts_2 = extract_packed_value(main_cpp, "ts_2")
assert packed_ts_2 != 0
packed_ts_3 = _extract_packed_value(main_cpp, "ts_3")
packed_ts_3 = extract_packed_value(main_cpp, "ts_3")
assert packed_ts_3 != 0
+31 -37
View File
@@ -21,12 +21,6 @@ from esphome.const import (
)
from esphome.core import CORE, ID, entity_helpers
from esphome.core.entity_helpers import (
_DC_SHIFT,
_DISABLED_BY_DEFAULT_SHIFT,
_ENTITY_CATEGORY_SHIFT,
_ICON_SHIFT,
_INTERNAL_SHIFT,
_UOM_SHIFT,
_register_string,
_setup_entity_impl,
entity_duplicate_validator,
@@ -946,7 +940,8 @@ async def test_setup_entity_with_entity_category(
await _setup_entity_impl(var, config, "sensor")
finalize_entity_strings(var, config)
packed = _extract_packed_value(added_expressions)
assert (packed >> _ENTITY_CATEGORY_SHIFT) & 0x3 == 2
assert packed != 0
assert "category:diagnostic" in added_expressions[0]
@pytest.mark.asyncio
@@ -998,6 +993,12 @@ async def test_setup_entity_decorator_mode(setup_test_environment: list[str]) ->
# Tests for finalize_entity_strings packing
#
# These tests verify that flags and string indices produce non-zero packed values
# and correct inline comments. The actual bit layout correctness (Python _*_SHIFT
# matching C++ ENTITY_FIELD_*_SHIFT) is verified end-to-end by the integration
# test test_host_mode_entity_fields, which compiles firmware and checks values
# via the native API.
def _extract_packed_value(expressions: list[str]) -> int:
@@ -1029,7 +1030,7 @@ async def test_finalize_no_flags(setup_test_environment: list[str]) -> None:
@pytest.mark.asyncio
async def test_finalize_internal(setup_test_environment: list[str]) -> None:
"""Test entity with internal=True packs the internal bit."""
"""Test entity with internal=True packs the internal flag."""
added_expressions = setup_test_environment
var = MockObj("sensor1")
config = {
@@ -1040,15 +1041,15 @@ async def test_finalize_internal(setup_test_environment: list[str]) -> None:
await _setup_entity_impl(var, config, "sensor")
finalize_entity_strings(var, config)
packed = _extract_packed_value(added_expressions)
assert packed & (1 << _INTERNAL_SHIFT) != 0
assert packed == (1 << _INTERNAL_SHIFT)
assert packed != 0
assert "// internal" in added_expressions[0]
@pytest.mark.asyncio
async def test_finalize_disabled_by_default(
setup_test_environment: list[str],
) -> None:
"""Test entity with disabled_by_default=True packs the bit."""
"""Test entity with disabled_by_default=True packs the flag."""
added_expressions = setup_test_environment
var = MockObj("sensor1")
config = {
@@ -1058,19 +1059,19 @@ async def test_finalize_disabled_by_default(
await _setup_entity_impl(var, config, "sensor")
finalize_entity_strings(var, config)
packed = _extract_packed_value(added_expressions)
assert packed & (1 << _DISABLED_BY_DEFAULT_SHIFT) != 0
assert packed == (1 << _DISABLED_BY_DEFAULT_SHIFT)
assert packed != 0
assert "// disabled_by_default" in added_expressions[0]
@pytest.mark.asyncio
async def test_finalize_entity_category(
setup_test_environment: list[str],
) -> None:
"""Test entity_category values (diagnostic=2, config=1) are packed."""
"""Test entity_category values are packed and described in comment."""
added_expressions = setup_test_environment
var = MockObj("sensor1")
# Test diagnostic (value 2)
# Test diagnostic
config = {
CONF_NAME: "Test",
CONF_DISABLED_BY_DEFAULT: False,
@@ -1078,10 +1079,11 @@ async def test_finalize_entity_category(
}
await _setup_entity_impl(var, config, "sensor")
finalize_entity_strings(var, config)
packed = _extract_packed_value(added_expressions)
assert (packed >> _ENTITY_CATEGORY_SHIFT) & 0x3 == 2
packed_diag = _extract_packed_value(added_expressions)
assert packed_diag != 0
assert "category:diagnostic" in added_expressions[0]
# Test config (value 1)
# Test config — different packed value
added_expressions.clear()
config2 = {
CONF_NAME: "Test2",
@@ -1090,15 +1092,17 @@ async def test_finalize_entity_category(
}
await _setup_entity_impl(var, config2, "sensor")
finalize_entity_strings(var, config2)
packed = _extract_packed_value(added_expressions)
assert (packed >> _ENTITY_CATEGORY_SHIFT) & 0x3 == 1
packed_cfg = _extract_packed_value(added_expressions)
assert packed_cfg != 0
assert packed_cfg != packed_diag
assert "category:config" in added_expressions[0]
@pytest.mark.asyncio
async def test_finalize_string_indices(
setup_test_environment: list[str],
) -> None:
"""Test device_class, unit_of_measurement, and icon are packed as indices."""
"""Test device_class, unit_of_measurement, and icon produce non-zero packed value."""
added_expressions = setup_test_environment
var = MockObj("sensor1")
config = {
@@ -1113,14 +1117,11 @@ async def test_finalize_string_indices(
setup_unit_of_measurement(config)
finalize_entity_strings(var, config)
packed = _extract_packed_value(added_expressions)
# All three string indices should be non-zero
assert (packed >> _DC_SHIFT) & 0xFF != 0
assert (packed >> _UOM_SHIFT) & 0xFF != 0
assert (packed >> _ICON_SHIFT) & 0xFF != 0
# No flags set
assert (packed >> _INTERNAL_SHIFT) & 1 == 0
assert (packed >> _DISABLED_BY_DEFAULT_SHIFT) & 1 == 0
assert (packed >> _ENTITY_CATEGORY_SHIFT) & 0x3 == 0
assert packed != 0
comment = added_expressions[0]
assert "dc:temperature" in comment
assert "uom:°C" in comment
assert "icon:mdi:thermometer" in comment
@pytest.mark.asyncio
@@ -1144,14 +1145,7 @@ async def test_finalize_all_fields(
setup_unit_of_measurement(config)
finalize_entity_strings(var, config)
packed = _extract_packed_value(added_expressions)
# Verify flags
assert (packed >> _INTERNAL_SHIFT) & 1 == 1
assert (packed >> _DISABLED_BY_DEFAULT_SHIFT) & 1 == 1
assert (packed >> _ENTITY_CATEGORY_SHIFT) & 0x3 == 2
# Verify string indices are non-zero
assert (packed >> _DC_SHIFT) & 0xFF != 0
assert (packed >> _UOM_SHIFT) & 0xFF != 0
assert (packed >> _ICON_SHIFT) & 0xFF != 0
assert packed != 0
# Verify comment contains all flags with actual string values
comment_line = added_expressions[0]
assert (