mirror of
https://github.com/esphome/esphome.git
synced 2026-10-24 06:55:37 +00:00
Compare commits
16
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b7f9057e7 | ||
|
|
fb37dc8b75 | ||
|
|
3220915d82 | ||
|
|
0ac066405f | ||
|
|
c2b23311f2 | ||
|
|
6d6f7bd2f8 | ||
|
|
af788e2c37 | ||
|
|
7e40bcd334 | ||
|
|
6069b820fa | ||
|
|
2bc39e50eb | ||
|
|
e263d52405 | ||
|
|
448790f483 | ||
|
|
a9c8d052df | ||
|
|
f77796877d | ||
|
|
2931a58a9b | ||
|
|
9d30bcc721 |
No files matched your search
@@ -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`)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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 *>>);
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in new issue
Block a user