Compare commits

..
Author SHA1 Message Date
J. Nick Koston 9b7f9057e7 [remote_base] Also require a trivial destructor for a flash dumper 2026-10-23 20:53:39 -10:00
J. Nick Koston fb37dc8b75 Merge branch 'remote-base-dumper-table-v2' into remote-base-flash-dumpers-v2 2026-10-23 20:52:27 -10:00
J. Nick Koston 3220915d82 [remote_base] Report extra secondary dumpers from dump_config 2026-10-23 20:52:27 -10:00
J. Nick Koston 0ac066405f [remote_base] Keep every dumper in flash; external protocol dumpers are stateless too 2026-10-23 20:49:07 -10:00
J. Nick Koston c2b23311f2 [remote_base] Warn when a receiver has more than one secondary dumper 2026-10-23 20:43:14 -10:00
J. Nick Koston 6d6f7bd2f8 Merge branch 'remote-base-dumper-table-v2' into remote-base-flash-dumpers-v2 2026-10-23 20:43:14 -10:00
J. Nick Koston af788e2c37 [remote_base] Fail the build when a flash dumper gains a data member 2026-10-23 20:38:31 -10:00
J. Nick Koston 7e40bcd334 [remote_base] Keep the YAML dumpers ahead of dumpers registered from another to_code 2026-10-23 20:34:34 -10:00
J. Nick Koston 6069b820fa Merge branch 'remote-base-dumper-table-v2' into remote-base-flash-dumpers-v2 2026-10-23 20:34:34 -10:00
J. Nick Koston 2bc39e50eb Merge branch 'const-vector-append-copy' into remote-base-dumper-table-v2 2026-10-23 20:34:07 -10:00
J. Nick Koston e263d52405 [core] Copy without uint32_t aliasing, share the copy with assign_copy, and add prepend_copy 2026-10-23 20:34:07 -10:00
J. Nick Koston 448790f483 [remote_base] Keep the stateless in-tree dumpers in flash 2026-10-23 20:19:30 -10:00
J. Nick Koston a9c8d052df [remote_base] Give each receiver an exact dumper table instead of a shared-size StaticVector 2026-10-23 20:19:30 -10:00
J. Nick Koston f77796877d [core] Add ConstVector::append_copy that reads flash tables a word at a time 2026-10-23 20:18:52 -10:00
J. Nick Koston 2931a58a9b [remote_base] Test dumper dispatch and registration 2026-10-23 20:04:46 -10:00
Keith BurzinskiandClaude Opus 5.5 9d30bcc721 [core] Do not warn that ESPHome's own core is an unmatched SBOM component (#21546)
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-24 00:52:50 -05:00
17 changed files with 368 additions and 57 deletions

No files matched your search

-2
View File
@@ -449,8 +449,6 @@ file does, and it is the authority when they disagree. The most useful starting
```
Restore an owned or standalone switch in `setup()` with `sw->restore()`, or `restore(default)`
to still drive a state when the restore mode is disabled.
A hub that applies its switches' restored states itself reads `sw->get_initial_write_state()`, which
already applies `inverted`.
- **Other codegen helpers:** `uart.new_uart_component(config, *ctor_args)` (and `i2c.new_i2c_component`,
`spi.new_spi_component`, `modbus.new_modbus_client_component`, `ble_client.new_ble_node_component`)
+61 -6
View File
@@ -1,4 +1,5 @@
from collections.abc import Callable
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any
@@ -53,7 +54,7 @@ from esphome.const import (
CONF_WAND_ID,
CONF_ZERO,
)
from esphome.core import ID, coroutine
from esphome.core import CORE, ID, CoroPriority, coroutine, coroutine_with_priority
from esphome.cpp_generator import MockObj
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
from esphome.types import ConfigType, SafeExpType, TemplateArgsType
@@ -115,9 +116,10 @@ REMOTE_TRANSMITTABLE_SCHEMA = cv.Schema(
)
# Listener and dumper lists are StaticVectors sized from these counts, so every registration
# must go through add_listener / add_dumper. Every receiver's list gets the same capacity, so
# the slots are keyed by receiver and the define is the largest count any one receiver needs.
# Listener lists are StaticVectors sized from these counts, so every registration must go
# through add_listener. Every receiver's list gets the same capacity, so the slots are keyed by
# receiver and the define is the largest count any one receiver needs. Dumpers go into an exact
# per receiver table; their define only marks that dumper storage and register_dumper are compiled in.
LISTENER_COUNT_DEFINE = "REMOTE_BASE_LISTENER_COUNT"
DUMPER_COUNT_DEFINE = "REMOTE_BASE_DUMPER_COUNT"
@@ -131,9 +133,42 @@ def add_listener(receiver: MockObj, listener: MockObj) -> None:
cg.add(receiver.register_listener(listener))
@dataclass
class _DumperTables:
# receiver id -> (receiver, its dumpers in registration order)
receivers: dict[str, tuple[MockObj, list[MockObj]]] = field(default_factory=dict)
emitted: bool = False
def _get_dumper_tables() -> _DumperTables:
return CORE.data.setdefault(f"{DOMAIN}_dumpers", _DumperTables())
@coroutine_with_priority(CoroPriority.FINAL)
async def _emit_dumper_tables() -> None:
tables = _get_dumper_tables()
tables.emitted = True
for receiver, dumpers in tables.receivers.values():
table = cg.shared_progmem_array(
"remote_dumpers",
RemoteReceiverDumperBase.operator("ptr"),
cg.ArrayInitializer(*dumpers),
constexpr=False,
)
cg.add(receiver.set_dumpers(table, len(dumpers)))
def add_dumper(receiver: MockObj, dumper: MockObj) -> None:
tables = _get_dumper_tables()
if tables.emitted:
raise ValueError(
"remote_base.add_dumper: the dumper tables were already emitted; add dumpers from to_code"
)
_request_dumper_slot(str(receiver))
cg.add(receiver.register_dumper(dumper))
receivers = tables.receivers
if not receivers:
CORE.add_job(_emit_dumper_tables)
receivers.setdefault(str(receiver), (receiver, []))[1].append(dumper)
async def register_listener(var: MockObj, config: ConfigType) -> None:
@@ -233,7 +268,27 @@ def register_trigger(name, type, data_type):
return decorator
def _new_flash_dumper(dumper_id: ID) -> cg.RawExpression:
"""Place a stateless dumper in flash; the receiver only ever calls it."""
storage = f"{dumper_id.id}__flash"
CORE.add_global_after_includes(
cg.RawStatement(
f"static_assert(sizeof({dumper_id.type}) == sizeof(void *) && "
f"std::is_trivially_destructible_v<{dumper_id.type}>, "
'"a dumper kept in flash must have no data members or destructor");'
)
)
CORE.add_global_after_includes(
cg.RawStatement(f"static constexpr {dumper_id.type} {storage} PROGMEM{{}};")
)
# A constant address, so it can sit in a receiver's flash dumper table
var = cg.RawExpression(f"const_cast<{dumper_id.type} *>(&{storage})")
CORE.register_variable(dumper_id, var)
return var
def register_dumper(name, type, schema=None):
"""Dumpers are stateless, so each is a flash object; one with data members fails the build."""
if schema is None:
schema = {}
registerer = DUMPER_REGISTRY.register(name, type, schema)
@@ -241,7 +296,7 @@ def register_dumper(name, type, schema=None):
def decorator(func):
async def new_func(config, dumper_id):
_request_protocol_if_in_tree(name)
var = cg.new_Pvariable(dumper_id)
var = _new_flash_dumper(dumper_id)
await coroutine(func)(var, config)
return var
+26 -16
View File
@@ -103,8 +103,7 @@ bool RemoteReceiverBinarySensorBase::on_receive(RemoteReceiveData src) {
#ifdef REMOTE_BASE_LISTENER_COUNT
void RemoteReceiverBase::register_listener(RemoteReceiverListener *listener) {
if (this->listeners_.size() == REMOTE_BASE_LISTENER_COUNT) {
ESP_LOGE(TAG, "No %s slot: register it from to_code() with remote_base.add_%s", LOG_STR_LITERAL("listener"),
LOG_STR_LITERAL("listener"));
ESP_LOGE(TAG, "No listener slot: register it from to_code() with remote_base.add_listener");
return;
}
this->listeners_.push_back(listener);
@@ -112,18 +111,24 @@ void RemoteReceiverBase::register_listener(RemoteReceiverListener *listener) {
#endif
#ifdef REMOTE_BASE_DUMPER_COUNT
void RemoteReceiverBase::register_dumper(RemoteReceiverDumperBase *dumper) {
if (dumper->is_secondary()) {
if (this->secondary_dumper_ == nullptr) {
this->secondary_dumper_ = dumper;
return;
}
} else if (this->dumpers_.size() != REMOTE_BASE_DUMPER_COUNT) {
this->dumpers_.push_back(dumper);
return;
void RemoteReceiverBase::set_dumpers(RemoteReceiverDumperBase *const *dumpers, size_t count) {
if (this->dumpers_.empty()) {
this->dumpers_.assign_static(dumpers, count);
} else {
// register_dumper() ran first from another to_code(): the YAML dumpers still go first, as on a StaticVector
this->dumpers_.prepend_copy(dumpers, count);
}
ESP_LOGE(TAG, "No %s slot: register it from to_code() with remote_base.add_%s", LOG_STR_LITERAL("dumper"),
LOG_STR_LITERAL("dumper"));
}
void RemoteReceiverBase::register_dumper(RemoteReceiverDumperBase *dumper) { this->dumpers_.append_copy(&dumper, 1); }
// Only the first secondary dumper runs; from dump_config() so it is seen however the dumpers were registered
void RemoteReceiverBase::warn_extra_secondaries_() {
uint8_t secondaries = 0;
for (auto *dumper : this->dumpers_)
secondaries += dumper->is_secondary();
if (secondaries > 1)
ESP_LOGW(TAG, "Only the first of %u secondary dumpers runs", secondaries);
}
#endif
@@ -134,12 +139,17 @@ void RemoteReceiverBase::call_listeners_dumpers_() {
#endif
#ifdef REMOTE_BASE_DUMPER_COUNT
bool success = false;
RemoteReceiverDumperBase *secondary = nullptr;
for (auto *dumper : this->dumpers_) {
if (dumper->dump(RemoteReceiveData(this->temp_, this->tolerance_, this->tolerance_mode_)))
if (dumper->is_secondary()) {
if (secondary == nullptr)
secondary = dumper;
} else if (dumper->dump(RemoteReceiveData(this->temp_, this->tolerance_, this->tolerance_mode_))) {
success = true;
}
}
if (!success && this->secondary_dumper_ != nullptr)
this->secondary_dumper_->dump(RemoteReceiveData(this->temp_, this->tolerance_, this->tolerance_mode_));
if (!success && secondary != nullptr)
secondary->dump(RemoteReceiveData(this->temp_, this->tolerance_, this->tolerance_mode_));
#endif
}
+10 -4
View File
@@ -337,8 +337,7 @@ class RemoteReceiverDumperBase {
class RemoteReceiverBase : public RemoteComponentBase {
public:
RemoteReceiverBase(InternalGPIOPin *pin) : RemoteComponentBase(pin) {}
// Slots are counted at code generation; without one the call fails at compile time with the same message
// the runtime check logs
// Listener slots are counted at code generation; without one the call fails at compile time
#ifdef REMOTE_BASE_LISTENER_COUNT
void register_listener(RemoteReceiverListener *listener);
#else
@@ -347,6 +346,10 @@ class RemoteReceiverBase : public RemoteComponentBase {
}
#endif
#ifdef REMOTE_BASE_DUMPER_COUNT
/// Codegen only (remote_base.add_dumper): the table must outlive this receiver.
void set_dumpers(RemoteReceiverDumperBase *const *dumpers, size_t count);
/// For components that register a dumper from C++, also from their own setup(); each call copies the list to the
/// heap.
void register_dumper(RemoteReceiverDumperBase *dumper);
#else
template<typename T> void register_dumper(T *) {
@@ -359,14 +362,17 @@ class RemoteReceiverBase : public RemoteComponentBase {
}
protected:
#ifdef REMOTE_BASE_DUMPER_COUNT
void warn_extra_secondaries_();
#endif
void call_listeners_dumpers_();
#ifdef REMOTE_BASE_LISTENER_COUNT
StaticVector<RemoteReceiverListener *, REMOTE_BASE_LISTENER_COUNT> listeners_;
#endif
#ifdef REMOTE_BASE_DUMPER_COUNT
StaticVector<RemoteReceiverDumperBase *, REMOTE_BASE_DUMPER_COUNT> dumpers_;
RemoteReceiverDumperBase *secondary_dumper_{nullptr}; // runs only when no primary dumper matched
// The first secondary dumper runs only when no primary dumper matched
ConstVector<RemoteReceiverDumperBase *, true> dumpers_;
#endif
RawTimings temp_;
uint32_t tolerance_{25};
@@ -88,6 +88,9 @@ void RemoteReceiverComponent::dump_config() {
(this->tolerance_mode_ == remote_base::TOLERANCE_MODE_TIME) ? LOG_STR_LITERAL(" us") : LOG_STR_LITERAL("%"),
this->filter_us_, this->idle_us_);
LOG_PIN(" Pin: ", this->pin_);
#ifdef REMOTE_BASE_DUMPER_COUNT
this->warn_extra_secondaries_();
#endif
}
void RemoteReceiverComponent::loop() {
@@ -140,6 +140,9 @@ void RemoteReceiverComponent::dump_config() {
(this->tolerance_mode_ == remote_base::TOLERANCE_MODE_TIME) ? LOG_STR_LITERAL(" us") : LOG_STR_LITERAL("%"),
this->carrier_frequency_, this->carrier_duty_percent_, this->filter_us_, this->idle_us_);
LOG_PIN(" Pin: ", this->pin_);
#ifdef REMOTE_BASE_DUMPER_COUNT
this->warn_extra_secondaries_();
#endif
}
void RemoteReceiverComponent::loop() {
-5
View File
@@ -60,11 +60,6 @@ optional<bool> Switch::get_initial_state_with_restore_mode() {
}
return initial_state;
}
optional<bool> Switch::get_initial_write_state() {
if (auto initial = this->get_initial_state_with_restore_mode())
return *initial != this->inverted_;
return {};
}
void Switch::restore() {
if (auto initial = this->get_initial_state_with_restore_mode())
this->control(*initial);
-2
View File
@@ -114,8 +114,6 @@ class Switch : public EntityBase {
* state.
*/
optional<bool> get_initial_state_with_restore_mode();
/// get_initial_state_with_restore_mode() with inverted applied: the level write_state() takes.
optional<bool> get_initial_write_state();
/// Drive the switch to the state its restore mode yields, through control(), so inversion and
/// write_state() apply; a disabled restore mode drives \p when_disabled, or nothing without one.
+23 -7
View File
@@ -177,14 +177,13 @@ template<typename T> class ConstVector<T, true> {
this->data_ = data;
this->size_ = size;
}
/// Appends count elements into one new heap array this owns; the current list and extra may be codegen tables in
/// flash.
void append_copy(const T *extra, size_t count) { this->merge_copy_(this->data_, this->size(), extra, count); }
/// Like append_copy(), with extra placed in front of the current list.
void prepend_copy(const T *extra, size_t count) { this->merge_copy_(extra, count, this->data_, this->size()); }
/// Copies the list into a heap array this owns, freeing a previous owned copy.
void assign_copy(const T *data, size_t size) {
auto *table = new T[size]; // NOLINT(cppcoreguidelines-owning-memory)
std::copy(data, data + size, table);
this->release_();
this->data_ = table;
this->size_ = size | OWNED_BIT;
}
void assign_copy(const T *data, size_t size) { this->merge_copy_(data, size, nullptr, 0); }
protected:
static constexpr size_t OWNED_BIT = size_t{1} << (sizeof(size_t) * 8 - 1);
@@ -193,6 +192,23 @@ template<typename T> class ConstVector<T, true> {
if (this->size_ & OWNED_BIT)
delete[] this->data_; // NOLINT(cppcoreguidelines-owning-memory)
}
void merge_copy_(const T *first, size_t first_count, const T *second, size_t second_count) {
auto *table = new T[first_count + second_count]; // NOLINT(cppcoreguidelines-owning-memory)
copy_words_(first, first_count, table);
copy_words_(second, second_count, table + first_count);
this->release_(); // after the copies: first or second may be the current list
this->data_ = table;
this->size_ = (first_count + second_count) | OWNED_BIT;
}
// Word loads through volatile, so the loop never becomes a memcpy that reads ESP8266 flash byte by byte
static void copy_words_(const T *src, size_t count, T *dst) {
auto *from = reinterpret_cast<const volatile uint32_t *>(src);
auto *to = reinterpret_cast<uint8_t *>(dst);
for (size_t i = 0; i < count * sizeof(T) / sizeof(uint32_t); i++) {
const uint32_t word = from[i];
std::memcpy(to + i * sizeof(uint32_t), &word, sizeof(word)); // into RAM: no aliasing through uint32_t
}
}
const T *data_{nullptr};
size_t size_{0}; // top bit set when data_ is an owned heap copy
+3 -1
View File
@@ -296,7 +296,9 @@ def _external_manifests() -> list[dict[str, Any]]:
found[component_dir] = source
break
else:
if not component_dir.is_relative_to(loader.CORE_COMPONENTS_PATH):
# Built in means anywhere in the package: the esphome domain itself
# is esphome/core, not under esphome/components
if not component_dir.is_relative_to(loader.CORE_COMPONENTS_PATH.parent):
_LOGGER.warning(
"SBOM: component '%s' in %s matches no external component "
"source and is listed as part of ESPHome",
@@ -0,0 +1,19 @@
esphome:
name: test
esp32:
board: esp32dev
logger:
remote_receiver:
- id: ir_rcvr
pin: GPIO4
dump:
- nec
- rc_switch
- raw
- id: rf_rcvr
pin: GPIO5
dump:
- rc_switch
@@ -2,6 +2,7 @@
from collections.abc import Callable, Generator
from pathlib import Path
import re
import sys
import pytest
@@ -11,6 +12,7 @@ from esphome.automation import ACTION_REGISTRY
from esphome.components import remote_base
import esphome.config_validation as cv
from esphome.core import CORE
from esphome.cpp_generator import MockObj
from ..helpers import get_define_value
@@ -135,3 +137,62 @@ def test_dump_list_is_deduplicated_across_forms() -> None:
def test_dump_list_rejects_invalid_entries_with_a_validation_error(bad: list) -> None:
with pytest.raises(cv.Invalid):
remote_base.validate_dumpers(bad)
def test_dumper_count_is_the_busiest_receivers(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
generate_main(component_config_path("receiver_two_dumper_lists.yaml"))
# nec, rc_switch and raw on one receiver, rc_switch on the other
assert get_define_value("REMOTE_BASE_DUMPER_COUNT") == "3"
@pytest.mark.usefixtures("restore_protocol_registries")
def test_external_dumpers_live_in_flash(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
"""External protocol dumpers are stateless too; the size check guards one that is not."""
main_cpp = generate_main(
component_config_path("receiver_with_external_protocol.yaml")
)
assert re.search(
r"static constexpr fake_protocol::FakeDumper \w+ PROGMEM\{\};", main_cpp
)
assert (
"static_assert(sizeof(fake_protocol::FakeDumper) == sizeof(void *)" in main_cpp
)
def test_each_receiver_gets_its_own_dumper_table(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
main_cpp = generate_main(component_config_path("receiver_two_dumper_lists.yaml"))
assert "register_dumper" not in main_cpp
assert re.search(r"ir_rcvr->set_dumpers\(remote_dumpers\w*, 3\);", main_cpp)
assert re.search(r"rf_rcvr->set_dumpers\(remote_dumpers\w*, 1\);", main_cpp)
def test_add_dumper_after_the_tables_were_emitted_raises() -> None:
"""The tables are written once at FINAL; a later dumper would otherwise be dropped."""
remote_base._get_dumper_tables().emitted = True
try:
with pytest.raises(ValueError, match="already emitted"):
remote_base.add_dumper(MockObj("rcvr", "->"), MockObj("dumper", "->"))
finally:
CORE.data.pop(f"{remote_base.DOMAIN}_dumpers")
def test_in_tree_dumpers_live_in_flash(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
main_cpp = generate_main(component_config_path("receiver_two_dumper_lists.yaml"))
assert re.search(
r"static constexpr remote_base::NECDumper \w+ PROGMEM\{\};", main_cpp
)
assert not re.search(r"new\(\w+\) remote_base::\w+Dumper\(\);", main_cpp)
# A dumper that gains a member must fail the build, not fault on the device
assert "static_assert(sizeof(remote_base::NECDumper) == sizeof(void *)" in main_cpp
@@ -78,6 +78,40 @@ TEST(ConstVector, CopyFromItsOwnStorage) {
EXPECT_STREQ(list[2], "c");
}
TEST(ConstVector, AppendCopiesTheTableAndTheExtrasIntoOneOwnedArray) {
static constexpr const char *const EXTRA[] = {"d", "e"};
ProbeVector empty;
empty.append_copy(EXTRA, 1);
EXPECT_TRUE(empty.owned());
ASSERT_EQ(empty.size(), 1U);
EXPECT_STREQ(empty[0], "d");
ProbeVector list;
list.assign_static(TABLE, 3);
list.append_copy(EXTRA, 2);
EXPECT_TRUE(list.owned());
EXPECT_NE(list.data(), TABLE);
ASSERT_EQ(list.size(), 5U);
EXPECT_TRUE(std::equal(TABLE, TABLE + 3, list.begin()));
EXPECT_STREQ(list[3], "d");
EXPECT_STREQ(list[4], "e");
list.append_copy(EXTRA, 1); // frees the previous owned array
EXPECT_EQ(list.size(), 6U);
EXPECT_STREQ(list[5], "d");
}
TEST(ConstVector, PrependPutsTheExtrasInFront) {
static constexpr const char *const EXTRA[] = {"d", "e"};
ProbeVector list;
list.assign_static(TABLE, 3);
list.append_copy(EXTRA, 1);
list.prepend_copy(EXTRA + 1, 1); // frees the previous owned array
ASSERT_EQ(list.size(), 5U);
EXPECT_STREQ(list[0], "e");
EXPECT_TRUE(std::equal(TABLE, TABLE + 3, list.begin() + 1));
EXPECT_STREQ(list[4], "d");
}
TEST(ConstVector, PlainViewHasNoOwnershipCost) {
static_assert(std::is_trivially_copyable_v<ConstVector<const char *>>);
static_assert(std::is_trivially_destructible_v<ConstVector<const char *>>);
+3
View File
@@ -1,3 +1,4 @@
import esphome.codegen as cg
from esphome.types import ConfigType
from tests.testing_helpers import ComponentManifestOverride
@@ -24,6 +25,8 @@ def override_manifest(manifest: ComponentManifestOverride) -> None:
"panasonic",
):
request_protocol(protocol)
# receiver_dumpers_test.cpp registers dumpers by hand
cg.add_define("REMOTE_BASE_DUMPER_COUNT", 4)
manifest.to_code = to_code_testing
# AUTO_LOAD is not resolved by the unit test build.
@@ -0,0 +1,109 @@
#include <gtest/gtest.h>
#include <vector>
#include "esphome/components/remote_base/remote_base.h"
namespace esphome::remote_base::testing {
class RecordingDumper : public RemoteReceiverDumperBase {
public:
RecordingDumper(std::vector<int> *calls, int id, bool matches, bool secondary = false)
: calls_(calls), id_(id), matches_(matches), secondary_(secondary) {}
bool dump(RemoteReceiveData /*src*/) override {
this->calls_->push_back(this->id_);
return this->matches_;
}
bool is_secondary() override { return this->secondary_; }
protected:
std::vector<int> *calls_;
int id_;
bool matches_;
bool secondary_;
};
class TestReceiver : public RemoteReceiverBase {
public:
TestReceiver() : RemoteReceiverBase(nullptr) {}
void receive() { this->call_listeners_dumpers_(); }
};
TEST(RemoteReceiverDumpers, PrimariesRunInOrderAndAMatchSkipsTheSecondary) {
std::vector<int> calls;
RecordingDumper raw(&calls, 0, true, true), first(&calls, 1, false), second(&calls, 2, true), third(&calls, 3, false);
TestReceiver receiver;
receiver.register_dumper(&raw);
receiver.register_dumper(&first);
receiver.register_dumper(&second);
receiver.register_dumper(&third);
receiver.receive();
EXPECT_EQ(calls, (std::vector<int>{1, 2, 3}));
}
TEST(RemoteReceiverDumpers, SecondaryRunsWhenNoPrimaryMatched) {
std::vector<int> calls;
RecordingDumper raw(&calls, 0, true, true), miss(&calls, 1, false);
TestReceiver receiver;
receiver.register_dumper(&miss);
receiver.register_dumper(&raw);
receiver.receive();
EXPECT_EQ(calls, (std::vector<int>{1, 0}));
}
TEST(RemoteReceiverDumpers, OnlyTheFirstSecondaryRuns) {
std::vector<int> calls;
RecordingDumper raw(&calls, 0, true, true), extra_raw(&calls, 9, true, true);
TestReceiver receiver;
receiver.register_dumper(&raw);
receiver.register_dumper(&extra_raw);
receiver.receive();
EXPECT_EQ(calls, (std::vector<int>{0}));
}
// External components (vevor decoders) register themselves from their own setup()
TEST(RemoteReceiverDumpers, RegisteringAfterConstructionAddsTheDumper) {
std::vector<int> calls;
RecordingDumper early(&calls, 1, false), late(&calls, 2, false);
TestReceiver receiver;
receiver.register_dumper(&early);
receiver.receive();
receiver.register_dumper(&late);
receiver.receive();
EXPECT_EQ(calls, (std::vector<int>{1, 1, 2}));
}
TEST(RemoteReceiverDumpers, CodegenTableKeepsTheSameDispatch) {
std::vector<int> calls;
RecordingDumper raw(&calls, 0, true, true), miss(&calls, 1, false), extra_raw(&calls, 9, true, true);
RemoteReceiverDumperBase *const table[] = {&raw, &miss, &extra_raw};
TestReceiver receiver;
receiver.set_dumpers(table, 3);
receiver.receive();
EXPECT_EQ(calls, (std::vector<int>{1, 0}));
}
TEST(RemoteReceiverDumpers, RegisterDumperKeepsTheCodegenTable) {
std::vector<int> calls;
RecordingDumper from_table(&calls, 1, false), early(&calls, 2, false), late(&calls, 3, false);
TestReceiver receiver;
receiver.register_dumper(&early); // an external component registering from to_code()
RemoteReceiverDumperBase *const table[] = {&from_table};
receiver.set_dumpers(table, 1);
receiver.register_dumper(&late); // or from its own setup()
receiver.receive();
EXPECT_EQ(calls, (std::vector<int>{1, 2, 3})); // the YAML table first, as before the table existed
}
TEST(RemoteReceiverDumpers, RawFromTheTableStaysTheSecondaryOverAnEarlierOne) {
std::vector<int> calls;
RecordingDumper raw(&calls, 1, false, true), external(&calls, 2, false, true);
TestReceiver receiver;
receiver.register_dumper(&external); // an external secondary registered from to_code()
RemoteReceiverDumperBase *const table[] = {&raw};
receiver.set_dumpers(table, 1);
receiver.receive();
EXPECT_EQ(calls, (std::vector<int>{1}));
}
} // namespace esphome::remote_base::testing
-12
View File
@@ -46,16 +46,4 @@ TEST(SwitchRestore, DefaultAppliesOnlyWhenTheModeIsDisabled) {
EXPECT_EQ(on.writes, std::vector<bool>{true});
}
TEST(SwitchRestore, InitialWriteStateAppliesInversion) {
RecordingSwitch on, inverted, disabled;
on.set_restore_mode(SWITCH_ALWAYS_ON);
inverted.set_restore_mode(SWITCH_ALWAYS_ON);
inverted.set_inverted(true);
disabled.set_restore_mode(SWITCH_RESTORE_DISABLED);
disabled.set_inverted(true);
EXPECT_EQ(on.get_initial_write_state(), optional<bool>(true));
EXPECT_EQ(inverted.get_initial_write_state(), optional<bool>(false));
EXPECT_FALSE(disabled.get_initial_write_state().has_value());
}
} // namespace esphome::switch_::testing
+13 -2
View File
@@ -386,7 +386,7 @@ def test_external_manifests_skip_other_components(
components_dir = tmp_path / "components"
get_sources().append(ExternalSource(components_dir=components_dir))
_add_external_component(components_dir, "widget")
_add_external_component(tmp_path / "builtin", "wifi")
_add_external_component(tmp_path / "pkg" / "components", "wifi")
CORE.loaded_integrations.update({"missing", "namespace"})
sbom.loader._COMPONENT_CACHE["namespace"] = SimpleNamespace(
module=SimpleNamespace()
@@ -399,7 +399,9 @@ def test_external_manifests_skip_other_components(
side_effect=sbom.loader._COMPONENT_CACHE.get,
),
patch.object(
sbom.loader, "CORE_COMPONENTS_PATH", (tmp_path / "builtin").resolve()
sbom.loader,
"CORE_COMPONENTS_PATH",
(tmp_path / "pkg" / "components").resolve(),
),
):
manifests = sbom._external_manifests()
@@ -703,3 +705,12 @@ def test_checkout_remote_detached(tmp_path: Path) -> None:
"https://user:token@github.com/someone/esphome.git",
)
assert not any(call.args[0][1] == "config" for call in run.call_args_list)
def test_external_manifests_esphome_core_is_built_in(
caplog: pytest.LogCaptureFixture,
) -> None:
"""The esphome domain resolves to esphome/core, which is still part of ESPHome."""
CORE.loaded_integrations.add("esphome")
assert sbom._external_manifests() == []
assert "SBOM" not in caplog.text