diff --git a/esphome/components/template/text/__init__.py b/esphome/components/template/text/__init__.py index 572b5ba0f4a..1266370cb21 100644 --- a/esphome/components/template/text/__init__.py +++ b/esphome/components/template/text/__init__.py @@ -3,6 +3,7 @@ import esphome.codegen as cg from esphome.components import text import esphome.config_validation as cv from esphome.const import ( + CONF_ID, CONF_INITIAL_VALUE, CONF_LAMBDA, CONF_MAX_LENGTH, @@ -12,6 +13,7 @@ from esphome.const import ( CONF_RESTORE_VALUE, CONF_SET_ACTION, ) +from esphome.core import ID from .. import template_ns @@ -84,8 +86,15 @@ async def to_code(config): if initial_value_config := config.get(CONF_INITIAL_VALUE): cg.add(var.set_initial_value(initial_value_config)) if config[CONF_RESTORE_VALUE]: - args = cg.TemplateArguments(config[CONF_MAX_LENGTH]) - saver = TextSaverTemplate.template(args).new() + saver_id = ID( + f"{config[CONF_ID].id}_value_saver", + is_declaration=True, + type=TextSaverBase, + ) + saver_type = TextSaverTemplate.template( + cg.TemplateArguments(config[CONF_MAX_LENGTH]) + ) + saver = cg.Pvariable(saver_id, saver_type.new()) cg.add(var.set_value_saver(saver)) if CONF_SET_ACTION in config: diff --git a/tests/component_tests/template/__init__.py b/tests/component_tests/template/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/component_tests/template/config/template_text_restore.yaml b/tests/component_tests/template/config/template_text_restore.yaml new file mode 100644 index 00000000000..4574470eab0 --- /dev/null +++ b/tests/component_tests/template/config/template_text_restore.yaml @@ -0,0 +1,14 @@ +esphome: + name: test + +host: + +text: + - platform: template + name: "Test Text Restore" + id: test_text_restore + optimistic: true + max_length: 10 + mode: text + initial_value: "hello" + restore_value: true diff --git a/tests/component_tests/template/test_template_text.py b/tests/component_tests/template/test_template_text.py new file mode 100644 index 00000000000..2ce9a88d671 --- /dev/null +++ b/tests/component_tests/template/test_template_text.py @@ -0,0 +1,44 @@ +"""Tests for the template text component.""" + +from __future__ import annotations + +from collections.abc import Callable +from pathlib import Path + + +def test_template_text_saver_uses_placement_new_with_templated_subclass( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + """Regression test for template text restore saver using placement new. + + When ``restore_value: true``, the saver is its own Pvariable with + placement new: storage is sized for ``TextSaver``, the + declared pointer stays at ``TemplateTextSaverBase *`` for polymorphism, + and the templated subclass constructor runs. A regression would either + reintroduce the heap ``new TextSaver<...>()`` expression or size the + storage for the base class and silently skip the subclass ctor. + """ + main_cpp = generate_main(component_config_path("template_text_restore.yaml")) + + # Storage is sized and aligned for the templated subclass. + assert "sizeof(template_::TextSaver<10>)" in main_cpp + assert "alignas(template_::TextSaver<10>)" in main_cpp + # Pointer declared as base type for polymorphism. + assert ( + "static template_::TemplateTextSaverBase *const test_text_restore_value_saver" + in main_cpp + ) + # Placement new runs the templated subclass constructor. + assert "new(test_text_restore_value_saver) template_::TextSaver<10>()" in main_cpp + # Base-class default ctor must NOT be used. + assert ( + "new(test_text_restore_value_saver) template_::TemplateTextSaverBase()" + not in main_cpp + ) + # No heap `new TextSaver<...>()` left over — the pre-fix pattern. + assert "new template_::TextSaver<" not in main_cpp + # Saver is wired into the text component. + assert ( + "test_text_restore->set_value_saver(test_text_restore_value_saver)" in main_cpp + )