mirror of
https://github.com/esphome/esphome.git
synced 2026-11-15 01:25:57 +00:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
95478bf221 | ||
|
|
fd3feebd7c | ||
|
|
276e113679 | ||
|
|
949f210c1c | ||
|
|
8ced42a7af | ||
|
|
c8503aa4a4 |
No files matched your search
+3
-35
@@ -1,6 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import Counter
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
@@ -60,6 +59,7 @@ from esphome.core import (
|
||||
TimePeriodMilliseconds,
|
||||
coroutine_with_priority,
|
||||
)
|
||||
from esphome.cpp_helpers import add_component_counts
|
||||
import esphome.final_validate as fv
|
||||
from esphome.helpers import (
|
||||
copy_file_if_changed,
|
||||
@@ -843,36 +843,6 @@ async def _add_controller_registry_dispatch() -> None:
|
||||
cg.add_global(cg.RawStatement('#include "esphome/core/controller_dispatch.h"'))
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.FINAL)
|
||||
async def _add_looping_components() -> None:
|
||||
# Emit ESPHOME_LOOPING_COMPONENT_COUNT and its storage. Sizing of looping_components_
|
||||
# happens in core to_code() so it lands before safe_mode's early return.
|
||||
entries = CORE.data.get("looping_component_entries", [])
|
||||
|
||||
# Build constexpr sum for the exact count, deduplicating by type
|
||||
# Uses HasLoopOverride<T> which handles ambiguous &T::loop from multiple inheritance
|
||||
type_counts = Counter(entries)
|
||||
terms = [
|
||||
f"({count} * HasLoopOverride<{cpp_type}>::value)"
|
||||
for cpp_type, count in type_counts.items()
|
||||
] or ["0"]
|
||||
constexpr_expr = " + \\\n ".join(terms)
|
||||
cg.add_global(
|
||||
cg.RawStatement(
|
||||
f"static constexpr size_t ESPHOME_LOOPING_COMPONENT_COUNT = \\\n"
|
||||
f" {constexpr_expr};"
|
||||
)
|
||||
)
|
||||
# In namespace esphome so analyze-memory counts it as core; sized at least 1 to avoid a zero-length array
|
||||
cg.add_global(
|
||||
cg.RawStatement(
|
||||
"namespace esphome {\n"
|
||||
"static Component *looping_components_storage[std::max<size_t>(ESPHOME_LOOPING_COMPONENT_COUNT, 1)];\n"
|
||||
"} // namespace esphome"
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.CORE)
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
# using namespace esphome is hardcoded in writer.py to guarantee it
|
||||
@@ -912,11 +882,10 @@ async def to_code(config: ConfigType) -> None:
|
||||
if name_add_mac_suffix:
|
||||
cg.add_define("ESPHOME_NAME_ADD_MAC_SUFFIX")
|
||||
cg.add(cg.App.pre_setup(name_expr, name_len, friendly_expr, friendly_len))
|
||||
# Define component count for static allocation
|
||||
cg.add_define("ESPHOME_COMPONENT_COUNT", len(CORE.component_ids))
|
||||
CORE.add_job(add_component_counts)
|
||||
|
||||
# Pre-init the list with exact capacity so calculate_looping_components_()
|
||||
# can skip the counting pass; the storage is emitted by _add_looping_components()
|
||||
# can skip the counting pass; the storage is emitted by add_component_counts()
|
||||
cg.add(
|
||||
cg.RawExpression(
|
||||
"App.looping_components_.init(std::span<Component *>("
|
||||
@@ -926,7 +895,6 @@ async def to_code(config: ConfigType) -> None:
|
||||
|
||||
CORE.add_job(_add_platform_defines)
|
||||
CORE.add_job(_add_controller_registry_dispatch)
|
||||
CORE.add_job(_add_looping_components)
|
||||
|
||||
CORE.add_job(_add_automations, config)
|
||||
|
||||
|
||||
+59
-3
@@ -1,3 +1,4 @@
|
||||
from collections import Counter
|
||||
from collections.abc import Awaitable, Callable, Hashable
|
||||
from dataclasses import dataclass, field
|
||||
import logging
|
||||
@@ -60,6 +61,58 @@ def _get_source_pool() -> ComponentSourcePool:
|
||||
return CORE.data[_COMPONENT_SOURCE_DOMAIN]
|
||||
|
||||
|
||||
_REGISTERED_COMPONENTS_DOMAIN = "registered_components"
|
||||
|
||||
# Below every finite priority, so the job runs only once every other job,
|
||||
# including FINAL ones and to_code tasks pushed down by waiting, is done.
|
||||
_AFTER_ALL_JOBS = float("-inf")
|
||||
|
||||
|
||||
@dataclass
|
||||
class _RegisteredComponents:
|
||||
"""C++ type of each register_component() call, which sizes App.components_
|
||||
and App.looping_components_."""
|
||||
|
||||
types: list[str] = field(default_factory=list)
|
||||
emitted: bool = False
|
||||
|
||||
|
||||
def _get_registered_components() -> _RegisteredComponents:
|
||||
if _REGISTERED_COMPONENTS_DOMAIN not in CORE.data:
|
||||
CORE.data[_REGISTERED_COMPONENTS_DOMAIN] = _RegisteredComponents()
|
||||
return CORE.data[_REGISTERED_COMPONENTS_DOMAIN]
|
||||
|
||||
|
||||
@coroutine_with_priority(_AFTER_ALL_JOBS)
|
||||
async def add_component_counts() -> None:
|
||||
"""Emit ESPHOME_COMPONENT_COUNT and ESPHOME_LOOPING_COMPONENT_COUNT from the
|
||||
components actually registered; declared but unregistered ids take no slot."""
|
||||
registered = _get_registered_components()
|
||||
registered.emitted = True
|
||||
add_define("ESPHOME_COMPONENT_COUNT", len(registered.types))
|
||||
# Exact looping count, summed per type at C++ compile time. HasLoopOverride<T>
|
||||
# handles an ambiguous &T::loop from multiple inheritance.
|
||||
terms = [
|
||||
f"({count} * HasLoopOverride<{cpp_type}>::value)"
|
||||
for cpp_type, count in Counter(registered.types).items()
|
||||
] or ["0"]
|
||||
constexpr_expr = " + \\\n ".join(terms)
|
||||
add_global(
|
||||
RawStatement(
|
||||
f"static constexpr size_t ESPHOME_LOOPING_COMPONENT_COUNT = \\\n"
|
||||
f" {constexpr_expr};"
|
||||
)
|
||||
)
|
||||
# In namespace esphome so analyze-memory counts it as core; at least 1 to avoid a zero-length array
|
||||
add_global(
|
||||
RawStatement(
|
||||
"namespace esphome {\n"
|
||||
"static Component *looping_components_storage[std::max<size_t>(ESPHOME_LOOPING_COMPONENT_COUNT, 1)];\n"
|
||||
"} // namespace esphome"
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _ensure_source_table_registered() -> None:
|
||||
"""Schedule the table generation job (once)."""
|
||||
pool = _get_source_pool()
|
||||
@@ -441,6 +494,11 @@ async def register_component(var, config):
|
||||
raise ValueError(
|
||||
f"Component ID {id_} was not declared to inherit from Component, or was registered twice. Please create a bug report with your configuration."
|
||||
)
|
||||
registered = _get_registered_components()
|
||||
if registered.emitted:
|
||||
raise ValueError(
|
||||
f"Component ID {id_} was registered after ESPHOME_COMPONENT_COUNT was emitted. Please create a bug report with your configuration."
|
||||
)
|
||||
CORE.component_ids.remove(id_)
|
||||
if CONF_SETUP_PRIORITY in config:
|
||||
set_setup_priority(var, config[CONF_SETUP_PRIORITY])
|
||||
@@ -475,9 +533,7 @@ async def register_component(var, config):
|
||||
else:
|
||||
add(App.register_component_(var))
|
||||
|
||||
# Collect C++ type for compile-time looping component count
|
||||
comp_entries = CORE.data.setdefault("looping_component_entries", [])
|
||||
comp_entries.append(str(var.base.type))
|
||||
registered.types.append(str(var.base.type))
|
||||
|
||||
return var
|
||||
|
||||
|
||||
@@ -707,63 +707,6 @@ def test_to_code_priority_above_safe_mode() -> None:
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_looping_components_handles_empty_entries() -> None:
|
||||
"""Test that _add_looping_components emits a valid constexpr when there are
|
||||
no looping component entries.
|
||||
|
||||
With zero entries the generated constexpr must still be syntactically valid
|
||||
C++ (`= 0;`), not an empty expression (`= ;`). This guards the empty-list
|
||||
case that would otherwise produce uncompilable main.cpp output.
|
||||
"""
|
||||
CORE.data["looping_component_entries"] = []
|
||||
|
||||
await config._add_looping_components()
|
||||
|
||||
constexpr_lines = [
|
||||
str(s)
|
||||
for s in CORE.global_statements
|
||||
if "constexpr size_t ESPHOME_LOOPING_COMPONENT_COUNT" in str(s)
|
||||
]
|
||||
assert len(constexpr_lines) == 1
|
||||
text = constexpr_lines[0]
|
||||
assert "static constexpr size_t ESPHOME_LOOPING_COMPONENT_COUNT" in text
|
||||
# The right-hand side must contain a literal `0`, not be empty.
|
||||
rhs = text.split("=", 1)[1]
|
||||
assert "0" in rhs
|
||||
assert rhs.strip().rstrip(";").strip(), (
|
||||
f"constexpr right-hand side must not be empty, got: {text!r}"
|
||||
)
|
||||
assert any(
|
||||
"static Component *looping_components_storage[std::max<size_t>(ESPHOME_LOOPING_COMPONENT_COUNT, 1)];"
|
||||
in str(s)
|
||||
for s in CORE.global_statements
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_looping_components_with_entries() -> None:
|
||||
"""Test that _add_looping_components builds a HasLoopOverride sum from entries."""
|
||||
CORE.data["looping_component_entries"] = [
|
||||
"esphome::wifi::WiFiComponent",
|
||||
"esphome::logger::Logger",
|
||||
"esphome::wifi::WiFiComponent",
|
||||
]
|
||||
|
||||
await config._add_looping_components()
|
||||
|
||||
constexpr_lines = [
|
||||
str(s)
|
||||
for s in CORE.global_statements
|
||||
if "constexpr size_t ESPHOME_LOOPING_COMPONENT_COUNT" in str(s)
|
||||
]
|
||||
assert len(constexpr_lines) == 1
|
||||
text = constexpr_lines[0]
|
||||
# Deduplicated by type, with per-type counts as multiplier.
|
||||
assert "(2 * HasLoopOverride<esphome::wifi::WiFiComponent>::value)" in text
|
||||
assert "(1 * HasLoopOverride<esphome::logger::Logger>::value)" in text
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_controller_registry_dispatch_without_controllers() -> None:
|
||||
"""Nothing is emitted when no controller registered."""
|
||||
|
||||
@@ -12,6 +12,7 @@ import pytest
|
||||
|
||||
from esphome import codegen as cg, const, cpp_helpers as ch, cpp_types as ct
|
||||
from esphome.core import CoroPriority, coroutine_with_priority
|
||||
from esphome.coroutine import FakeAwaitable
|
||||
from esphome.cpp_generator import MockObj
|
||||
from esphome.cpp_helpers import ComponentSourcePool, register_component_source
|
||||
|
||||
@@ -253,6 +254,111 @@ def test_slot_counter_request_after_emit_raises() -> None:
|
||||
request()
|
||||
|
||||
|
||||
def _component_var(id_: str, cpp_type: str = "foo::Bar") -> Mock:
|
||||
base = Mock()
|
||||
base.__str__ = lambda self: id_
|
||||
base.type = Mock()
|
||||
base.type.__str__ = lambda self: cpp_type
|
||||
return Mock(base=base)
|
||||
|
||||
|
||||
def _looping_count_statement() -> str:
|
||||
lines = [
|
||||
str(s)
|
||||
for s in ch.CORE.global_statements
|
||||
if "constexpr size_t ESPHOME_LOOPING_COMPONENT_COUNT" in str(s)
|
||||
]
|
||||
assert len(lines) == 1
|
||||
return lines[0]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def stub_component_codegen(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""Keep the real CORE (jobs, defines) but skip emitting C++ statements."""
|
||||
monkeypatch.setattr(ch, "add", Mock())
|
||||
monkeypatch.setattr(ch, "App", Mock())
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("stub_component_codegen")
|
||||
@pytest.mark.parametrize(
|
||||
("declared", "registered", "expected"),
|
||||
[
|
||||
({"comp_a", "comp_b", "declared_only"}, ["comp_a", "comp_b"], "2"),
|
||||
({"declared_only"}, [], "0"),
|
||||
],
|
||||
)
|
||||
def test_component_count_skips_unregistered_ids(
|
||||
declared: set[str], registered: list[str], expected: str
|
||||
) -> None:
|
||||
"""An id declared with a Component type but never registered takes no slot."""
|
||||
ch.CORE.component_ids.update(declared)
|
||||
|
||||
async def registrar() -> None:
|
||||
for id_ in registered:
|
||||
await ch.register_component(_component_var(id_), {})
|
||||
|
||||
ch.CORE.add_job(ch.add_component_counts)
|
||||
ch.CORE.add_job(registrar)
|
||||
ch.CORE.flush_tasks()
|
||||
assert _define_value("ESPHOME_COMPONENT_COUNT") == expected
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("stub_component_codegen")
|
||||
def test_looping_component_count_without_components_is_zero() -> None:
|
||||
"""With no components the constexpr is still valid C++ (`= 0;`, not `= ;`)."""
|
||||
ch.CORE.add_job(ch.add_component_counts)
|
||||
ch.CORE.flush_tasks()
|
||||
rhs = _looping_count_statement().split("=", 1)[1]
|
||||
assert rhs.replace("\\\n", "").strip() == "0;"
|
||||
# The storage is sized at least 1 so a zero count is not a zero-length array
|
||||
assert any(
|
||||
"static Component *looping_components_storage[std::max<size_t>(ESPHOME_LOOPING_COMPONENT_COUNT, 1)];"
|
||||
in str(s)
|
||||
for s in ch.CORE.global_statements
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("stub_component_codegen")
|
||||
def test_component_counts_include_final_and_waiting_registrations() -> None:
|
||||
"""Registrations from FINAL jobs, and from tasks that waited long enough
|
||||
to sink below FINAL, land in both counts before they are emitted."""
|
||||
ch.CORE.component_ids.update({"final_comp", "slow_comp", "slow_comp_2"})
|
||||
|
||||
@coroutine_with_priority(CoroPriority.FINAL)
|
||||
async def final_registrar() -> None:
|
||||
await ch.register_component(_component_var("final_comp", "foo::Final"), {})
|
||||
|
||||
async def slow_registrar() -> None:
|
||||
# Each yield lowers the task's priority by one, 1500 puts it below FINAL.
|
||||
for _ in range(1500):
|
||||
await FakeAwaitable(iter([None]))
|
||||
await ch.register_component(_component_var("slow_comp", "foo::Slow"), {})
|
||||
await ch.register_component(_component_var("slow_comp_2", "foo::Slow"), {})
|
||||
|
||||
ch.CORE.add_job(ch.add_component_counts)
|
||||
ch.CORE.add_job(final_registrar)
|
||||
ch.CORE.add_job(slow_registrar)
|
||||
ch.CORE.flush_tasks()
|
||||
assert _define_value("ESPHOME_COMPONENT_COUNT") == "3"
|
||||
# Summed per type, so each type is checked once at compile time.
|
||||
looping = _looping_count_statement()
|
||||
assert "(1 * HasLoopOverride<foo::Final>::value)" in looping
|
||||
assert "(2 * HasLoopOverride<foo::Slow>::value)" in looping
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.usefixtures("stub_component_codegen")
|
||||
async def test_component_count_register_after_emit_raises() -> None:
|
||||
"""A registration after the counts were emitted would be dropped from
|
||||
App.components_ at run time, so it fails loudly and leaves the id alone."""
|
||||
ch.CORE.component_ids.add("too_late")
|
||||
ch.CORE.add_job(ch.add_component_counts)
|
||||
ch.CORE.flush_tasks()
|
||||
with pytest.raises(ValueError, match="after ESPHOME_COMPONENT_COUNT"):
|
||||
await ch.register_component(_component_var("too_late"), {})
|
||||
assert "too_late" in ch.CORE.component_ids
|
||||
|
||||
|
||||
def _load_module(
|
||||
monkeypatch: pytest.MonkeyPatch, name: str, path: Path, source: str
|
||||
) -> ModuleType:
|
||||
|
||||
Reference in new issue
Block a user