From c704d9780454504b6325ff91932b8cb4f66f48eb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 6 Mar 2026 10:54:06 -1000 Subject: [PATCH 1/4] Strengthen internal flag assertions in component tests All internal-flag tests now extract the packed value and verify bit 24 is set/clear for the correct entities, instead of just checking configure_entity_() call presence. --- .../binary_sensor/test_binary_sensor.py | 19 +++++++++++++---- tests/component_tests/button/test_button.py | 20 +++++++++++++----- tests/component_tests/text/test_text.py | 21 ++++++++++++++----- .../text_sensor/test_text_sensor.py | 9 ++++---- 4 files changed, 51 insertions(+), 18 deletions(-) diff --git a/tests/component_tests/binary_sensor/test_binary_sensor.py b/tests/component_tests/binary_sensor/test_binary_sensor.py index 2667e90dda..e029fa19bb 100644 --- a/tests/component_tests/binary_sensor/test_binary_sensor.py +++ b/tests/component_tests/binary_sensor/test_binary_sensor.py @@ -1,5 +1,17 @@ """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)) + def test_binary_sensor_is_setup(generate_main): """ @@ -44,10 +56,9 @@ def test_binary_sensor_config_value_internal_set(generate_main): "tests/component_tests/binary_sensor/test_binary_sensor.yaml" ) - # Then - # internal flag is now packed into configure_entity_() third argument (bit 24) - assert "bs_1->configure_entity_(" in main_cpp - assert "bs_2->configure_entity_(" in main_cpp + # 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 def test_binary_sensor_config_value_use_raw_set(generate_main): diff --git a/tests/component_tests/button/test_button.py b/tests/component_tests/button/test_button.py index cd767dd65b..6973ec0f64 100644 --- a/tests/component_tests/button/test_button.py +++ b/tests/component_tests/button/test_button.py @@ -1,5 +1,17 @@ """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)) + def test_button_is_setup(generate_main): """ @@ -39,8 +51,6 @@ def test_button_config_value_internal_set(generate_main): # When main_cpp = generate_main("tests/component_tests/button/test_button.yaml") - # Then - # internal flag is packed into configure_entity_() third argument (bit 24) - # wol_1 has internal: true → bit 24 set → packed value 16777216 - assert "wol_1->configure_entity_(" in main_cpp - assert "wol_2->configure_entity_(" in main_cpp + # 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 diff --git a/tests/component_tests/text/test_text.py b/tests/component_tests/text/test_text.py index bad7ad3a33..31c66d8784 100644 --- a/tests/component_tests/text/test_text.py +++ b/tests/component_tests/text/test_text.py @@ -1,4 +1,16 @@ -"""Tests for the binary sensor component.""" +"""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)) def test_text_is_setup(generate_main): @@ -37,10 +49,9 @@ def test_text_config_value_internal_set(generate_main): # When main_cpp = generate_main("tests/component_tests/text/test_text.yaml") - # Then - # internal flag is now packed into configure_entity_() third argument (bit 24) - assert "it_2->configure_entity_(" in main_cpp - assert "it_3->configure_entity_(" in main_cpp + # 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 def test_text_config_value_mode_set(generate_main): diff --git a/tests/component_tests/text_sensor/test_text_sensor.py b/tests/component_tests/text_sensor/test_text_sensor.py index 21a273b947..2d77756b2c 100644 --- a/tests/component_tests/text_sensor/test_text_sensor.py +++ b/tests/component_tests/text_sensor/test_text_sensor.py @@ -2,6 +2,8 @@ import re +_INTERNAL_BIT = 1 << 24 + def _extract_packed_value(main_cpp, var_name): """Extract the third (packed) argument from a configure_entity_ call.""" @@ -49,10 +51,9 @@ def test_text_sensor_config_value_internal_set(generate_main): # When main_cpp = generate_main("tests/component_tests/text_sensor/test_text_sensor.yaml") - # Then - # internal flag is now packed into configure_entity_() third argument (bit 24) - assert "ts_2->configure_entity_(" in main_cpp - assert "ts_3->configure_entity_(" in main_cpp + # 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 def test_text_sensor_device_class_set(generate_main): From 41c413d5f7aa64ed7fe91fc9cc72e76f2d53b47b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 6 Mar 2026 10:56:51 -1000 Subject: [PATCH 2/4] Move extract_packed_value to shared component test helper Add tests/component_tests/helpers.py with typed extract_packed_value() and INTERNAL_BIT constant, replacing 5 duplicate copies. --- .../binary_sensor/test_binary_sensor.py | 16 +++------------ tests/component_tests/button/test_button.py | 16 +++------------ tests/component_tests/helpers.py | 15 ++++++++++++++ tests/component_tests/sensor/test_sensor.py | 12 ++--------- tests/component_tests/text/test_text.py | 16 +++------------ .../text_sensor/test_text_sensor.py | 20 +++++-------------- 6 files changed, 31 insertions(+), 64 deletions(-) create mode 100644 tests/component_tests/helpers.py diff --git a/tests/component_tests/binary_sensor/test_binary_sensor.py b/tests/component_tests/binary_sensor/test_binary_sensor.py index e029fa19bb..10d7f80834 100644 --- a/tests/component_tests/binary_sensor/test_binary_sensor.py +++ b/tests/component_tests/binary_sensor/test_binary_sensor.py @@ -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): diff --git a/tests/component_tests/button/test_button.py b/tests/component_tests/button/test_button.py index 6973ec0f64..a35994a682 100644 --- a/tests/component_tests/button/test_button.py +++ b/tests/component_tests/button/test_button.py @@ -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 diff --git a/tests/component_tests/helpers.py b/tests/component_tests/helpers.py new file mode 100644 index 0000000000..e9fbb33173 --- /dev/null +++ b/tests/component_tests/helpers.py @@ -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)) diff --git a/tests/component_tests/sensor/test_sensor.py b/tests/component_tests/sensor/test_sensor.py index d9ab3a022c..9d18fa36b8 100644 --- a/tests/component_tests/sensor/test_sensor.py +++ b/tests/component_tests/sensor/test_sensor.py @@ -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 diff --git a/tests/component_tests/text/test_text.py b/tests/component_tests/text/test_text.py index 31c66d8784..c74dfb8a47 100644 --- a/tests/component_tests/text/test_text.py +++ b/tests/component_tests/text/test_text.py @@ -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): diff --git a/tests/component_tests/text_sensor/test_text_sensor.py b/tests/component_tests/text_sensor/test_text_sensor.py index 2d77756b2c..1ff31ab96b 100644 --- a/tests/component_tests/text_sensor/test_text_sensor.py +++ b/tests/component_tests/text_sensor/test_text_sensor.py @@ -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 From 86dd579deaa0603a150c3bceb497bded46b72273 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 6 Mar 2026 11:00:19 -1000 Subject: [PATCH 3/4] Remove internal shift constants from unit tests Tests now verify packed values via comment strings and non-zero checks rather than decoding individual bit positions. This removes the coupling to internal bit layout constants. --- tests/unit_tests/core/test_entity_helpers.py | 62 ++++++++------------ 1 file changed, 25 insertions(+), 37 deletions(-) diff --git a/tests/unit_tests/core/test_entity_helpers.py b/tests/unit_tests/core/test_entity_helpers.py index a68955fe08..acf315c1b0 100644 --- a/tests/unit_tests/core/test_entity_helpers.py +++ b/tests/unit_tests/core/test_entity_helpers.py @@ -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 @@ -1029,7 +1024,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 +1035,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 +1053,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 +1073,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 +1086,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 +1111,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 +1139,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 ( From c956b33e93816c9a6505ddfc3ef2e68e5046495d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 6 Mar 2026 11:02:35 -1000 Subject: [PATCH 4/4] Add comment noting shift correctness is verified by integration test --- tests/unit_tests/core/test_entity_helpers.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/unit_tests/core/test_entity_helpers.py b/tests/unit_tests/core/test_entity_helpers.py index acf315c1b0..d6cbb8c6be 100644 --- a/tests/unit_tests/core/test_entity_helpers.py +++ b/tests/unit_tests/core/test_entity_helpers.py @@ -993,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: