Merge remote-tracking branch 'origin/dev' into store-yaml-firmware

# Conflicts:
#	esphome/components/api/api.proto
#	esphome/components/api/api_pb2.h
#	esphome/components/api/api_pb2_service.cpp
#	esphome/yaml_util.py
#	tests/integration/conftest.py
#	tests/unit_tests/test_yaml_util.py
This commit is contained in:
J. Nick Koston
2026-09-14 11:07:43 -05:00
2889 changed files with 129953 additions and 28736 deletions
+3
View File
@@ -28,6 +28,7 @@ create an `__init__.py` in your component's test directory and define `override_
```python
from tests.testing_helpers import ComponentManifestOverride
def override_manifest(manifest: ComponentManifestOverride) -> None:
# Re-enable the component's own to_code (needed when the component must
# emit C++ setup code that the test binary depends on at link time).
@@ -39,6 +40,7 @@ Or supply a lightweight stub instead of the real `to_code`:
```python
from tests.testing_helpers import ComponentManifestOverride
def override_manifest(manifest: ComponentManifestOverride) -> None:
async def to_code_testing(config):
# Only emit what the C++ tests actually need
@@ -54,6 +56,7 @@ e.g. `tests/components/my_sensor/sensor/__init__.py`):
```python
from tests.testing_helpers import ComponentManifestOverride
def override_manifest(manifest: ComponentManifestOverride) -> None:
manifest.enable_codegen()
```
+1
View File
@@ -4,3 +4,4 @@ output:
gate_pin: ${gate_pin}
zero_cross_pin: ${zero_cross_pin}
zero_cross_interrupt_type: ANY
min_power: 0%
@@ -0,0 +1,7 @@
# Deprecated `pin: TEMPERATURE`, superseded by the `internal_temperature` platform.
# Remove before 2027.2.0
sensor:
- id: adc_temperature_sensor
platform: adc
pin: TEMPERATURE
name: ADC Test temperature
@@ -3,7 +3,7 @@ light:
id: led_matrix_32x8
default_transition_length: 500ms
chipset: ws2812
rgb_order: GRB
channel_colors: GRB
num_leds: 256
pin: ${pin}
@@ -3,7 +3,7 @@ light:
id: led_matrix_32x8
default_transition_length: 500ms
chipset: ws2812
rgb_order: GRB
channel_colors: GRB
num_leds: 256
pin: ${pin}
@@ -0,0 +1 @@
airthings_ble:
@@ -0,0 +1,6 @@
esp32_ble_tracker:
id: ble_tracker_hub
# Explicit ble_hub_id: pins the neutral binding as a declared key.
airthings_ble:
ble_hub_id: ble_tracker_hub
@@ -0,0 +1,3 @@
packages:
ble: !include ../../test_build_components/common/ble/esp32-idf.yaml
airthings_ble: !include common.yaml
@@ -0,0 +1,3 @@
packages:
ln882h_ble_tracker: !include ../ln882h_ble_tracker/common.yaml
airthings_ble: !include common-ln.yaml
@@ -0,0 +1,8 @@
# Config-only: the CI base board (generic-bk7252, BLE 4.2) cannot compile the
# BLE 5.x tracker, so this fixture proves validation (schema + neutral binding)
# on a non-esp32 platform; codegen and compilation are not exercised here.
bk72xx_ble_tracker:
id: ble_tracker_hub
airthings_ble:
ble_hub_id: ble_tracker_hub
@@ -0,0 +1,21 @@
# `platform: animation` entry exercising the shared `defaults:`/`files:` expansion.
display:
- platform: sdl
id: animation_display
auto_clear_enabled: false
dimensions:
width: 480
height: 480
image:
- platform: animation
defaults:
type: rgb565
transparency: opaque
resize: 50x50
files:
- id: platform_defaults_animation
file: $component_dir/anim.gif
- id: platform_defaults_animation_rgb
file: $component_dir/anim.apng
type: rgb
+17
View File
@@ -0,0 +1,17 @@
import esphome.codegen as cg
from esphome.core import CORE
from tests.testing_helpers import ComponentManifestOverride
def override_manifest(manifest: ComponentManifestOverride) -> None:
# USE_API compiles every api source, so emit what they need. No socket
# override: an __init__.py there makes pytest import its conftest as socket.conftest.
async def to_code_testing(config):
cg.add_define("USE_API")
cg.add_define("USE_API_PLAINTEXT")
cg.add_define("API_MAX_SEND_QUEUE", 8)
cg.add_define("MAX_API_CONNECTIONS", 1)
cg.add_define("USE_SOCKET_IMPL_BSD_SOCKETS")
CORE.register_controller() # api_server registers with the controller registry
manifest.to_code = to_code_testing
+13 -1
View File
@@ -9,6 +9,14 @@ esphome:
event: esphome.button_pressed
data:
message: Button was pressed
- homeassistant.event:
event: esphome.button_pressed_with_variables
data_template:
message: Button {{ button_name }} ({{ button_index }}) was pressed from {{ button_source }}
variables:
button_name: !lambda 'return std::string("test_button");'
button_index: !lambda 'return 1;'
button_source: static_value
- homeassistant.action:
action: notify.html5
data:
@@ -53,8 +61,12 @@ api:
reboot_timeout: 0min
actions:
- action: hello_world
description: Log a greeting
variables:
name: string
name:
type: string
description: Name to greet
example: World
then:
- logger.log:
format: Hello World %s!
+2 -1
View File
@@ -1,4 +1,5 @@
<<: !include common-base.yaml
packages:
base: !include common-base.yaml
api:
encryption:
+65
View File
@@ -0,0 +1,65 @@
#include <gtest/gtest.h>
#include <cstdint>
#include <cstring>
#include "esphome/components/api/api_buffer.h"
namespace esphome::api::testing {
// Pointer plus two 16 bit sizes
static_assert(sizeof(APIBuffer) <= 2 * sizeof(void *));
TEST(APIBuffer, RefusesSizesAbove16Bits) {
APIBuffer buf;
ASSERT_TRUE(buf.resize(16));
EXPECT_FALSE(buf.reserve(UINT16_MAX + 1));
EXPECT_EQ(buf.size(), 16u);
EXPECT_EQ(buf.capacity(), 16u);
EXPECT_TRUE(buf.reserve(UINT16_MAX));
EXPECT_EQ(buf.capacity(), UINT16_MAX);
}
static const uint8_t BYTES[] = {1, 2, 3, 4, 5, 6};
TEST(APIBuffer, AppendReturnsTheNewBytes) {
APIBuffer buf;
ASSERT_TRUE(buf.reserve(8));
uint8_t *first = buf.append(3);
ASSERT_NE(first, nullptr);
std::memcpy(first, BYTES, 3);
EXPECT_EQ(buf.size(), 3u);
EXPECT_EQ(buf.capacity(), 8u);
// Grows through realloc and keeps what was there
uint8_t *second = buf.append(6);
ASSERT_EQ(second, buf.data() + 3);
std::memcpy(second, BYTES + 3, 3);
EXPECT_EQ(buf.size(), 9u);
EXPECT_EQ(buf.capacity(), 9u);
EXPECT_EQ(std::memcmp(buf.data(), BYTES, 6), 0);
}
TEST(APIBuffer, DropFrontSlidesTheRestDown) {
APIBuffer buf;
uint8_t *bytes = buf.append(6);
ASSERT_NE(bytes, nullptr);
std::memcpy(bytes, BYTES, 6);
buf.drop_front(2);
EXPECT_EQ(buf.size(), 4u);
EXPECT_EQ(buf.capacity(), 6u);
EXPECT_EQ(std::memcmp(buf.data(), BYTES + 2, 4), 0);
// Growing afterwards keeps the slid bytes
ASSERT_TRUE(buf.reserve(64));
EXPECT_EQ(buf.size(), 4u);
EXPECT_EQ(std::memcmp(buf.data(), BYTES + 2, 4), 0);
// Dropping everything leaves an empty buffer with its capacity
buf.drop_front(4);
EXPECT_EQ(buf.size(), 0u);
EXPECT_EQ(buf.capacity(), 64u);
}
} // namespace esphome::api::testing
@@ -0,0 +1,510 @@
#include <gtest/gtest.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <unistd.h>
#include <algorithm>
#include <cerrno>
#include <cstdint>
#include <cstring>
#include <initializer_list>
#include <memory>
#include <vector>
#include "esphome/components/api/api_overflow_buffer.h"
#ifdef USE_HOST
namespace esphome::api::testing {
// Idle cost is the buffer plus one word of bookkeeping
static_assert(sizeof(APIOverflowBuffer) <= sizeof(APIBuffer) + sizeof(void *));
// Exposes storage so tests can check it is reused, not reallocated
class TestOverflowBuffer : public APIOverflowBuffer {
public:
using APIOverflowBuffer::LEN_PREFIX;
using APIOverflowBuffer::MAX_BYTES;
using APIOverflowBuffer::MAX_LONE_BYTES;
struct Storage {
size_t capacity;
const uint8_t *data;
bool operator==(const Storage &) const = default;
};
size_t capacity() const { return this->buf_.capacity(); }
Storage storage() const { return {this->buf_.capacity(), this->buf_.data()}; }
uint8_t count() const { return this->count_; }
size_t live() const { return this->buf_.size() - this->head_; }
/// Simulates a socket write inside try_drain() re-entering the send path
void set_draining(bool draining) { this->draining_ = draining; }
};
static std::vector<uint8_t> make_message(size_t len, uint8_t seed) {
std::vector<uint8_t> msg(len);
for (size_t i = 0; i < len; i++)
msg[i] = static_cast<uint8_t>(seed + i);
return msg;
}
static bool enqueue(TestOverflowBuffer &buf, const std::vector<uint8_t> &msg, uint16_t skip = 0) {
struct iovec iov = {const_cast<uint8_t *>(msg.data()), msg.size()};
return buf.enqueue_iov(&iov, 1, static_cast<uint16_t>(msg.size()), skip);
}
static void append(std::vector<uint8_t> &dst, const std::vector<uint8_t> &src, size_t skip = 0) {
dst.insert(dst.end(), src.begin() + skip, src.end());
}
static std::vector<uint8_t> concat(std::initializer_list<std::vector<uint8_t>> parts) {
std::vector<uint8_t> out;
for (const auto &part : parts)
append(out, part);
return out;
}
/// The pipe delivers the filler first, then the drained messages.
static void expect_after_filler(const std::vector<uint8_t> &received, size_t filler,
const std::vector<uint8_t> &expected) {
ASSERT_EQ(received.size(), filler + expected.size());
EXPECT_TRUE(std::equal(expected.begin(), expected.end(), received.begin() + filler));
}
// Non-blocking socket pair with small buffers, so the writer fills like a stalled TCP connection
class OverflowBufferTest : public ::testing::Test {
protected:
void SetUp() override {
int fds[2];
ASSERT_EQ(::socketpair(AF_UNIX, SOCK_STREAM, 0, fds), 0);
int size = 4096;
ASSERT_EQ(::setsockopt(fds[0], SOL_SOCKET, SO_SNDBUF, &size, sizeof(size)), 0);
ASSERT_EQ(::setsockopt(fds[1], SOL_SOCKET, SO_RCVBUF, &size, sizeof(size)), 0);
ASSERT_EQ(::fcntl(fds[1], F_SETFL, O_NONBLOCK), 0);
this->reader_ = fds[1];
this->sock_ = std::make_unique<socket::Socket>(fds[0]);
ASSERT_EQ(this->sock_->setblocking(false), 0);
}
void TearDown() override { ::close(this->reader_); }
/// Write filler until the socket refuses; returns the bytes accepted
size_t fill_pipe_() {
uint8_t junk[512];
std::memset(junk, 0xEE, sizeof(junk));
size_t total = 0;
for (;;) {
ssize_t written = this->sock_->write(junk, sizeof(junk));
if (written <= 0)
break;
total += static_cast<size_t>(written);
}
return total;
}
/// Append whatever the pipe currently holds.
void read_into_(std::vector<uint8_t> &out) {
uint8_t tmp[1024];
for (;;) {
ssize_t n = ::read(this->reader_, tmp, sizeof(tmp));
if (n <= 0)
break;
out.insert(out.end(), tmp, tmp + n);
}
}
/// Drain once; a refusal must be a would-block, never a hard error.
ssize_t drain_(TestOverflowBuffer &buf) {
ssize_t sent = buf.try_drain(this->sock_.get());
if (sent == -1) {
EXPECT_TRUE(errno == EWOULDBLOCK || errno == EAGAIN);
}
return sent;
}
/// Read and drain until the backlog is empty; returns all bytes received
std::vector<uint8_t> drain_all_(TestOverflowBuffer &buf) {
std::vector<uint8_t> received;
for (int i = 0; i < 10000 && !buf.empty(); i++) {
this->read_into_(received);
// A hard socket error would never clear the backlog; stop instead of spinning
if (this->drain_(buf) == -1 && errno != EWOULDBLOCK && errno != EAGAIN)
break;
}
EXPECT_TRUE(buf.empty());
this->read_into_(received);
return received;
}
struct Stall {
size_t filler;
std::vector<uint8_t> first, second, received;
TestOverflowBuffer::Storage before;
};
/// Park two messages, then drain the first fully and the second part way
void stall_mid_message_(TestOverflowBuffer &buf, Stall &s) {
s.filler = this->fill_pipe_();
s.first = make_message(1500, 20);
ASSERT_GT(s.filler, s.first.size()); // the first message must drain in one go
// Larger than the whole pipe, so a drain always stops inside it
s.second = make_message(std::max<size_t>(s.filler + 1, std::min<size_t>(s.filler * 3, 12000)), 60);
ASSERT_GT(s.second.size(), s.filler);
ASSERT_TRUE(enqueue(buf, s.first));
ASSERT_TRUE(enqueue(buf, s.second));
s.before = buf.storage();
this->read_into_(s.received);
ASSERT_GT(this->drain_(buf), 0);
ASSERT_EQ(buf.count(), 1);
}
int reader_{-1};
std::unique_ptr<socket::Socket> sock_;
};
TEST_F(OverflowBufferTest, IdleBufferOwnsNoStorage) {
TestOverflowBuffer buf;
EXPECT_TRUE(buf.empty());
EXPECT_EQ(buf.capacity(), 0u);
EXPECT_EQ(buf.storage().data, nullptr);
}
TEST_F(OverflowBufferTest, StorageIsReusedAcrossStalls) {
TestOverflowBuffer buf;
auto msg = make_message(1000, 1);
size_t filler = this->fill_pipe_();
ASSERT_TRUE(enqueue(buf, msg));
const auto storage = buf.storage();
EXPECT_GE(storage.capacity, msg.size() + TestOverflowBuffer::LEN_PREFIX);
for (int stall = 0; stall < 5; stall++) {
expect_after_filler(this->drain_all_(buf), filler, msg);
EXPECT_TRUE(buf.empty());
// Same allocation every time: no free, no new allocation
EXPECT_EQ(buf.storage(), storage);
filler = this->fill_pipe_();
ASSERT_TRUE(enqueue(buf, msg));
EXPECT_EQ(buf.storage(), storage);
}
}
TEST_F(OverflowBufferTest, ReleaseWhileQueuedFreesOnceDrained) {
TestOverflowBuffer buf;
auto msg = make_message(1000, 7);
size_t filler = this->fill_pipe_();
ASSERT_TRUE(enqueue(buf, msg));
const size_t capacity = buf.capacity();
// Requested while the backlog still holds data: storage must stay until sent
buf.release();
EXPECT_FALSE(buf.empty());
EXPECT_EQ(buf.capacity(), capacity);
expect_after_filler(this->drain_all_(buf), filler, msg);
EXPECT_TRUE(buf.empty());
EXPECT_EQ(buf.capacity(), 0u);
EXPECT_EQ(buf.storage().data, nullptr);
// A later stall allocates again and keeps it, since nobody asked for a release
filler = this->fill_pipe_();
ASSERT_TRUE(enqueue(buf, msg));
EXPECT_GT(buf.capacity(), 0u);
this->drain_all_(buf);
EXPECT_GT(buf.capacity(), 0u);
}
TEST_F(OverflowBufferTest, ReleaseWhenEmptyFreesImmediately) {
TestOverflowBuffer buf;
auto msg = make_message(100, 3);
this->fill_pipe_();
ASSERT_TRUE(enqueue(buf, msg));
this->drain_all_(buf);
EXPECT_GT(buf.capacity(), 0u);
buf.release();
EXPECT_EQ(buf.capacity(), 0u);
EXPECT_EQ(buf.storage().data, nullptr);
}
TEST_F(OverflowBufferTest, PreservesOrderAndSkipsSentPrefix) {
TestOverflowBuffer buf;
auto first = make_message(700, 10);
auto second_a = make_message(300, 50);
auto second_b = make_message(400, 90);
auto third = make_message(200, 130);
size_t filler = this->fill_pipe_();
// 100 bytes of the first message were already accepted by the socket
ASSERT_TRUE(enqueue(buf, first, 100));
// Two iovecs with the skip covering all of the first one plus part of the second
struct iovec iov[2] = {{second_a.data(), second_a.size()}, {second_b.data(), second_b.size()}};
const uint16_t second_skip = static_cast<uint16_t>(second_a.size() + 5);
ASSERT_TRUE(buf.enqueue_iov(iov, 2, static_cast<uint16_t>(second_a.size() + second_b.size()), second_skip));
ASSERT_TRUE(enqueue(buf, third));
EXPECT_EQ(buf.count(), 3);
// Nothing can go out while the pipe is full
EXPECT_EQ(this->drain_(buf), -1);
EXPECT_EQ(buf.count(), 3);
std::vector<uint8_t> expected;
append(expected, first, 100);
append(expected, second_b, 5);
append(expected, third);
expect_after_filler(this->drain_all_(buf), filler, expected);
}
TEST_F(OverflowBufferTest, RefusesWhenQueueIsFull) {
TestOverflowBuffer buf;
auto msg = make_message(16, 1);
size_t filler = this->fill_pipe_();
for (int i = 0; i < API_MAX_SEND_QUEUE; i++) {
ASSERT_TRUE(enqueue(buf, msg)) << "message " << i;
}
EXPECT_FALSE(enqueue(buf, msg));
EXPECT_EQ(buf.count(), API_MAX_SEND_QUEUE);
// Draining frees the slots again
std::vector<uint8_t> expected;
for (int i = 0; i < API_MAX_SEND_QUEUE; i++)
append(expected, msg);
expect_after_filler(this->drain_all_(buf), filler, expected);
this->fill_pipe_();
EXPECT_TRUE(enqueue(buf, msg));
EXPECT_EQ(buf.count(), 1);
}
TEST_F(OverflowBufferTest, SkipAtIovecBoundary) {
TestOverflowBuffer buf;
auto sent = make_message(300, 50);
auto unsent = make_message(400, 90);
size_t filler = this->fill_pipe_();
// The skip covers the first iovec exactly, so only the second is copied
struct iovec iov[2] = {{sent.data(), sent.size()}, {unsent.data(), unsent.size()}};
ASSERT_TRUE(
buf.enqueue_iov(iov, 2, static_cast<uint16_t>(sent.size() + unsent.size()), static_cast<uint16_t>(sent.size())));
EXPECT_EQ(buf.live(), unsent.size() + TestOverflowBuffer::LEN_PREFIX);
expect_after_filler(this->drain_all_(buf), filler, unsent);
}
TEST_F(OverflowBufferTest, AppendsBehindSentPrefixWhenItFits) {
TestOverflowBuffer buf;
size_t filler = this->fill_pipe_();
auto first = make_message(200, 20);
// Size the second message so the two land half way into a 256 byte step,
// leaving exactly 128 bytes of slack whatever the pipe accepted
const size_t base = std::max<size_t>(filler + 1, std::min<size_t>(filler * 3, 12000));
const size_t second_len = (base / 256 + 1) * 256 + 128 - first.size() - 2 * TestOverflowBuffer::LEN_PREFIX;
auto second = make_message(second_len, 60);
ASSERT_GT(second.size(), filler);
ASSERT_TRUE(enqueue(buf, first));
ASSERT_TRUE(enqueue(buf, second));
const auto storage = buf.storage();
const size_t slack = storage.capacity - first.size() - second.size() - 2 * TestOverflowBuffer::LEN_PREFIX;
ASSERT_EQ(slack, 128u);
auto third = make_message(slack - TestOverflowBuffer::LEN_PREFIX, 200);
std::vector<uint8_t> received;
this->read_into_(received);
ASSERT_GT(this->drain_(buf), 0);
ASSERT_EQ(buf.count(), 1);
const size_t live = buf.live();
// Fits in the tail, so the sent prefix is left alone
ASSERT_TRUE(enqueue(buf, third));
EXPECT_EQ(buf.storage(), storage);
EXPECT_EQ(buf.live(), live + third.size() + TestOverflowBuffer::LEN_PREFIX);
append(received, this->drain_all_(buf));
expect_after_filler(received, filler, concat({first, second, third}));
}
TEST_F(OverflowBufferTest, ReleaseSurvivesFurtherEnqueues) {
TestOverflowBuffer buf;
auto first = make_message(300, 7);
auto second = make_message(300, 70);
size_t filler = this->fill_pipe_();
ASSERT_TRUE(enqueue(buf, first));
buf.release();
ASSERT_TRUE(enqueue(buf, second));
EXPECT_GT(buf.capacity(), 0u);
expect_after_filler(this->drain_all_(buf), filler, concat({first, second}));
EXPECT_EQ(buf.capacity(), 0u);
}
TEST_F(OverflowBufferTest, RefusesWhenByteLimitIsExceeded) {
TestOverflowBuffer buf;
// Two of these fill the byte budget exactly, well before the slot count is reached
static_assert(API_MAX_SEND_QUEUE >= 3);
auto msg = make_message(TestOverflowBuffer::MAX_BYTES / 2 - TestOverflowBuffer::LEN_PREFIX, 1);
this->fill_pipe_();
ASSERT_TRUE(enqueue(buf, msg));
ASSERT_TRUE(enqueue(buf, msg));
EXPECT_FALSE(enqueue(buf, msg));
EXPECT_EQ(buf.count(), 2);
}
TEST_F(OverflowBufferTest, LoneMessageMayExceedByteLimit) {
TestOverflowBuffer buf;
// The oversized message must still fit under the lone message ceiling
static_assert(TestOverflowBuffer::MAX_BYTES + 100 + TestOverflowBuffer::LEN_PREFIX <=
TestOverflowBuffer::MAX_LONE_BYTES);
auto big = make_message(TestOverflowBuffer::MAX_BYTES + 100, 5);
auto small = make_message(16, 9);
// Refusing the only message would drop the connection for nothing
size_t filler = this->fill_pipe_();
ASSERT_TRUE(enqueue(buf, big));
EXPECT_EQ(buf.count(), 1);
// With a backlog present the byte limit applies again
EXPECT_FALSE(enqueue(buf, small));
EXPECT_EQ(buf.count(), 1);
expect_after_filler(this->drain_all_(buf), filler, big);
}
TEST_F(OverflowBufferTest, LoneMessageAboveOffsetLimitIsRefused) {
TestOverflowBuffer buf;
// Payload plus prefix is past the lone message ceiling
auto msg = make_message(TestOverflowBuffer::MAX_LONE_BYTES, 3);
this->fill_pipe_();
EXPECT_FALSE(enqueue(buf, msg));
EXPECT_TRUE(buf.empty());
EXPECT_EQ(buf.capacity(), 0u);
}
TEST_F(OverflowBufferTest, HardSocketErrorLeavesBacklogIntact) {
TestOverflowBuffer buf;
auto msg = make_message(300, 40);
this->fill_pipe_();
ASSERT_TRUE(enqueue(buf, msg));
// A closed socket fails every write outright, unlike a full one
ASSERT_EQ(this->sock_->close(), 0);
errno = 0;
EXPECT_EQ(buf.try_drain(this->sock_.get()), -1);
EXPECT_NE(errno, EWOULDBLOCK);
EXPECT_NE(errno, EAGAIN);
EXPECT_EQ(buf.count(), 1);
EXPECT_EQ(buf.live(), msg.size() + TestOverflowBuffer::LEN_PREFIX);
}
TEST_F(OverflowBufferTest, GrowsWhileReclaimingSentPrefix) {
TestOverflowBuffer buf;
Stall s;
ASSERT_NO_FATAL_FAILURE(this->stall_mid_message_(buf, s));
// One byte too many to fit even after the sent prefix is reclaimed: grows in one copy
auto third = make_message(s.before.capacity - buf.live() + 1, 200);
ASSERT_TRUE(enqueue(buf, third));
EXPECT_GT(buf.capacity(), s.before.capacity);
EXPECT_EQ(buf.count(), 2);
append(s.received, this->drain_all_(buf));
expect_after_filler(s.received, s.filler, concat({s.first, s.second, third}));
}
TEST_F(OverflowBufferTest, NestedDrainMakesNoProgress) {
TestOverflowBuffer buf;
auto msg = make_message(300, 40);
size_t filler = this->fill_pipe_();
ASSERT_TRUE(enqueue(buf, msg));
std::vector<uint8_t> received;
this->read_into_(received);
// Room is available, but a nested drain must leave the outer one's message alone
buf.set_draining(true);
EXPECT_EQ(this->drain_(buf), 0);
EXPECT_EQ(buf.count(), 1);
std::vector<uint8_t> nothing;
this->read_into_(nothing);
EXPECT_TRUE(nothing.empty());
buf.set_draining(false);
append(received, this->drain_all_(buf));
expect_after_filler(received, filler, msg);
}
TEST_F(OverflowBufferTest, NestedEnqueueAppendsWithinCapacity) {
TestOverflowBuffer buf;
auto first = make_message(500, 10);
auto second = make_message(4, 90);
size_t filler = this->fill_pipe_();
ASSERT_TRUE(enqueue(buf, first));
const auto storage = buf.storage();
ASSERT_GE(storage.capacity, first.size() + second.size() + 2 * TestOverflowBuffer::LEN_PREFIX);
buf.set_draining(true);
EXPECT_TRUE(enqueue(buf, second));
EXPECT_EQ(buf.count(), 2);
EXPECT_EQ(buf.storage(), storage);
buf.set_draining(false);
expect_after_filler(this->drain_all_(buf), filler, concat({first, second}));
}
TEST_F(OverflowBufferTest, NestedEnqueueRefusesToGrow) {
TestOverflowBuffer buf;
auto first = make_message(500, 10);
auto second = make_message(100, 90);
size_t filler = this->fill_pipe_();
ASSERT_TRUE(enqueue(buf, first));
const auto storage = buf.storage();
ASSERT_LT(storage.capacity, first.size() + second.size() + 2 * TestOverflowBuffer::LEN_PREFIX);
// Growing would free the bytes the outer write() is sending from
buf.set_draining(true);
EXPECT_FALSE(enqueue(buf, second));
EXPECT_EQ(buf.count(), 1);
EXPECT_EQ(buf.storage(), storage);
buf.set_draining(false);
expect_after_filler(this->drain_all_(buf), filler, first);
}
TEST_F(OverflowBufferTest, NestedEnqueueRefusesToCompact) {
TestOverflowBuffer buf;
Stall s;
ASSERT_NO_FATAL_FAILURE(this->stall_mid_message_(buf, s));
auto third = make_message(1000, 200);
// Sliding the remainder down would move the bytes the outer write() points at
buf.set_draining(true);
EXPECT_FALSE(enqueue(buf, third));
EXPECT_EQ(buf.count(), 1);
EXPECT_EQ(buf.storage(), s.before);
buf.set_draining(false);
// Once the drain is over the same enqueue compacts and succeeds
ASSERT_TRUE(enqueue(buf, third));
EXPECT_EQ(buf.storage(), s.before);
append(s.received, this->drain_all_(buf));
expect_after_filler(s.received, s.filler, concat({s.first, s.second, third}));
}
TEST_F(OverflowBufferTest, CompactsInsteadOfGrowingAfterPartialDrain) {
TestOverflowBuffer buf;
Stall s;
ASSERT_NO_FATAL_FAILURE(this->stall_mid_message_(buf, s));
auto third = make_message(1000, 200);
// The sent first message is reclaimed by sliding the remainder down, not by reallocating
ASSERT_TRUE(enqueue(buf, third));
EXPECT_EQ(buf.storage(), s.before);
append(s.received, this->drain_all_(buf));
expect_after_filler(s.received, s.filler, concat({s.first, s.second, third}));
}
} // namespace esphome::api::testing
#endif // USE_HOST
@@ -54,7 +54,7 @@ static void verify_mac(uint64_t mac, size_t expected_bytes) {
size_t ref_len = reference_encode(mac, ref_buf);
APIBuffer api_buf;
api_buf.resize(16);
ASSERT_TRUE(api_buf.resize(16));
uint8_t *pos = api_buf.data();
#ifdef ESPHOME_DEBUG_API
uint8_t *proto_debug_end_ = api_buf.data() + api_buf.size();
@@ -0,0 +1,7 @@
sensor:
- platform: atc_mithermometer
mac_address: A4:C1:38:4E:16:78
temperature:
name: ATC Temperature
humidity:
name: ATC Humidity
@@ -1,7 +1,10 @@
esp32_ble_tracker:
id: ble_tracker_hub
sensor:
# Explicit ble_hub_id: pins the neutral binding as a declared key.
- platform: atc_mithermometer
ble_hub_id: ble_tracker_hub
mac_address: A4:C1:38:4E:16:78
temperature:
name: ATC Temperature
@@ -0,0 +1,3 @@
packages:
ln882h_ble_tracker: !include ../ln882h_ble_tracker/common.yaml
atc_mithermometer: !include common-ln.yaml
@@ -0,0 +1,10 @@
# The esp32_ble_id -> ble_hub_id alias (removal 2027.2.0) still validates.
esp32_ble_tracker:
id: ble_tracker_hub
sensor:
- platform: atc_mithermometer
esp32_ble_id: ble_tracker_hub
mac_address: A4:C1:38:4E:16:78
temperature:
name: ATC Legacy Key Temperature
@@ -0,0 +1,17 @@
# Config-only: the CI base board (generic-bk7252, BLE 4.2) cannot compile the
# BLE 5.x tracker, so this fixture proves validation (schema + neutral binding)
# on a non-esp32 platform; codegen and compilation are not exercised here.
bk72xx_ble_tracker:
id: ble_hub
sensor:
- platform: atc_mithermometer
ble_hub_id: ble_hub
mac_address: A4:C1:38:4E:16:78
temperature:
name: BK ATC Temperature
# No ble_hub_id: exercises the generated binding real configs use.
- platform: atc_mithermometer
mac_address: A4:C1:38:4E:16:79
temperature:
name: BK ATC Implicit Temperature
+5
View File
@@ -0,0 +1,5 @@
from tests.testing_helpers import ComponentManifestOverride
def override_manifest(manifest: ComponentManifestOverride) -> None:
manifest.dependencies = manifest.dependencies + ["sensor", "spi"]
@@ -0,0 +1,62 @@
#include <gtest/gtest.h>
#include "esphome/components/atm90e32/atm90e32.h"
namespace esphome::atm90e32::testing {
TEST(ATM90E32OffsetRegisterVerification, AcceptsExactSignedReadback) {
EXPECT_TRUE(offset_register_value_matches(0x007B, 123));
EXPECT_TRUE(offset_register_value_matches(0xFF85, -123));
}
TEST(ATM90E32OffsetRegisterVerification, RejectsMismatchedReadback) {
EXPECT_FALSE(offset_register_value_matches(0x007C, 123));
EXPECT_FALSE(offset_register_value_matches(0xFF84, -123));
}
TEST(ATM90E32OffsetRestoreState, ReportsVerifiedStoredValuesAsRestored) {
const auto state = resolve_offset_restore_state(true, true, false);
EXPECT_TRUE(state.restored);
EXPECT_TRUE(state.values_verified);
}
TEST(ATM90E32OffsetRestoreState, ReportsVerifiedConfigFallbackAsNotRestored) {
const auto state = resolve_offset_restore_state(true, false, true);
EXPECT_FALSE(state.restored);
EXPECT_TRUE(state.values_verified);
}
TEST(ATM90E32OffsetRestoreState, ReportsFailedConfigFallbackAsUnverified) {
const auto state = resolve_offset_restore_state(true, false, false);
EXPECT_FALSE(state.restored);
EXPECT_FALSE(state.values_verified);
}
TEST(ATM90E32OffsetRestoreState, ReportsConfigWithoutStoredValuesAsNotRestored) {
const auto state = resolve_offset_restore_state(false, true, false);
EXPECT_FALSE(state.restored);
EXPECT_TRUE(state.values_verified);
}
TEST(ATM90E32OffsetPersistence, RollsBackStoredValuesOrZeroSentinel) {
const OffsetCalibration previous[3]{{1, -1}, {2, -2}, {3, -3}};
OffsetCalibration rollback[3]{};
prepare_offset_rollback(previous, true, rollback);
for (uint8_t phase = 0; phase < 3; phase++) {
EXPECT_EQ(rollback[phase].first_offset, previous[phase].first_offset);
EXPECT_EQ(rollback[phase].second_offset, previous[phase].second_offset);
}
prepare_offset_rollback(previous, false, rollback);
for (const auto &phase : rollback) {
EXPECT_EQ(phase.first_offset, 0);
EXPECT_EQ(phase.second_offset, 0);
}
}
} // namespace esphome::atm90e32::testing
+1
View File
@@ -4,4 +4,5 @@ media_source:
- platform: audio_http
id: audio_http_source
buffer_size: 100000
persistent_ring_buffer: true
task_stack_in_psram: true
@@ -0,0 +1,7 @@
sensor:
- platform: b_parasite
mac_address: F0:CA:F0:CA:01:01
humidity:
name: b-parasite Air Humidity
temperature:
name: b-parasite Air Temperature
+3
View File
@@ -1,7 +1,10 @@
esp32_ble_tracker:
id: ble_tracker_hub
sensor:
# Explicit ble_hub_id: pins the neutral binding as a declared key.
- platform: b_parasite
ble_hub_id: ble_tracker_hub
mac_address: F0:CA:F0:CA:01:01
humidity:
name: b-parasite Air Humidity
@@ -0,0 +1,3 @@
packages:
ln882h_ble_tracker: !include ../ln882h_ble_tracker/common.yaml
b_parasite: !include common-ln.yaml
@@ -0,0 +1,26 @@
# Config-only: the CI base board (generic-bk7252, BLE 4.2) cannot compile the
# BLE 5.x tracker, so this fixture proves validation (schema + neutral binding)
# on a non-esp32 platform; codegen and compilation are not exercised here.
bk72xx_ble_tracker:
id: ble_tracker_hub
sensor:
# Explicit ble_hub_id: pins the neutral binding as a declared key.
- platform: b_parasite
ble_hub_id: ble_tracker_hub
mac_address: F0:CA:F0:CA:01:01
humidity:
name: b-parasite Air Humidity
temperature:
name: b-parasite Air Temperature
moisture:
name: b-parasite Soil Moisture
battery_voltage:
name: b-parasite Battery Voltage
illuminance:
name: b-parasite Illuminance
# No ble_hub_id: exercises the generated binding real configs use.
- platform: b_parasite
mac_address: F0:CA:F0:CA:01:02
temperature:
name: BK b-parasite Implicit Temperature
@@ -1,6 +1,6 @@
light:
- platform: beken_spi_led_strip
rgb_order: GRB
channel_colors: GRB
pin: P16
num_leds: 30
chipset: ws2812
@@ -0,0 +1,10 @@
# The deprecated rgb_order / is_rgbw / is_wrgb keys, kept working until 2027.3.0.
# Config-only, and only one strip because P16 is the sole supported pin.
light:
- platform: beken_spi_led_strip
name: Legacy RGBW
pin: P16
num_leds: 30
chipset: sk6812
rgb_order: GRB
is_rgbw: true # -> GRBW
@@ -136,3 +136,19 @@ binary_sensor:
invalid_cooldown: 2s
then:
- logger.log: "Click with custom cooldown"
# Test on_click and on_double_click (compiles match_interval via
# USE_BINARY_SENSOR_CLICK_TRIGGER)
- platform: template
id: click_triggers
name: "Click Triggers"
on_click:
min_length: 50ms
max_length: 350ms
then:
- logger.log: "Clicked"
on_double_click:
min_length: 50ms
max_length: 350ms
then:
- logger.log: "Double clicked"
@@ -0,0 +1,11 @@
bk72xx_ble_tracker:
id: ble_tracker
scan_parameters:
# Boundary coverage: the documented 2.5 ms floor on window (expressible only
# via the microsecond-accurate validation), a non-round interval exercising the
# 0.625 ms unit conversion without collapsing onto the window's unit count,
# and the non-continuous config path.
interval: 5000us
window: 2500us
duration: 5min
continuous: false
@@ -0,0 +1,7 @@
bk72xx_ble_tracker:
id: ble_tracker
scan_parameters:
interval: 100ms
window: 30ms
duration: 5min
continuous: true
@@ -0,0 +1,54 @@
packages:
bk72xx_ble_tracker: !include common.yaml
esphome:
on_boot:
then:
- bk72xx_ble_tracker.start_scan
- bk72xx_ble_tracker.start_scan:
continuous: true
- bk72xx_ble_tracker.stop_scan
- bk72xx_ble_tracker.stop_scan: ble_tracker
bk72xx_ble_tracker:
on_ble_advertise:
- mac_address: AC:37:43:77:5F:4C
then:
- lambda: |-
char addr[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
ESP_LOGD("main", "The device address is %s", x.address_str_to(addr));
- mac_address:
- AC:37:43:77:5F:4C
- AC:37:43:77:5F:4D
then:
- lambda: |-
char addr[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
ESP_LOGD("main", "The device address is %s", x.address_str_to(addr));
on_ble_service_data_advertise:
- service_uuid: ABCD
# mac_address exercises the UUID triggers' set_address() codegen branch.
mac_address: AC:37:43:77:5F:4C
then:
- lambda: |-
ESP_LOGD("main", "Length of service data is %zu", x.size());
- service_uuid: ABCDABCD
then:
- lambda: |-
ESP_LOGD("main", "32-bit service data is %zu", x.size());
- service_uuid: ABCDABCD-ABCD-ABCD-ABCD-ABCDABCDABCD
then:
- lambda: |-
ESP_LOGD("main", "128-bit service data is %zu", x.size());
on_ble_manufacturer_data_advertise:
- manufacturer_id: ABCD
then:
- lambda: |-
ESP_LOGD("main", "Length of manufacturer data is %zu", x.size());
- manufacturer_id: ABCDABCD-ABCD-ABCD-ABCD-ABCDABCDABCD
then:
- lambda: |-
ESP_LOGD("main", "128-bit manufacturer data is %zu", x.size());
on_scan_end:
- then:
- lambda: |-
ESP_LOGD("main", "Scan ended");
@@ -0,0 +1,2 @@
packages:
bk72xx_ble_tracker: !include common-boundary.yaml
@@ -0,0 +1,8 @@
# Passive scanning variant: the package merge keeps the shared parameters from
# common.yaml and overrides only the mode.
packages:
bk72xx_ble_tracker: !include common.yaml
bk72xx_ble_tracker:
scan_parameters:
active: false
@@ -0,0 +1,2 @@
packages:
bk72xx_ble_tracker: !include common.yaml
@@ -6,7 +6,11 @@ def override_manifest(manifest: ComponentManifestOverride) -> None:
# resolve_irk() is compiled only when a sensor configures irk:
# (request_irk_support() emits USE_BLE_DEVICE_IRK). The unit-test build has
# no sensors, so emit the define here to put the real IRK path under test.
# Likewise the scan-response merger (emitted by the split-report trackers)
# and the listener vector it dispatches into (codegen-sized by consumers).
async def to_code_testing(config):
cg.add_define("USE_BLE_DEVICE_IRK")
cg.add_define("USE_BLE_SCAN_RESPONSE_MERGER")
cg.add_define("ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT", 4)
manifest.to_code = to_code_testing
@@ -8,7 +8,7 @@ namespace esphome::ble_device_base::testing {
// from_scan_result() ingests BLE controller order (LSB-first); the public
// accessors must expose the historical esp32 semantics: address() in printable
// (MSB-first) order, address_uint64() with byte 0 in the LSB, address_str()
// (MSB-first) order, address_uint64() with byte 0 in the LSB, address_str_to()
// printed MSB-first.
namespace {
// Device AA:BB:CC:DD:EE:FF — controller order delivers FF first.
@@ -25,7 +25,43 @@ TEST(BleDeviceAddress, AccessorsMatchEsp32Semantics) {
EXPECT_EQ(device.address_uint64(), 0xAABBCCDDEEFFULL);
char buf[ESPBTDevice::MAC_ADDRESS_PRETTY_BUFFER_SIZE];
EXPECT_STREQ(device.address_str_to(buf), "AA:BB:CC:DD:EE:FF");
// The deprecated wrapper must keep returning the same string until its 2027.2.0 removal.
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
EXPECT_EQ(device.address_str(), "AA:BB:CC:DD:EE:FF");
#pragma GCC diagnostic pop
}
// mac_lsb_first_to_uint64() packs the controller-order bytes a raw-advertisement
// callback delivers into the printable-order uint64 the native API speaks — the
// value esp32_ble::ble_addr_to_uint64() has always produced for that address.
TEST(BleDeviceAddress, MacLsbFirstToUint64MatchesWireValue) {
EXPECT_EQ(mac_lsb_first_to_uint64(MAC_LSB_FIRST), 0xAABBCCDDEEFFULL);
}
// The helper and the parsed-device accessor are two routes to the same wire
// value: byte order must agree no matter which path an advertisement takes.
TEST(BleDeviceAddress, MacLsbFirstToUint64AgreesWithParsedDevice) {
ESPBTDevice device;
device.from_scan_result(MAC_LSB_FIRST, -50, BLE_ADDR_TYPE_PUBLIC, nullptr, 0);
EXPECT_EQ(mac_lsb_first_to_uint64(MAC_LSB_FIRST), device.address_uint64());
}
// uint64_to_mac_msb_first() is the inverse: unpacking the wire value yields
// printable (MSB-first) order, and round-tripping through the LSB-first
// packer restores the original value.
TEST(BleDeviceAddress, Uint64ToMacMsbFirstRoundTrip) {
uint8_t msb_first[6];
uint64_to_mac_msb_first(0xAABBCCDDEEFFULL, msb_first);
EXPECT_EQ(msb_first[0], 0xaa);
EXPECT_EQ(msb_first[5], 0xff);
uint8_t lsb_first[6];
for (int i = 0; i < 6; i++)
lsb_first[i] = msb_first[5 - i];
EXPECT_EQ(mac_lsb_first_to_uint64(lsb_first), 0xAABBCCDDEEFFULL);
}
} // namespace esphome::ble_device_base::testing
@@ -0,0 +1,91 @@
#include "esphome/components/ble_device_base/ble_device.h"
#include <gtest/gtest.h>
#include <cstring>
#include <string>
#include <vector>
namespace esphome::ble_device_base {
namespace {
// AD types under test
constexpr uint8_t AD_SHORT_NAME = 0x08;
constexpr uint8_t AD_COMPLETE_NAME = 0x09;
void append_name(std::vector<uint8_t> &adv, uint8_t ad_type, const char *name) {
size_t len = strlen(name);
adv.push_back(static_cast<uint8_t>(len + 1));
adv.push_back(ad_type);
adv.insert(adv.end(), name, name + len);
}
ESPBTDevice device_from(const std::vector<uint8_t> &adv) {
const uint8_t mac[6] = {0x11, 0x22, 0x33, 0x44, 0x55, 0x66};
ESPBTDevice device;
device.from_scan_result(mac, -59, 0, adv.data(), static_cast<uint16_t>(adv.size()));
return device;
}
} // namespace
TEST(BleAdvName, ParsesACompleteName) {
std::vector<uint8_t> adv;
append_name(adv, AD_COMPLETE_NAME, "TP96");
ESPBTDevice device = device_from(adv);
EXPECT_EQ(device.get_name(), "TP96");
// The backing buffer is NUL-terminated so c_str() is usable directly.
EXPECT_STREQ(device.get_name().c_str(), "TP96");
}
TEST(BleAdvName, LongestNameWinsShortenedThenComplete) {
// A merged adv + scan-response frame can carry both forms; the shortened
// one must never replace the complete one.
std::vector<uint8_t> adv;
append_name(adv, AD_SHORT_NAME, "Radon");
append_name(adv, AD_COMPLETE_NAME, "RadonEye");
EXPECT_EQ(device_from(adv).get_name(), "RadonEye");
}
TEST(BleAdvName, LongestNameWinsCompleteThenShortened) {
std::vector<uint8_t> adv;
append_name(adv, AD_COMPLETE_NAME, "RadonEye");
append_name(adv, AD_SHORT_NAME, "Radon");
EXPECT_EQ(device_from(adv).get_name(), "RadonEye");
}
TEST(BleAdvName, MaxLengthNameFitsAndTerminates) {
// 29 bytes is the largest name a legacy AD element can carry and exactly
// fills the fixed buffer.
std::string max_name(29, 'a');
std::vector<uint8_t> adv;
append_name(adv, AD_COMPLETE_NAME, max_name.c_str());
ESPBTDevice device = device_from(adv);
EXPECT_EQ(device.get_name().size(), 29u);
EXPECT_EQ(device.get_name(), max_name);
EXPECT_STREQ(device.get_name().c_str(), max_name.c_str());
}
TEST(BleAdvName, ReparseResetsThePreviousName) {
const uint8_t mac[6] = {0x11, 0x22, 0x33, 0x44, 0x55, 0x66};
std::vector<uint8_t> first;
append_name(first, AD_COMPLETE_NAME, "RadonEye");
std::vector<uint8_t> second;
append_name(second, AD_COMPLETE_NAME, "TP96");
ESPBTDevice device;
device.from_scan_result(mac, -59, 0, first.data(), static_cast<uint16_t>(first.size()));
ASSERT_EQ(device.get_name(), "RadonEye");
// A shorter name from a fresh report must fully replace the longer one:
// the longest-name rule applies within one report, not across reports.
device.from_scan_result(mac, -59, 0, second.data(), static_cast<uint16_t>(second.size()));
EXPECT_EQ(device.get_name(), "TP96");
EXPECT_STREQ(device.get_name().c_str(), "TP96");
}
TEST(BleAdvName, NoNamePresentIsEmpty) {
std::vector<uint8_t> adv = {0x02, 0x0A, 0x00}; // TX power only
EXPECT_TRUE(device_from(adv).get_name().empty());
}
} // namespace esphome::ble_device_base
@@ -19,6 +19,33 @@ const uint8_t TAG[4] = {0x48, 0x4d, 0xaa, 0x56};
const uint8_t PLAINTEXT[7] = {0x02, 0x01, 0x64, 0x03, 0x10, 0x8a, 0x01};
} // namespace
// Xiaomi's parameters differ from BTHome's: a 12-byte nonce and a 1-byte AAD
// (0x11). Both the AAD block and the l = 3 length encoding are only reachable
// through this shape, so they need their own vector. Generated the same way.
namespace {
const uint8_t NONCE_XIAOMI[12] = {0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b};
const uint8_t AAD_XIAOMI[1] = {0x11};
const uint8_t CIPHERTEXT_XIAOMI[5] = {0xc3, 0x7e, 0x0a, 0x1d, 0x23};
const uint8_t TAG_XIAOMI[4] = {0x98, 0x79, 0x87, 0xc6};
const uint8_t PLAINTEXT_XIAOMI[5] = {0x04, 0x10, 0x02, 0xd4, 0x00};
} // namespace
TEST(BleAesCcm, DecryptsXiaomiShapedVector) {
uint8_t out[sizeof(PLAINTEXT_XIAOMI)] = {};
EXPECT_TRUE(aes_ccm_auth_decrypt(KEY, NONCE_XIAOMI, sizeof(NONCE_XIAOMI), AAD_XIAOMI, sizeof(AAD_XIAOMI),
CIPHERTEXT_XIAOMI, sizeof(CIPHERTEXT_XIAOMI), out, TAG_XIAOMI, sizeof(TAG_XIAOMI)));
EXPECT_EQ(0, memcmp(out, PLAINTEXT_XIAOMI, sizeof(PLAINTEXT_XIAOMI)));
}
TEST(BleAesCcm, RejectsWrongAssociatedData) {
uint8_t bad_aad[sizeof(AAD_XIAOMI)];
memcpy(bad_aad, AAD_XIAOMI, sizeof(AAD_XIAOMI));
bad_aad[0] ^= 0x01;
uint8_t out[sizeof(PLAINTEXT_XIAOMI)] = {};
EXPECT_FALSE(aes_ccm_auth_decrypt(KEY, NONCE_XIAOMI, sizeof(NONCE_XIAOMI), bad_aad, sizeof(bad_aad),
CIPHERTEXT_XIAOMI, sizeof(CIPHERTEXT_XIAOMI), out, TAG_XIAOMI, sizeof(TAG_XIAOMI)));
}
TEST(BleAesCcm, DecryptsAndAuthenticatesKnownVector) {
uint8_t out[sizeof(PLAINTEXT)] = {};
EXPECT_TRUE(aes_ccm_auth_decrypt(KEY, NONCE, sizeof(NONCE), nullptr, 0, CIPHERTEXT, sizeof(CIPHERTEXT), out, TAG,
@@ -27,6 +27,60 @@ TEST(BleDeviceUuid, ThirtyTwoBitMatchesEquivalentLongForm) {
EXPECT_TRUE(u32 == u128);
}
// A default-constructed UUID is UNSET, the historical "not configured" sentinel
// (len 0 through the esp32 get_uuid() adapter).
TEST(BleDeviceUuid, DefaultConstructedIsUnset) {
const ESPBTUUID unset;
EXPECT_EQ(unset.type(), ESPBTUUID::Type::UNSET);
EXPECT_TRUE(unset == ESPBTUUID());
EXPECT_FALSE(unset == ESPBTUUID::from_uint16(0x1234));
EXPECT_FALSE(unset.contains(0x00, 0x00));
}
// Every factory yields a non-UNSET UUID, even for 0x0000: only default construction and a
// failed text parse are unset, keeping type() != UNSET equivalent to the old len > 0 check.
TEST(BleDeviceUuid, AllFactoriesProduceSetUuids) {
const uint8_t raw[16] = {0xFB, 0x34, 0x9B, 0x5F, 0x80, 0x00, 0x00, 0x80,
0x00, 0x10, 0x00, 0x00, 0x34, 0x12, 0x00, 0x00};
EXPECT_NE(ESPBTUUID::from_uint16(0x0000).type(), ESPBTUUID::Type::UNSET);
EXPECT_NE(ESPBTUUID::from_uint32(0).type(), ESPBTUUID::Type::UNSET);
EXPECT_NE(ESPBTUUID::from_raw(raw).type(), ESPBTUUID::Type::UNSET);
EXPECT_NE(ESPBTUUID::from_raw_reversed(raw).type(), ESPBTUUID::Type::UNSET);
EXPECT_NE(ESPBTUUID::from_raw("180F", 4).type(), ESPBTUUID::Type::UNSET);
EXPECT_NE(ESPBTUUID::from_raw("0000180F", 8).type(), ESPBTUUID::Type::UNSET);
EXPECT_NE(ESPBTUUID::from_raw(reinterpret_cast<const char *>(raw), 16).type(), ESPBTUUID::Type::UNSET);
EXPECT_NE(ESPBTUUID::from_raw("6E400001-B5A3-F393-E0A9-E50E24DCCA9E").type(), ESPBTUUID::Type::UNSET);
}
// 0x0000 is a valid short UUID on real devices (esphome/aioesphomeapi#1742); an unset
// UUID must never compare equal to it. Unset equals only unset.
TEST(BleDeviceUuid, UnsetIsNotEqualToZeroUuid) {
EXPECT_FALSE(ESPBTUUID() == ESPBTUUID::from_uint16(0x0000));
EXPECT_FALSE(ESPBTUUID::from_uint16(0x0000) == ESPBTUUID());
const uint8_t base[16] = {0xFB, 0x34, 0x9B, 0x5F, 0x80, 0x00, 0x00, 0x80,
0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
EXPECT_FALSE(ESPBTUUID() == ESPBTUUID::from_raw(base));
EXPECT_TRUE(ESPBTUUID() == ESPBTUUID());
EXPECT_FALSE(ESPBTUUID().as_128bit().is_set()); // widening preserves the unset state
// A configured 0x0000 still matches its own 128-bit base UUID expansion.
EXPECT_TRUE(ESPBTUUID::from_uint16(0x0000) == ESPBTUUID::from_raw(base));
}
// is_set() is the sentinel check; an unset UUID prints as "None" instead of a
// valid-looking all-zero 128-bit UUID.
TEST(BleDeviceUuid, IsSetAndUnsetToStr) {
char buf[UUID_STR_LEN];
EXPECT_FALSE(ESPBTUUID().is_set());
EXPECT_STREQ(ESPBTUUID().to_str(buf), "None");
EXPECT_TRUE(ESPBTUUID::from_uint16(0x0000).is_set());
EXPECT_STREQ(ESPBTUUID::from_uint16(0x0000).to_str(buf), "0x0000");
}
// Text parsing of an invalid length historically produced a len-0 (unset) UUID.
TEST(BleDeviceUuid, InvalidTextFormParsesToUnset) {
EXPECT_EQ(ESPBTUUID::from_raw("nope", 3).type(), ESPBTUUID::Type::UNSET);
}
TEST(BleDeviceUuid, DifferentUuidsDoNotMatch) {
EXPECT_FALSE(ESPBTUUID::from_uint16(0x1234) == ESPBTUUID::from_uint16(0x1235));
const uint8_t raw128[16] = {0xFB, 0x34, 0x9B, 0x5F, 0x80, 0x00, 0x00, 0x80,
@@ -0,0 +1,82 @@
// The GATT client contract compiles in no real build until a hub backend is
// configured; this TU pins it on the host so the header cannot rot unseen.
// The contract is a concept (BLEGattConnection is a per-platform alias), so
// the minimal backend here proves the concept stays satisfiable and routes
// events through the GattClientListener interface the way a real backend does.
#define USE_BLE_GATT_CLIENT
#include "esphome/components/ble_device_base/ble_gatt_client.h"
#include <gtest/gtest.h>
namespace esphome::ble_device_base::testing {
// Overrides only what it records; the interface's defaults cover the rest.
class RecordingListener : public GattClientListener {
public:
void on_connection_state(bool connected, uint16_t mtu, int error) override { this->connected_ = connected; }
void on_service_discovery_done(int error) override { this->discovery_error_ = error; }
void on_write_result(uint16_t handle, int error) override { this->write_handle_ = handle; }
bool connected_{false};
int discovery_error_{0};
uint16_t write_handle_{0};
};
class MinimalConnection {
public:
void set_listener(GattClientListener *listener) { this->listener_ = listener; }
int connect(uint64_t address, uint8_t addr_type) {
this->listener_->on_connection_state(true, 517, 0);
return 0;
}
bool cancel_gatt_disconnect() { return false; }
int gatt_disconnect() { return 0; }
int discover_services() {
this->listener_->on_service_discovery_done(0);
return 0;
}
int read_characteristic(uint16_t handle) { return GATT_ERR_NOT_CONNECTED; }
int write_characteristic(uint16_t handle, const uint8_t *data, uint16_t len, bool response) {
this->listener_->on_write_result(handle, 0);
return 0;
}
int read_descriptor(uint16_t handle) { return 0; }
int write_descriptor(uint16_t handle, const uint8_t *data, uint16_t len) { return 0; }
int notify_characteristic(uint16_t handle, bool enable) { return 0; }
int pair() { return GATT_ERR_NOT_CONNECTED; }
int update_connection_params(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout) {
return 0;
}
GattServiceTable get_service_table() { return {}; }
void release_services() {}
void set_connection_type(ConnectionType ct) {}
protected:
GattClientListener *listener_{nullptr};
};
static_assert(BLEGattConnectionContract<MinimalConnection>,
"a minimal backend must satisfy the contract the alias asserts");
TEST(BleGattClientContract, MinimalImplementerCompilesAndRoutesEvents) {
MinimalConnection connection;
RecordingListener listener;
connection.set_listener(&listener);
EXPECT_EQ(connection.connect(0xAABBCCDDEEFFULL, 0), 0);
EXPECT_TRUE(listener.connected_);
EXPECT_EQ(connection.discover_services(), 0);
EXPECT_EQ(listener.discovery_error_, 0);
EXPECT_EQ(connection.read_characteristic(1), GATT_ERR_NOT_CONNECTED);
EXPECT_EQ(connection.write_characteristic(7, nullptr, 0, true), 0);
EXPECT_EQ(listener.write_handle_, 7);
// A default table is empty and safe to walk.
GattServiceTable table = connection.get_service_table();
EXPECT_EQ(table.service_count, 0);
EXPECT_EQ(table.characteristic_count, 0);
EXPECT_EQ(table.descriptor_count, 0);
}
} // namespace esphome::ble_device_base::testing
@@ -0,0 +1,158 @@
#include <gtest/gtest.h>
#include <cstdint>
#include <vector>
#include "esphome/components/ble_device_base/ble_device.h"
namespace esphome::ble_device_base::testing {
// from_manufacturer_data() accepts exactly the iBeacon frame: Apple company ID,
// 23 payload bytes, and the 0x02/0x15 sub-type/length prefix. The prefix check
// is stricter than the legacy esp32 parser (which surfaced any 23-byte Apple
// payload as a beacon) — a declared behavior change; these tests pin the
// accept/reject boundary.
namespace {
ServiceData make_apple_payload(uint8_t sub_type, uint8_t length, size_t size = 23) {
ServiceData data;
data.uuid = ESPBTUUID::from_uint16(0x004C); // Apple company ID
data.data.assign(size, 0);
if (size >= 2) {
data.data[0] = sub_type;
data.data[1] = length;
}
// BeaconData layout: sub_type[0], length[1], proximity_uuid[2..17],
// major[18..19], minor[20..21], signal_power[22] — all wire values big-endian.
if (size >= 23) {
data.data[18] = 0x12; // major 0x1234
data.data[19] = 0x34;
data.data[20] = 0x56; // minor 0x5678
data.data[21] = 0x78;
data.data[22] = 0xC5; // signal power -59 dBm
}
return data;
}
} // namespace
TEST(BleIBeacon, AcceptsWellFormedFrame) {
auto beacon = ESPBLEiBeacon::from_manufacturer_data(make_apple_payload(0x02, 0x15));
ASSERT_TRUE(beacon.has_value());
// Explicit guard: clang-tidy's unchecked-optional-access models neither
// gtest's ASSERT_TRUE nor value() as a check.
if (beacon.has_value()) {
// Pins every scalar accessor's offset and the on-wire big-endian order.
EXPECT_EQ(beacon->get_major(), 0x1234);
EXPECT_EQ(beacon->get_minor(), 0x5678);
EXPECT_EQ(beacon->get_signal_power(), -59);
}
}
TEST(BleIBeacon, RejectsWrongSubType) {
// Apple "nearby" and other frames of coincidental length must not parse.
EXPECT_FALSE(ESPBLEiBeacon::from_manufacturer_data(make_apple_payload(0x10, 0x15)).has_value());
}
TEST(BleIBeacon, RejectsWrongLengthByte) {
EXPECT_FALSE(ESPBLEiBeacon::from_manufacturer_data(make_apple_payload(0x02, 0x14)).has_value());
}
TEST(BleIBeacon, RejectsWrongPayloadSize) {
EXPECT_FALSE(ESPBLEiBeacon::from_manufacturer_data(make_apple_payload(0x02, 0x15, 22)).has_value());
EXPECT_FALSE(ESPBLEiBeacon::from_manufacturer_data(make_apple_payload(0x02, 0x15, 24)).has_value());
}
TEST(BleIBeacon, RejectsNonAppleCompany) {
auto data = make_apple_payload(0x02, 0x15);
data.uuid = ESPBTUUID::from_uint16(0x0059); // Nordic
EXPECT_FALSE(ESPBLEiBeacon::from_manufacturer_data(data).has_value());
}
TEST(BleIBeacon, PrefixRejectedFlagsOnlyTheSubTypeCase) {
// The out-param drives the get_ibeacon() diagnostic for frames the legacy
// parser accepted: exactly the 23-byte Apple payload with a wrong prefix.
// Wrong size and non-Apple frames were never accepted and must stay silent.
bool flagged = false;
EXPECT_FALSE(ESPBLEiBeacon::from_manufacturer_data(make_apple_payload(0x10, 0x15), &flagged).has_value());
EXPECT_TRUE(flagged);
flagged = false;
ESPBLEiBeacon::from_manufacturer_data(make_apple_payload(0x02, 0x15), &flagged);
EXPECT_FALSE(flagged);
flagged = false;
ESPBLEiBeacon::from_manufacturer_data(make_apple_payload(0x10, 0x15, 22), &flagged);
EXPECT_FALSE(flagged);
flagged = false;
auto nordic = make_apple_payload(0x10, 0x15);
nordic.uuid = ESPBTUUID::from_uint16(0x0059);
ESPBLEiBeacon::from_manufacturer_data(nordic, &flagged);
EXPECT_FALSE(flagged);
}
namespace {
// One AD manufacturer-data record: [len][0xFF][company LE][payload...].
void append_mfr_record(std::vector<uint8_t> &adv, uint16_t company, const std::vector<uint8_t> &payload) {
adv.push_back(static_cast<uint8_t>(1 + 2 + payload.size()));
adv.push_back(0xFF);
adv.push_back(static_cast<uint8_t>(company & 0xFF));
adv.push_back(static_cast<uint8_t>(company >> 8));
adv.insert(adv.end(), payload.begin(), payload.end());
}
std::vector<uint8_t> beacon_payload(uint8_t sub_type, uint8_t length) {
std::vector<uint8_t> p(23, 0);
p[0] = sub_type;
p[1] = length;
p[18] = 0x12;
p[19] = 0x34;
p[20] = 0x56;
p[21] = 0x78;
p[22] = 0xC5;
return p;
}
ESPBTDevice device_from(const std::vector<uint8_t> &adv) {
const uint8_t mac[6] = {0x11, 0x22, 0x33, 0x44, 0x55, 0x66};
ESPBTDevice device;
device.from_scan_result(mac, -59, 0, adv.data(), static_cast<uint16_t>(adv.size()));
return device;
}
} // namespace
// get_ibeacon() wraps the parser with first-rejection capture and the log
// gate; pin its short circuits so a regression there needs a code change, not
// a review, to surface.
TEST(BleIBeacon, GetIbeaconReturnsBeaconDespitePrecedingRejectedFrame) {
std::vector<uint8_t> adv;
append_mfr_record(adv, 0x004C, beacon_payload(0x10, 0x15)); // rejected prefix
append_mfr_record(adv, 0x004C, beacon_payload(0x02, 0x15)); // real iBeacon
auto device = device_from(adv);
auto beacon = device.get_ibeacon();
ASSERT_TRUE(beacon.has_value());
if (beacon.has_value()) {
EXPECT_EQ(beacon->get_major(), 0x1234);
}
}
TEST(BleIBeacon, GetIbeaconEmptyWhenOnlyRejectedFrames) {
std::vector<uint8_t> adv;
append_mfr_record(adv, 0x004C, beacon_payload(0x10, 0x15));
auto device = device_from(adv);
EXPECT_FALSE(device.get_ibeacon().has_value());
}
TEST(BleIBeacon, GetIbeaconEmptyWithoutManufacturerData) {
std::vector<uint8_t> adv;
adv.push_back(0x02); // flags record only
adv.push_back(0x01);
adv.push_back(0x06);
auto device = device_from(adv);
EXPECT_FALSE(device.get_ibeacon().has_value());
}
} // namespace esphome::ble_device_base::testing
@@ -0,0 +1,96 @@
#include <gtest/gtest.h>
#include <cstdint>
#include "esphome/components/ble_device_base/ble_hub.h"
namespace esphome::ble_device_base::testing {
// Exercises the hub contract around RawAdvertisementCallback, not just the
// struct: a hub stores one slot via set_raw_advertisement_callback(), fires it
// only when set ("no subscriber" is the default-constructed slot), and a new
// registration replaces the old ("one consumer at a time").
//
// The in-tree emit site (BK72xxBLETracker::on_scan_report) compiles against
// the Beken SDK and cannot run host-side, so the guard-and-fire semantics are
// pinned here through a minimal host hub carrying only the slot under test.
namespace {
class FakeHub {
public:
void set_raw_advertisement_callback(RawAdvertisementCallback callback) { this->callback_ = callback; }
/// The emit path every tracker implements: fire only when a subscriber is set.
void emit(const RawAdvertisement &adv) {
if (this->callback_.is_set())
this->callback_.invoke(adv);
}
protected:
RawAdvertisementCallback callback_; // default-constructed: no subscriber
};
struct CapturingSubscriber {
RawAdvertisement last{};
int calls{0};
static void trampoline(void *self, const RawAdvertisement &adv) {
auto *sub = static_cast<CapturingSubscriber *>(self);
sub->last = adv;
sub->calls++;
}
};
// Device AA:BB:CC:DD:EE:FF, packed the way the API speaks it.
constexpr uint64_t TEST_ADDRESS = 0xAABBCCDDEEFFULL;
const uint8_t ADV_DATA[4] = {0x02, 0x01, 0x06, 0x00};
RawAdvertisement make_test_adv() {
return RawAdvertisement{
.address = TEST_ADDRESS, .data = ADV_DATA, .data_len = sizeof(ADV_DATA), .rssi = -63, .addr_type = 1};
}
} // namespace
TEST(RawAdvertisementCallback, DefaultConstructedSlotIsNotSet) {
const RawAdvertisementCallback callback{};
EXPECT_FALSE(callback.is_set());
}
TEST(RawAdvertisementCallback, SubscriberSeesFieldsUnchanged) {
FakeHub hub;
CapturingSubscriber subscriber;
hub.set_raw_advertisement_callback({&subscriber, CapturingSubscriber::trampoline});
hub.emit(make_test_adv());
ASSERT_EQ(subscriber.calls, 1);
EXPECT_EQ(subscriber.last.address, TEST_ADDRESS);
EXPECT_EQ(subscriber.last.data, ADV_DATA);
EXPECT_EQ(subscriber.last.data_len, sizeof(ADV_DATA));
EXPECT_EQ(subscriber.last.rssi, -63);
EXPECT_EQ(subscriber.last.addr_type, 1);
}
TEST(RawAdvertisementCallback, NoSubscriberDoesNotFire) {
FakeHub hub;
// No set_raw_advertisement_callback(): emitting must be a guarded no-op,
// not a jump through a garbage pointer.
hub.emit(make_test_adv());
}
TEST(RawAdvertisementCallback, NewSubscriberReplacesOld) {
FakeHub hub;
CapturingSubscriber first;
CapturingSubscriber second;
hub.set_raw_advertisement_callback({&first, CapturingSubscriber::trampoline});
hub.set_raw_advertisement_callback({&second, CapturingSubscriber::trampoline});
hub.emit(make_test_adv());
EXPECT_EQ(first.calls, 0); // one consumer at a time
ASSERT_EQ(second.calls, 1);
EXPECT_EQ(second.last.rssi, -63);
}
} // namespace esphome::ble_device_base::testing
@@ -0,0 +1,193 @@
// The host test build gets this from the manifest override; clang-tidy does not.
#ifndef USE_BLE_SCAN_RESPONSE_MERGER
#define USE_BLE_SCAN_RESPONSE_MERGER
#endif
#include <gtest/gtest.h>
#include <cstdint>
#include <cstring>
#include <vector>
#include "esphome/components/ble_device_base/scan_response_merger.h"
namespace esphome::ble_device_base::testing {
namespace {
// Pins the merge policy three trackers share (ln882h, rp2, bk72xx): slot
// bookkeeping, the same-device reuse path, the table-full fallback, the
// 62-byte truncation, the advertisement-RSSI choice and the raw_only gate.
// Delivery is observed through a real AdvDispatcher: the raw callback sees
// every frame (including raw_only), a listener only the parsed ones.
struct DeliveredFrame {
uint64_t address;
std::vector<uint8_t> data;
int8_t rssi;
};
struct RawCapture {
std::vector<DeliveredFrame> frames;
static void trampoline(void *self, const RawAdvertisement &adv) {
auto *capture = static_cast<RawCapture *>(self);
capture->frames.push_back({adv.address, std::vector<uint8_t>(adv.data, adv.data + adv.data_len), adv.rssi});
}
};
class CountingListener : public ESPBTDeviceListener {
public:
bool parse_device(const ESPBTDevice &device) override {
this->parsed++;
return true; // claimed: keeps the discovered log quiet
}
int parsed{0};
};
class ScanResponseMergerTest : public ::testing::Test {
protected:
void SetUp() override {
this->dispatcher_.set_raw_advertisement_callback({&this->raw_, &RawCapture::trampoline});
this->dispatcher_.register_listener(&this->listener_);
this->merger_.bind(&this->dispatcher_, &this->scan_continuous_, "test");
}
void stash_(const uint8_t (&mac)[6], int8_t rssi, uint8_t data_len, uint8_t fill, uint32_t now = 0) {
std::vector<uint8_t> data(data_len, fill);
this->merger_.stash_adv(mac, rssi, 0, data.data(), data_len, now);
}
void scan_rsp_(const uint8_t (&mac)[6], int8_t rssi, uint8_t data_len, uint8_t fill) {
std::vector<uint8_t> data(data_len, fill);
this->merger_.submit_scan_rsp(mac, rssi, 0, data.data(), data_len);
}
ScanResponseMerger merger_;
AdvDispatcher dispatcher_;
RawCapture raw_;
CountingListener listener_;
bool scan_continuous_{true};
};
constexpr uint8_t MAC_A[6] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06};
constexpr uint8_t MAC_B[6] = {0x11, 0x12, 0x13, 0x14, 0x15, 0x16};
TEST_F(ScanResponseMergerTest, MatchedPairDeliversOneMergedFrameWithAdvRssi) {
this->stash_(MAC_A, -40, 20, 0xAA);
EXPECT_TRUE(this->raw_.frames.empty()); // held, not delivered
this->scan_rsp_(MAC_A, -70, 10, 0xBB);
ASSERT_EQ(this->raw_.frames.size(), 1u);
const auto &frame = this->raw_.frames[0];
ASSERT_EQ(frame.data.size(), 30u); // adv + response as ONE frame
EXPECT_EQ(frame.data[0], 0xAA);
EXPECT_EQ(frame.data[19], 0xAA);
EXPECT_EQ(frame.data[20], 0xBB);
// The advertisement's RSSI, never the scan response's.
EXPECT_EQ(frame.rssi, -40);
EXPECT_EQ(this->listener_.parsed, 1);
EXPECT_TRUE(this->merger_.empty());
}
TEST_F(ScanResponseMergerTest, ReAdvertisementDeliversHeldFrameAndReusesSlot) {
this->stash_(MAC_A, -40, 20, 0xAA);
this->stash_(MAC_A, -45, 22, 0xCC); // same device again: first frame is delivered
ASSERT_EQ(this->raw_.frames.size(), 1u);
EXPECT_EQ(this->raw_.frames[0].data.size(), 20u);
EXPECT_EQ(this->raw_.frames[0].rssi, -40);
EXPECT_FALSE(this->merger_.empty()); // the second advertisement now holds the slot
this->scan_rsp_(MAC_A, -70, 5, 0xBB);
ASSERT_EQ(this->raw_.frames.size(), 2u);
EXPECT_EQ(this->raw_.frames[1].data.size(), 27u); // 22 + 5, merged from the reused slot
EXPECT_EQ(this->raw_.frames[1].rssi, -45);
}
TEST_F(ScanResponseMergerTest, FullTableDegradesToUnmergedDelivery) {
uint8_t mac[6] = {0x20, 0x00, 0x00, 0x00, 0x00, 0x00};
for (uint8_t i = 0; i < 8; i++) {
mac[5] = i;
this->stash_(mac, -50, 10, i);
}
EXPECT_TRUE(this->raw_.frames.empty()); // 8 slots, all held
mac[5] = 8;
this->stash_(mac, -50, 10, 8); // 9th device: no slot left
ASSERT_EQ(this->raw_.frames.size(), 1u); // delivered immediately, unmerged
EXPECT_EQ(this->raw_.frames[0].data.size(), 10u);
this->merger_.flush(); // the 8 held frames are all still intact
EXPECT_EQ(this->raw_.frames.size(), 9u);
EXPECT_TRUE(this->merger_.empty());
}
TEST_F(ScanResponseMergerTest, MergeTruncatesAtBufferCapacity) {
this->stash_(MAC_A, -40, 31, 0xAA);
this->scan_rsp_(MAC_A, -70, 40, 0xBB); // only 31 bytes of room remain
ASSERT_EQ(this->raw_.frames.size(), 1u);
EXPECT_EQ(this->raw_.frames[0].data.size(), 62u);
EXPECT_EQ(this->raw_.frames[0].data[31], 0xBB);
EXPECT_EQ(this->raw_.frames[0].data[61], 0xBB);
}
TEST_F(ScanResponseMergerTest, UnmatchedScanResponseIsRawOnly) {
this->scan_rsp_(MAC_B, -60, 12, 0xDD);
ASSERT_EQ(this->raw_.frames.size(), 1u); // still forwarded on the raw path
EXPECT_EQ(this->raw_.frames[0].rssi, -60);
EXPECT_EQ(this->listener_.parsed, 0); // but never parsed for listeners
}
TEST_F(ScanResponseMergerTest, AddrTypeIsPartOfTheMatchKey) {
std::vector<uint8_t> adv(20, 0xAA);
this->merger_.stash_adv(MAC_A, -40, /*addr_type=*/0, adv.data(), adv.size(), 0);
std::vector<uint8_t> rsp(10, 0xBB);
this->merger_.submit_scan_rsp(MAC_A, -70, /*addr_type=*/1, rsp.data(), rsp.size());
// Same MAC, different addr_type: no merge — the response goes out raw_only.
ASSERT_EQ(this->raw_.frames.size(), 1u);
EXPECT_EQ(this->raw_.frames[0].data.size(), 10u);
EXPECT_EQ(this->listener_.parsed, 0);
EXPECT_FALSE(this->merger_.empty()); // the advertisement is still held
}
TEST_F(ScanResponseMergerTest, SweepDeliversOnlyPastTheTimeout) {
this->stash_(MAC_A, -40, 20, 0xAA, /*now=*/1000);
this->merger_.sweep(1300); // exactly 300 ms: not yet past the timeout
EXPECT_TRUE(this->raw_.frames.empty());
this->merger_.sweep(1301);
ASSERT_EQ(this->raw_.frames.size(), 1u);
EXPECT_EQ(this->raw_.frames[0].rssi, -40);
EXPECT_EQ(this->listener_.parsed, 1); // timeout delivery is a full parse, not raw_only
EXPECT_TRUE(this->merger_.empty());
}
TEST_F(ScanResponseMergerTest, FlushDeliversEverythingImmediately) {
this->stash_(MAC_A, -40, 20, 0xAA, /*now=*/1000);
this->stash_(MAC_B, -50, 15, 0xBB, /*now=*/1000);
this->merger_.flush();
EXPECT_EQ(this->raw_.frames.size(), 2u);
EXPECT_EQ(this->listener_.parsed, 2);
EXPECT_TRUE(this->merger_.empty());
}
TEST_F(ScanResponseMergerTest, UnboundMergerDropsInsteadOfCrashing) {
ScanResponseMerger unbound;
std::vector<uint8_t> data(20, 0xAA);
unbound.stash_adv(MAC_A, -40, 0, data.data(), data.size(), 0);
unbound.submit_scan_rsp(MAC_A, -70, 0, data.data(), data.size());
unbound.sweep(1000);
unbound.flush(); // no null jump anywhere
EXPECT_TRUE(unbound.empty());
}
TEST_F(ScanResponseMergerTest, PartialBindIsTreatedAsUnbound) {
ScanResponseMerger partial;
partial.bind(&this->dispatcher_, nullptr, "test");
std::vector<uint8_t> data(20, 0xAA);
partial.submit_scan_rsp(MAC_A, -70, 0, data.data(), data.size());
partial.stash_adv(MAC_A, -40, 0, data.data(), data.size(), 0);
partial.flush(); // dropped, not dispatched through half a binding
EXPECT_TRUE(this->raw_.frames.empty());
}
} // namespace
} // namespace esphome::ble_device_base::testing
@@ -0,0 +1,52 @@
#include <gtest/gtest.h>
#include <cstdint>
#include "esphome/components/ble_device_base/ble_hub.h"
namespace esphome::ble_device_base::testing {
// Pins the ScannerStateCallback slot semantics, mirroring test_raw_callback:
// a default-constructed slot is "no subscriber", a set slot delivers the
// state, and a new registration replaces the old.
namespace {
struct CapturingSubscriber {
ScannerState last{ScannerState::IDLE};
int calls{0};
static void trampoline(void *self, ScannerState state) {
auto *sub = static_cast<CapturingSubscriber *>(self);
sub->last = state;
sub->calls++;
}
};
} // namespace
TEST(ScannerStateCallback, DefaultConstructedSlotIsNotSet) {
const ScannerStateCallback callback{};
EXPECT_FALSE(callback.is_set());
}
TEST(ScannerStateCallback, SubscriberSeesState) {
CapturingSubscriber subscriber;
ScannerStateCallback callback{&subscriber, CapturingSubscriber::trampoline};
ASSERT_TRUE(callback.is_set());
callback.invoke(ScannerState::RUNNING);
EXPECT_EQ(subscriber.calls, 1);
EXPECT_EQ(subscriber.last, ScannerState::RUNNING);
}
TEST(ScannerStateCallback, NewSubscriberReplacesOld) {
CapturingSubscriber first;
CapturingSubscriber second;
ScannerStateCallback callback{&first, CapturingSubscriber::trampoline};
callback = {&second, CapturingSubscriber::trampoline};
callback.invoke(ScannerState::STOPPED);
EXPECT_EQ(first.calls, 0);
EXPECT_EQ(second.calls, 1);
EXPECT_EQ(second.last, ScannerState::STOPPED);
}
} // namespace esphome::ble_device_base::testing
@@ -0,0 +1,4 @@
binary_sensor:
- platform: ble_presence
mac_address: 11:22:33:44:55:66
name: BLE Test Presence
@@ -1,7 +1,10 @@
esp32_ble_tracker:
id: ble_tracker_hub
binary_sensor:
# Explicit ble_hub_id: pins the neutral binding as a declared key.
- platform: ble_presence
ble_hub_id: ble_tracker_hub
mac_address: AC:37:43:77:5F:4C
name: ESP32 BLE Tracker Google Home Mini
- platform: ble_presence
@@ -0,0 +1,3 @@
packages:
ln882h_ble_tracker: !include ../ln882h_ble_tracker/common.yaml
ble_presence: !include common-ln.yaml
@@ -0,0 +1,14 @@
# Config-only: the CI base board (generic-bk7252, BLE 4.2) cannot compile the
# BLE 5.x tracker, so this fixture proves validation (schema + neutral binding)
# on a non-esp32 platform; codegen and compilation are not exercised here.
bk72xx_ble_tracker:
id: ble_hub
binary_sensor:
- platform: ble_presence
ble_hub_id: ble_hub
mac_address: AC:37:43:77:5F:4C
name: BK BLE Presence
- platform: ble_presence
irk: 1234567890abcdef1234567890abcdef
name: BK BLE Presence IRK
+5
View File
@@ -0,0 +1,5 @@
sensor:
- platform: ble_rssi
# irk: is the only thing that emits USE_BLE_DEVICE_IRK off ESP32
irk: 1234567890abcdef1234567890abcdef
name: BLE Test RSSI
+6 -1
View File
@@ -1,7 +1,10 @@
esp32_ble_tracker:
id: ble_tracker_hub
sensor:
# Explicit ble_hub_id: pins the neutral binding as a declared key.
- platform: ble_rssi
ble_hub_id: ble_tracker_hub
mac_address: AC:37:43:77:5F:4C
name: BLE Google Home Mini RSSI value
- platform: ble_rssi
@@ -14,7 +17,9 @@ sensor:
service_uuid: 11223344-5566-7788-99aa-bbccddeeff00
name: BLE Test Service 128
- platform: ble_rssi
service_uuid: 11223344-5566-7788-99aa-bbccddeeff00
ibeacon_uuid: 11223344-5566-7788-99aa-bbccddeeff00
ibeacon_major: 100
ibeacon_minor: 1
name: BLE Test iBeacon UUID
- platform: ble_rssi
irk: 1234567890abcdef1234567890abcdef
@@ -0,0 +1,3 @@
packages:
ln882h_ble_tracker: !include ../ln882h_ble_tracker/common.yaml
ble_rssi: !include common-ln.yaml
@@ -0,0 +1,11 @@
# Config-only: pins the esp32_ble_id: -> ble_hub_id: deprecation alias — the
# legacy key must keep validating (with a rename warning) until its removal
# release (2027.2.0).
esp32_ble_tracker:
id: legacy_tracker
sensor:
- platform: ble_rssi
esp32_ble_id: legacy_tracker
mac_address: AC:37:43:77:5F:4C
name: Legacy Key RSSI
@@ -0,0 +1,14 @@
# Config-only: the CI base board (generic-bk7252, BLE 4.2) cannot compile the
# BLE 5.x tracker, so this fixture proves validation (schema + neutral binding)
# on a non-esp32 platform; codegen and compilation are not exercised here.
bk72xx_ble_tracker:
id: ble_hub
sensor:
- platform: ble_rssi
ble_hub_id: ble_hub
mac_address: AC:37:43:77:5F:4C
name: BK BLE RSSI
- platform: ble_rssi
irk: 1234567890abcdef1234567890abcdef
name: BK BLE RSSI IRK
@@ -0,0 +1,3 @@
text_sensor:
- platform: ble_scanner
name: BLE Test Scanner
+3
View File
@@ -1,5 +1,8 @@
esp32_ble_tracker:
id: ble_tracker_hub
text_sensor:
# Explicit ble_hub_id: pins the neutral binding as a declared key.
- platform: ble_scanner
ble_hub_id: ble_tracker_hub
name: Scanner
@@ -0,0 +1,3 @@
packages:
ln882h_ble_tracker: !include ../ln882h_ble_tracker/common.yaml
ble_scanner: !include common-ln.yaml
@@ -0,0 +1,13 @@
# Config-only: the CI base board (generic-bk7252, BLE 4.2) cannot compile the
# BLE 5.x tracker, so this fixture proves validation (schema + neutral binding)
# on a non-esp32 platform; codegen and compilation are not exercised here.
bk72xx_ble_tracker:
id: ble_hub
text_sensor:
- platform: ble_scanner
ble_hub_id: ble_hub
name: BK Scanner
# No ble_hub_id: exercises the generated binding _require_hub guards.
- platform: ble_scanner
name: BK Scanner Implicit
@@ -0,0 +1,21 @@
import esphome.codegen as cg
from tests.testing_helpers import ComponentManifestOverride
def override_manifest(manifest: ComponentManifestOverride) -> None:
# close_service_batch compiles only under USE_BLUETOOTH_PROXY_CONNECTIONS;
# emit the backend define so the host build exercises it.
async def to_code_testing(config):
# These defines are global to the merged host test binary. The api sources are
# compiled in it too (the api tests define USE_API), and USE_BLUETOOTH_PROXY would make
# them include and call bluetooth_proxy, which has no host build without a BLE hub.
cg.add_define("USE_BLE_GATT_CLIENT")
cg.add_define("USE_BLE_GATT_CLIENT_STUB_BACKEND")
# Gates the connection half of the API surface, which is what
# close_service_batch and the GATT response types live behind.
cg.add_define("USE_BLUETOOTH_PROXY_CONNECTIONS")
cg.add_define("BLUETOOTH_PROXY_MAX_CONNECTIONS", 1)
manifest.to_code = to_code_testing
# The batcher sizes api protobuf messages.
manifest.dependencies = manifest.dependencies + ["api"]
@@ -0,0 +1,8 @@
wifi:
ssid: MySSID
password: password1
ota:
- platform: esphome
api:
@@ -0,0 +1,58 @@
#include "esphome/components/bluetooth_connection/bluetooth_connection.h"
#include <gtest/gtest.h>
#include "esphome/components/api/api_pb2.h"
namespace esphome::bluetooth_connection {
// The three cursor behaviors: a fitting service advances and continues, an
// overflowing batch with >1 service pops and retries it, and a single
// oversized service is force-advanced so the stream cannot wedge.
static void add_service(api::BluetoothGATTGetServicesResponse &resp, uint16_t characteristics) {
resp.services.emplace_back();
auto &svc = resp.services.back();
svc.handle = resp.services.size();
svc.uuid = {0x1234567890ABCDEFULL, 0xFEDCBA0987654321ULL};
svc.characteristics.init(characteristics);
for (uint16_t i = 0; i < characteristics; i++) {
auto &chr = svc.characteristics.emplace_back();
chr.handle = 100 + i;
chr.properties = 0x12;
chr.uuid = {0x1234567890ABCDEFULL, 0xFEDCBA0987654321ULL};
}
}
TEST(CloseServiceBatch, FittingServiceAdvancesAndContinues) {
api::BluetoothGATTGetServicesResponse resp;
add_service(resp, 1);
size_t current_size = 0;
int16_t cursor = 0;
EXPECT_EQ(close_service_batch(resp, current_size, cursor, 0, "AA:BB"), BatchClose::CONTINUE);
EXPECT_EQ(cursor, 1);
EXPECT_GT(current_size, 0u);
}
TEST(CloseServiceBatch, OverflowPopsAndRetriesWithoutAdvancing) {
api::BluetoothGATTGetServicesResponse resp;
add_service(resp, 1);
add_service(resp, 1);
size_t current_size = MAX_PACKET_SIZE - 10; // any service is bigger than 10 bytes
int16_t cursor = 5;
EXPECT_EQ(close_service_batch(resp, current_size, cursor, 0, "AA:BB"), BatchClose::SEND);
EXPECT_EQ(resp.services.size(), 1u); // popped for the next batch
EXPECT_EQ(cursor, 5); // not advanced: retried next batch
}
TEST(CloseServiceBatch, SingleOversizedServiceForceAdvances) {
api::BluetoothGATTGetServicesResponse resp;
add_service(resp, 60); // ~30 bytes per characteristic, far past the budget
ASSERT_GT(resp.services.back().calculate_size(), MAX_PACKET_SIZE);
size_t current_size = 0;
int16_t cursor = 7;
EXPECT_EQ(close_service_batch(resp, current_size, cursor, 0, "AA:BB"), BatchClose::SEND);
EXPECT_EQ(cursor, 8); // advanced despite not fitting, so the stream moves on
}
} // namespace esphome::bluetooth_connection
@@ -0,0 +1,49 @@
// Pins the shared UUID wire packing and the size-estimate budget the service
// streamers rely on, in both efficient and legacy client modes.
#include "esphome/components/bluetooth_connection/bluetooth_connection.h"
#include <gtest/gtest.h>
namespace esphome::bluetooth_connection::testing {
using ble_device_base::ESPBTUUID;
TEST(GattUuidPacking, ShortUuidUsedWhenClientSupportsIt) {
std::array<uint64_t, 2> uuid128{};
uint32_t short_uuid = 0;
fill_gatt_uuid(uuid128, short_uuid, ESPBTUUID::from_uint16(0x180F), true);
EXPECT_EQ(short_uuid, 0x180Fu);
EXPECT_EQ(uuid128[0], 0u);
EXPECT_EQ(uuid128[1], 0u);
}
TEST(GattUuidPacking, LegacyClientGetsBaseUuidExpansion) {
// 0000180F-0000-1000-8000-00805F9B34FB
std::array<uint64_t, 2> uuid128{};
uint32_t short_uuid = 0;
fill_gatt_uuid(uuid128, short_uuid, ESPBTUUID::from_uint16(0x180F), false);
EXPECT_EQ(short_uuid, 0u);
EXPECT_EQ(uuid128[0], 0x0000180F00001000ULL);
EXPECT_EQ(uuid128[1], 0x800000805F9B34FBULL);
}
TEST(GattUuidPacking, FullUuidPassesThroughBigEndian) {
// 12345678-90AB-CDEF-1122-334455667788, stored little-endian in ESPBTUUID.
const uint8_t big_endian[16] = {0x12, 0x34, 0x56, 0x78, 0x90, 0xAB, 0xCD, 0xEF,
0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88};
std::array<uint64_t, 2> uuid128{};
uint32_t short_uuid = 0;
// Efficient mode must still use the 128-bit form for 128-bit UUIDs.
fill_gatt_uuid(uuid128, short_uuid, ESPBTUUID::from_raw_reversed(big_endian), true);
EXPECT_EQ(short_uuid, 0u);
EXPECT_EQ(uuid128[0], 0x1234567890ABCDEFULL);
EXPECT_EQ(uuid128[1], 0x1122334455667788ULL);
}
TEST(GattUuidPacking, EstimateGrowsWithCharacteristicsAndMode) {
// The estimate only gates batching; pin its shape, not exact bytes.
EXPECT_LT(estimate_service_size(0, true), estimate_service_size(0, false));
EXPECT_LT(estimate_service_size(1, false), estimate_service_size(2, false));
}
} // namespace esphome::bluetooth_connection::testing
@@ -0,0 +1,12 @@
# Distinct shape from bluetooth_proxy's own rp2 fixtures: explicit slot count
# on the platform whose backend lives in this component (validate-only, so it
# never collides with grouped builds).
packages:
common: !include common.yaml
rp2_ble_tracker:
# Two slots: the one shape where the wrap pools are smaller than the cap.
bluetooth_proxy:
active: true
connection_slots: 2
@@ -0,0 +1,12 @@
# Compile the gated filter path; no external component is in-tree to call
# enable_advertisement_filter(), so the define is forced here.
<<: !include common.yaml
esphome:
build_flags:
- "-DUSE_BLUETOOTH_PROXY_ADVERTISEMENT_FILTER"
esp32_ble_tracker:
bluetooth_proxy:
active: true
@@ -0,0 +1,12 @@
# Advertisement-only proxy on esp32 by explicit choice: no GATT backend is
# compiled (USE_BLE_GATT_CLIENT unset), which pins the HAS_GATT gating and the
# address-scoped maintenance path that a connections build never exercises.
# Under batch grouping the active default build is what runs; the standalone
# compile of this fixture is what exercises the passive gating.
packages:
common: !include common.yaml
esp32_ble_tracker:
bluetooth_proxy:
active: false
@@ -0,0 +1,11 @@
# Advertisement-only proxy on rp2 by explicit choice. Variant tests compile
# as their own builds when this component is tested individually; under CI
# batch grouping the active default build is what runs, so this fixture's
# guarantee is the individual run plus config validation.
packages:
common: !include common.yaml
rp2_ble_tracker:
bluetooth_proxy:
active: false
@@ -0,0 +1,10 @@
# Advertisement-only proxy on the ln882x BLE hub (active-scan-capable, in-tree
# since #16691) — a target CI fully compiles. Same bare-hub arrangement as
# test.rp2040-ard.yaml: no explicit ble_hub_id so a grouped build cannot
# collide with ln882h_ble_tracker's own fixture id.
packages:
common: !include common.yaml
ln882h_ble_tracker:
bluetooth_proxy:
@@ -0,0 +1,15 @@
# Full proxy on the rp2 BLE hub: active defaults to true here (esp32 parity),
# so this compiles the BTstack GATT client backend with the default three
# connection slots, exercising the rp2040_ble/btstack_memory.cpp pool --wrap
# link.
# No explicit ble_hub_id: the generated binding resolves the single declared
# hub, and an inline id here would collide with rp2_ble_tracker's own fixture
# once CI merges both components into one grouped rp2040-ard build (grouped
# component dicts collapse; only one id survives). The explicit-key form is
# covered by validate.rp2040-ard.yaml, which never participates in grouping.
packages:
common: !include common.yaml
rp2_ble_tracker:
bluetooth_proxy:
@@ -0,0 +1,9 @@
# Pico 2 W build of the full proxy: links the rp2350 framework archive, so
# the pool --wrap overrides and their per-architecture layout asserts are
# exercised for this chip too (see test.rp2040-ard.yaml for the slot shape).
packages:
common: !include common.yaml
rp2_ble_tracker:
bluetooth_proxy:
@@ -0,0 +1,11 @@
# Advertisement-only proxy on the bk72xx BLE hub (active-scan-capable since the
# tracker's packed-command start). Config-only: the CI base board generic-bk7252
# is BLE 4.2 and cannot compile the BLE 5.x tracker. Same bare-hub arrangement
# as test.ln882x-ard.yaml: no explicit ble_hub_id so a grouped build cannot
# collide with bk72xx_ble_tracker's own fixture id.
packages:
common: !include common.yaml
bk72xx_ble_tracker:
bluetooth_proxy:
@@ -0,0 +1,13 @@
# Connections given as a bare list, with no explicit per-entry id. The ids are
# generated during validation, so this config breaks if the schema validates the
# connections list more than once.
packages:
common: !include common.yaml
esp32_ble_tracker:
bluetooth_proxy:
active: true
connections:
- {}
- {}
@@ -0,0 +1,11 @@
# Explicit ble_hub_id on the rp2 hub — the documented disambiguator once a
# platform has more than one tracker. Validate-only: never merged into grouped
# builds, so the inline id cannot collide with rp2_ble_tracker's own fixture.
packages:
common: !include common.yaml
rp2_ble_tracker:
id: ble_hub
bluetooth_proxy:
ble_hub_id: ble_hub
@@ -0,0 +1,9 @@
sensor:
- platform: bthome_mithermometer
mac_address: A4:C1:38:4E:16:78
# bindkey compiles ble_device_base::aes_ccm_auth_decrypt off Espressif
bindkey: eef418daf699a0c188f3bfd17e4565d9
temperature:
name: BTHome Temperature
humidity:
name: BTHome Humidity
@@ -1,7 +1,10 @@
esp32_ble_tracker:
id: ble_tracker_hub
sensor:
# Explicit ble_hub_id: pins the neutral binding as a declared key.
- platform: bthome_mithermometer
ble_hub_id: ble_tracker_hub
mac_address: A4:C1:38:4E:16:78
bindkey: eef418daf699a0c188f3bfd17e4565d9
temperature:
@@ -0,0 +1,3 @@
packages:
ln882h_ble_tracker: !include ../ln882h_ble_tracker/common.yaml
bthome_mithermometer: !include common-ln.yaml
@@ -0,0 +1,18 @@
# Config-only: the CI base board (generic-bk7252, BLE 4.2) cannot compile the
# BLE 5.x tracker, so this fixture proves validation (schema + neutral binding)
# on a non-esp32 platform; codegen and compilation are not exercised here.
bk72xx_ble_tracker:
id: ble_hub
sensor:
- platform: bthome_mithermometer
ble_hub_id: ble_hub
mac_address: A4:C1:38:4E:16:78
bindkey: eef418daf699a0c188f3bfd17e4565d9
temperature:
name: BK BTHome Temperature
# No ble_hub_id: exercises the generated binding real configs use.
- platform: bthome_mithermometer
mac_address: A4:C1:38:4E:16:79
temperature:
name: BK BTHome Implicit Temperature
+11
View File
@@ -0,0 +1,11 @@
import esphome.codegen as cg
from tests.testing_helpers import ComponentManifestOverride
def override_manifest(manifest: ComponentManifestOverride) -> None:
# No host camera platform exists to emit USE_CAMERA; define it here so
# the iterator CAMERA state compiles into the test binary.
async def to_code_testing(config):
cg.add_define("USE_CAMERA")
manifest.to_code = to_code_testing
@@ -0,0 +1,79 @@
#include <gtest/gtest.h>
#include "esphome/core/component_iterator.h"
#ifdef USE_CAMERA
#include "esphome/components/camera/camera.h"
namespace esphome::testing {
class StubCamera : public camera::Camera {
public:
void add_listener(camera::CameraListener *listener) override {}
camera::CameraImageReader *create_image_reader() override { return nullptr; }
void request_image(camera::CameraRequester requester) override {}
void start_stream(camera::CameraRequester requester) override {}
void stop_stream(camera::CameraRequester requester) override {}
};
// Iterator that accepts everything except the camera, which can refuse a
// configurable number of times. The CAMERA state is a singleton path
// distinct from process_platform_item_; this pins the same contract:
// a refused camera is re-offered, never skipped.
class CameraRefusingIterator : public ComponentIterator {
public:
// NOLINTBEGIN(bugprone-macro-parentheses)
#define ENTITY_TYPE_(type, singular, plural, count, upper) \
bool on_##singular(type *obj) override { return true; }
#define ENTITY_CONTROLLER_TYPE_(type, singular, plural, count, upper, callback) \
ENTITY_TYPE_(type, singular, plural, count, upper)
#include "esphome/core/entity_types.h"
#undef ENTITY_TYPE_
#undef ENTITY_CONTROLLER_TYPE_
// NOLINTEND(bugprone-macro-parentheses)
bool on_camera(camera::Camera *obj) override {
this->camera_calls++;
if (this->camera_refusals > 0) {
this->camera_refusals--;
return false;
}
return true;
}
int camera_calls{0};
int camera_refusals{0};
};
// Far above the fixed number of iterator states
static constexpr size_t BIG_BUDGET = 1000;
class ComponentIteratorCameraTest : public ::testing::Test {
protected:
void SetUp() override {
// Constructing a Camera installs the process-wide singleton
static StubCamera stub_camera;
ASSERT_EQ(camera::Camera::instance(), &stub_camera);
}
};
TEST_F(ComponentIteratorCameraTest, RefusedCameraIsReofferedNotSkipped) {
CameraRefusingIterator it;
it.camera_refusals = 2;
it.begin();
// Runs until the camera refuses, which stops the pass
it.try_advance(BIG_BUDGET);
EXPECT_EQ(it.camera_calls, 1);
EXPECT_FALSE(it.completed());
// The camera is re-offered once per call, not skipped
it.try_advance(BIG_BUDGET);
EXPECT_EQ(it.camera_calls, 2);
EXPECT_FALSE(it.completed());
// Once accepted, the iteration completes
it.try_advance(BIG_BUDGET);
EXPECT_TRUE(it.completed());
EXPECT_EQ(it.camera_calls, 3);
}
} // namespace esphome::testing
#endif // USE_CAMERA
+18
View File
@@ -0,0 +1,18 @@
tinyusb:
id: tinyusb_test
usb_lang_id: 0x0123
usb_manufacturer_str: ESPHomeTestManufacturer
usb_product_id: 0x1234
usb_product_str: ESPHomeTestProduct
usb_serial_str: ESPHomeTestSerialNumber
usb_vendor_id: 0x2345
uart:
- id: uart_0
tx_pin: 14
rx_pin: 13
baud_rate: 115200
usb_cdc_acm:
interfaces:
- id: cdc_acm_1
@@ -0,0 +1,12 @@
# Second UART/CDC pair for a two-bridge setup. Kept out of common.yaml because the
# ESP32-S2 has only two UART controllers and the logger occupies one, so a second
# uart there would fail at runtime.
uart:
- id: uart_1
tx_pin: 15
rx_pin: 16
baud_rate: 115200
usb_cdc_acm:
interfaces:
- id: cdc_acm_2
@@ -0,0 +1,15 @@
packages:
cdc_acm_uart: !include common.yaml
cdc_acm_uart_dual: !include common_dual.yaml
bridge:
- platform: cdc_acm_uart
uart_id: uart_0
usb_cdc_acm_id: cdc_acm_1
dtr_pin: 40
rts_pin: 41
- platform: cdc_acm_uart
uart_id: uart_1
usb_cdc_acm_id: cdc_acm_2
dtr_pin: 20
rts_pin: 21
@@ -0,0 +1,14 @@
# ESP32-S2 has no USB_SERIAL_JTAG, so the logger defaults to USB_CDC, which shares
# the USB OTG peripheral with tinyusb. Use a hardware UART for logging instead.
logger:
hardware_uart: UART0
packages:
cdc_acm_uart: !include common.yaml
bridge:
- platform: cdc_acm_uart
uart_id: uart_0
usb_cdc_acm_id: cdc_acm_1
dtr_pin: 40
rts_pin: 41
@@ -0,0 +1,17 @@
packages:
cdc_acm_uart: !include common.yaml
cdc_acm_uart_dual: !include common_dual.yaml
bridge:
- platform: cdc_acm_uart
uart_id: uart_0
usb_cdc_acm_id: cdc_acm_1
dtr_pin: 40
rts_pin: 41
- platform: cdc_acm_uart
uart_id: uart_1
usb_cdc_acm_id: cdc_acm_2
# GPIO19/20 are USB D-/D+ on the S3 (which the CDC side itself uses); use
# unrelated free pins here.
dtr_pin: 17
rts_pin: 18
+73
View File
@@ -0,0 +1,73 @@
#include <gtest/gtest.h>
#include "esphome/components/climate/climate.h"
namespace esphome::climate::testing {
// Minimal concrete Climate that offers a fixed set of modes, so the restore path can be exercised
// without any hardware or platform component.
class TestClimate : public Climate {
public:
ClimateTraits traits() override {
auto traits = ClimateTraits();
traits.set_supported_modes({CLIMATE_MODE_OFF, CLIMATE_MODE_COOL});
traits.set_supported_fan_modes({CLIMATE_FAN_LOW, CLIMATE_FAN_HIGH});
return traits;
}
protected:
void control(const ClimateCall &call) override {}
};
TEST(ClimateRestoreStateTest, RestoresASupportedMode) {
TestClimate climate;
// Value-initialized: several members (mode, swing_mode, the temperature union) have no default
// member initializer, so leaving the {} off would read indeterminate values.
ClimateDeviceRestoreState state{};
state.mode = CLIMATE_MODE_COOL;
state.apply(&climate);
EXPECT_EQ(climate.mode, CLIMATE_MODE_COOL);
}
TEST(ClimateRestoreStateTest, DoesNotRestoreAnUnsupportedMode) {
TestClimate climate;
ClimateDeviceRestoreState state{};
state.mode = CLIMATE_MODE_HEAT;
state.apply(&climate);
// The device never advertised HEAT, so the mode stays where it was.
EXPECT_EQ(climate.mode, CLIMATE_MODE_OFF);
}
TEST(ClimateRestoreStateTest, LeavesTheCurrentModeAloneRatherThanForcingOff) {
TestClimate climate;
// apply() is public and nothing restricts it to setup(), so the entity is not necessarily off
// when an unsupported mode is dropped. It keeps what it had rather than being forced to OFF.
climate.mode = CLIMATE_MODE_COOL;
ClimateDeviceRestoreState state{};
state.mode = CLIMATE_MODE_HEAT;
state.apply(&climate);
EXPECT_EQ(climate.mode, CLIMATE_MODE_COOL);
}
TEST(ClimateRestoreStateTest, KeepsRestoringTheOtherFieldsWhenTheModeIsDropped) {
TestClimate climate;
ClimateDeviceRestoreState state{};
state.mode = CLIMATE_MODE_HEAT;
state.target_temperature = 21.0f;
state.uses_custom_fan_mode = false;
state.fan_mode = CLIMATE_FAN_HIGH;
state.apply(&climate);
EXPECT_EQ(climate.mode, CLIMATE_MODE_OFF);
EXPECT_FLOAT_EQ(climate.target_temperature, 21.0f);
// Compared as an optional: this asserts both that the fan mode was restored and what it holds.
EXPECT_EQ(climate.fan_mode, CLIMATE_FAN_HIGH);
}
} // namespace esphome::climate::testing
+1 -2
View File
@@ -30,8 +30,7 @@ climate:
- switch.turn_on: climate_heater_switch
- switch.turn_off: climate_cooler_switch
# Thermostat-based climate so climate.control: action variants get build
# coverage (bang_bang doesn't support fan modes, presets, etc.). Climate
# has no template platform, so thermostat is the right vehicle.
# coverage (bang_bang doesn't support fan modes, presets, etc.).
- platform: thermostat
id: climate_test_thermostat
name: Test Thermostat
@@ -12,5 +12,8 @@ climate:
- platform: climate_ir_lg
name: LG Climate
transmitter_id: xmitr
header_high: 3300us
header_low: 9840us
advanced_commands_support: true
sensor: climate_ir_lg_temp_sensor
humidity_sensor: humidity_sensor
+11
View File
@@ -0,0 +1,11 @@
# Pulls in sensor so entity iteration paths compile (USE_SENSOR);
# tests register their own instances. Plain yaml.safe_load, no ESPHome tags.
# An alphabetically-earlier component's sensor: block shadows this one in
# combined builds; the tests' sensor-count ASSERT catches a capacity drop.
sensor:
- platform: template
id: bench_sensor_a
name: "Bench A"
- platform: template
id: bench_sensor_b
name: "Bench B"
+74
View File
@@ -55,4 +55,78 @@ TEST(HelpersTest, Ilog10RoundTripMatchesLog10) {
}
}
TEST(StaticVectorTest, ConvertingConstructorFromSmaller) {
StaticVector<uint8_t, 5> small{0x03, 0x00, 0x10, 0x00, 0x01};
StaticVector<uint8_t, 253> big = small;
ASSERT_EQ(big.size(), small.size());
for (size_t i = 0; i < small.size(); i++) {
EXPECT_EQ(big[i], small[i]) << "mismatch at index " << i;
}
}
TEST(StaticVectorTest, ConvertingConstructorPartiallyFilledAndEmpty) {
StaticVector<uint8_t, 5> partial{0xAA, 0xBB};
StaticVector<uint8_t, 8> from_partial = partial;
ASSERT_EQ(from_partial.size(), 2u);
EXPECT_EQ(from_partial[0], 0xAA);
EXPECT_EQ(from_partial[1], 0xBB);
StaticVector<uint8_t, 5> empty;
StaticVector<uint8_t, 8> from_empty = empty;
EXPECT_TRUE(from_empty.empty());
}
TEST(StaticVectorTest, ConvertingConstructorSameSize) {
StaticVector<int, 3> src{1, 2, 3};
StaticVector<int, 3> dst = src;
ASSERT_EQ(dst.size(), 3u);
EXPECT_EQ(dst[2], 3);
}
TEST(StringContainsIgnoreCaseTest, NullPointerAlwaysFalse) {
const char *haystack = nullptr;
const char *needle = nullptr;
EXPECT_FALSE(str_contains_ignore_case(haystack, needle));
EXPECT_FALSE(str_contains_ignore_case("Hello World", needle));
EXPECT_FALSE(str_contains_ignore_case(haystack, "anything"));
}
TEST(StringContainsIgnoreCaseTest, EmptySearchMatches) {
const char *haystack = "Hello World";
EXPECT_TRUE(str_contains_ignore_case_fallback(haystack, ""));
}
TEST(StringContainsIgnoreCaseTest, MiscCaseMatches) {
const char *haystack = "Hello World";
EXPECT_TRUE(str_contains_ignore_case_fallback(haystack, "Hello"));
EXPECT_TRUE(str_contains_ignore_case_fallback(haystack, "hello"));
EXPECT_TRUE(str_contains_ignore_case_fallback(haystack, "HELLO"));
EXPECT_TRUE(str_contains_ignore_case_fallback(haystack, "hELLO"));
}
TEST(StringContainsIgnoreCaseTest, MiscNotMatching) {
const char *haystack = "Hello World";
// Expected to match
EXPECT_TRUE(str_contains_ignore_case_fallback(haystack, "Hell"));
// Expected not to match
EXPECT_FALSE(str_contains_ignore_case_fallback(haystack, "Heaven"));
EXPECT_FALSE(str_contains_ignore_case_fallback(haystack, "Hello!"));
EXPECT_FALSE(str_contains_ignore_case_fallback(haystack, "world!"));
}
TEST(StringContainsIgnoreCaseTest, FallbackMatchesLibc) {
const char *haystack = "Hello World";
for (const char *needle : {"", "Hello", "hELLO", "HELLO", "Hell", "world", "World", "Heaven", "Hello!", "d"}) {
EXPECT_EQ(str_contains_ignore_case_fallback(haystack, needle), str_contains_ignore_case(haystack, needle))
<< "needle: " << needle;
}
EXPECT_EQ(str_contains_ignore_case_fallback("", ""), str_contains_ignore_case("", ""));
EXPECT_EQ(str_contains_ignore_case_fallback("ab", "abc"), str_contains_ignore_case("ab", "abc"));
}
} // namespace esphome
+134
View File
@@ -0,0 +1,134 @@
#include <gtest/gtest.h>
#include <string>
#include "esphome/core/helpers.h"
#include "esphome/core/string_ref.h"
namespace esphome::testing {
namespace {
// Large enough that none of the inputs below are ever dropped.
constexpr size_t TEST_BUFFER_SIZE = 64 * JSON_ESCAPE_MAX_EXPANSION + 1;
// Escape into a stack buffer and return the result as a string so the expectations stay readable.
std::string escape(const std::string &value) {
char buf[TEST_BUFFER_SIZE];
return json_escape_into_buffer(buf, StringRef(value.c_str(), value.size()));
}
// Same, but with the short control forms turned off.
std::string escape_long(const std::string &value) {
char buf[TEST_BUFFER_SIZE];
return json_escape_into_buffer(buf, StringRef(value.c_str(), value.size()), false);
}
} // namespace
// Plain ASCII with no special characters is passed through unchanged.
TEST(JsonEscape, PlainStringUnchanged) {
EXPECT_EQ(escape("MyNetwork"), "MyNetwork");
EXPECT_EQ(escape(""), "");
}
// A double quote is escaped so it does not terminate the surrounding JSON string.
TEST(JsonEscape, EscapesDoubleQuote) {
EXPECT_EQ(escape("a\"b"), "a\\\"b");
// A double quote followed by other characters stays inside the JSON string.
EXPECT_EQ(escape("\">end"), "\\\">end");
}
// A backslash is doubled so it does not start an escape sequence in the output.
TEST(JsonEscape, EscapesBackslash) {
EXPECT_EQ(escape("a\\b"), "a\\\\b");
// A trailing backslash must not escape the closing quote of the JSON string.
EXPECT_EQ(escape("net\\"), "net\\\\");
}
// The control characters with short JSON forms use those forms.
TEST(JsonEscape, EscapesShortFormControls) {
EXPECT_EQ(escape("\n"), "\\n");
EXPECT_EQ(escape("\r"), "\\r");
EXPECT_EQ(escape("\t"), "\\t");
EXPECT_EQ(escape("\b"), "\\b");
EXPECT_EQ(escape("\f"), "\\f");
}
// Other control characters (< 0x20) without a short form become \u00XX with lowercase hex.
TEST(JsonEscape, EscapesOtherControlsAsUnicode) {
EXPECT_EQ(escape(std::string("\x00", 1)), "\\u0000");
EXPECT_EQ(escape("\x01"), "\\u0001");
EXPECT_EQ(escape("\x10"), "\\u0010");
EXPECT_EQ(escape("\x1f"), "\\u001f");
// 0x7f (DEL) is >= 0x20, so it is NOT escaped by this helper.
EXPECT_EQ(escape("\x7f"), "\x7f");
}
// With the short forms turned off, every control character is written as \u00XX instead.
TEST(JsonEscape, LongControlEscapes) {
EXPECT_EQ(escape_long("\n"), "\\u000a");
EXPECT_EQ(escape_long("\r"), "\\u000d");
EXPECT_EQ(escape_long("\t"), "\\u0009");
EXPECT_EQ(escape_long("\b"), "\\u0008");
EXPECT_EQ(escape_long("\f"), "\\u000c");
// Controls without a short form are unaffected by the flag.
EXPECT_EQ(escape_long("\x01"), "\\u0001");
}
// The flag only affects control characters. A quote or backslash is never written as \u00XX, because that form is
// no shorter and both modes have always emitted the two character escape.
TEST(JsonEscape, LongModeStillUsesTwoCharQuoteAndBackslash) {
EXPECT_EQ(escape_long("a\"b"), "a\\\"b");
EXPECT_EQ(escape_long("a\\b"), "a\\\\b");
// Ordinary text is untouched in either mode.
EXPECT_EQ(escape_long("MyDevice"), "MyDevice");
}
// Bytes >= 0x20, including multi-byte UTF-8 sequences, are passed through verbatim.
TEST(JsonEscape, PassesThroughUtf8) {
// "café" in UTF-8 (é == 0xC3 0xA9).
EXPECT_EQ(escape("caf\xc3\xa9"), "caf\xc3\xa9");
// Emoji (📶, 4-byte UTF-8) survives unchanged.
EXPECT_EQ(escape("\xf0\x9f\x93\xb6"), "\xf0\x9f\x93\xb6");
}
// A mix of special and normal characters is escaped in place without disturbing the rest.
TEST(JsonEscape, MixedContent) { EXPECT_EQ(escape("a\"b\\c\nd"), "a\\\"b\\\\c\\nd"); }
// A buffer sized at JSON_ESCAPE_MAX_EXPANSION bytes per input byte holds the worst case exactly.
TEST(JsonEscape, WorstCaseInputFitsExactly) {
constexpr size_t input_len = 8;
char buf[input_len * JSON_ESCAPE_MAX_EXPANSION + 1];
const std::string input(input_len, '\x01');
std::string expected;
for (size_t i = 0; i < input_len; i++)
expected += "\\u0001";
EXPECT_EQ(json_escape_into_buffer(buf, StringRef(input.c_str(), input.size())), expected);
}
// An escape sequence that would not fit is dropped whole rather than written partially, and the result stays null
// terminated.
TEST(JsonEscape, DropsEscapeThatWouldNotFit) {
// Room for one \u00XX sequence plus the null terminator, but two are requested.
char buf[JSON_ESCAPE_MAX_EXPANSION + 1];
const std::string input(2, '\x01');
const std::string result = json_escape_into_buffer(buf, StringRef(input.c_str(), input.size()));
EXPECT_EQ(result, "\\u0001");
EXPECT_EQ(buf[JSON_ESCAPE_MAX_EXPANSION], '\0');
}
// Plain characters are truncated at the buffer size, leaving room for the null terminator.
TEST(JsonEscape, TruncatesPlainInput) {
char buf[5];
const std::string input(20, 'a');
EXPECT_STREQ(json_escape_into_buffer(buf, StringRef(input.c_str(), input.size())), "aaaa");
}
// A zero length buffer cannot even hold a null terminator, so an empty string is returned instead of writing.
TEST(JsonEscape, EmptyBufferIsSafe) {
const std::string input("test");
EXPECT_STREQ(json_escape_into_buffer(std::span<char>(), StringRef(input.c_str(), input.size())), "");
}
} // namespace esphome::testing
@@ -0,0 +1,195 @@
#include <gtest/gtest.h>
#include "esphome/core/component_iterator.h"
#ifdef USE_SENSOR
#include "esphome/components/sensor/sensor.h"
#include "esphome/core/application.h"
#endif
namespace esphome::testing {
// Iterator whose begin/end callbacks can refuse a configurable number of
// times; all entity callbacks accept (any registered entities are accepted).
class RefusingIterator : public ComponentIterator {
public:
// NOLINTBEGIN(bugprone-macro-parentheses)
#define ENTITY_TYPE_(type, singular, plural, count, upper) \
bool on_##singular(type *obj) override { return true; }
#define ENTITY_CONTROLLER_TYPE_(type, singular, plural, count, upper, callback) \
ENTITY_TYPE_(type, singular, plural, count, upper)
#include "esphome/core/entity_types.h"
#undef ENTITY_TYPE_
#undef ENTITY_CONTROLLER_TYPE_
// NOLINTEND(bugprone-macro-parentheses)
bool on_begin() override { return step(this->begin_calls, this->begin_refusals); }
bool on_end() override { return step(this->end_calls, this->end_refusals); }
int begin_calls{0};
int end_calls{0};
int begin_refusals{0};
int end_refusals{0};
protected:
static bool step(int &calls, int &refusals) {
calls++;
if (refusals > 0) {
refusals--;
return false;
}
return true;
}
};
// Far above the fixed number of iterator states
static constexpr size_t BIG_BUDGET = 1000;
TEST(ComponentIterator, NotRunningMakesNoProgress) {
RefusingIterator it;
it.try_advance(BIG_BUDGET);
EXPECT_TRUE(it.completed());
EXPECT_EQ(it.begin_calls, 0);
EXPECT_EQ(it.end_calls, 0);
}
TEST(ComponentIterator, CompletesInOneCallWithoutRefusals) {
RefusingIterator it;
it.begin();
it.try_advance(BIG_BUDGET);
EXPECT_TRUE(it.completed());
EXPECT_EQ(it.begin_calls, 1);
EXPECT_EQ(it.end_calls, 1);
}
TEST(ComponentIterator, StepBudgetIsHonored) {
RefusingIterator it;
it.begin();
it.try_advance(1);
EXPECT_EQ(it.begin_calls, 1);
EXPECT_EQ(it.end_calls, 0);
EXPECT_FALSE(it.completed());
}
TEST(ComponentIterator, RefusedStepStopsBatchAndRetriesSameStep) {
RefusingIterator it;
it.end_refusals = 3;
it.begin();
// First call runs until the refused end step, which stops the pass
it.try_advance(BIG_BUDGET);
EXPECT_EQ(it.end_calls, 1);
EXPECT_FALSE(it.completed());
// The refused step is retried once per call, not skipped
it.try_advance(BIG_BUDGET);
it.try_advance(BIG_BUDGET);
EXPECT_EQ(it.end_calls, 3);
EXPECT_FALSE(it.completed());
// Once accepted, the iteration completes
it.try_advance(BIG_BUDGET);
EXPECT_TRUE(it.completed());
EXPECT_EQ(it.end_calls, 4);
}
TEST(ComponentIterator, RefusedBeginStopsBatchAndRetries) {
RefusingIterator it;
it.begin_refusals = 2;
it.begin();
it.try_advance(BIG_BUDGET);
it.try_advance(BIG_BUDGET);
EXPECT_EQ(it.begin_calls, 2);
EXPECT_FALSE(it.completed());
it.try_advance(BIG_BUDGET);
EXPECT_TRUE(it.completed());
EXPECT_EQ(it.begin_calls, 3);
}
// The deprecated advance() wrapper must keep the legacy once-per-loop
// pattern working during the deprecation window.
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
TEST(ComponentIterator, DeprecatedAdvanceKeepsLegacyPatternWorking) {
RefusingIterator it;
it.end_refusals = 2;
it.begin();
size_t guard = 0;
while (!it.completed() && guard++ < BIG_BUDGET) {
it.advance();
}
EXPECT_TRUE(it.completed());
// Two refused end steps were retried, then accepted
EXPECT_EQ(it.end_calls, 3);
}
#pragma GCC diagnostic pop
#ifdef USE_SENSOR
// Iterator whose sensor callback can refuse or yield; pins the per-item
// contract: a refused item is re-offered with at_ unchanged, never skipped.
class ItemRefusingIterator : public RefusingIterator {
public:
bool on_sensor(sensor::Sensor *obj) override {
this->last_sensor = obj;
if (!step(this->sensor_calls, this->sensor_refusals))
return false;
if (this->yield_on_sensor)
this->yield_after_step_();
return true;
}
sensor::Sensor *last_sensor{nullptr};
int sensor_calls{0};
int sensor_refusals{0};
bool yield_on_sensor{false};
};
class ComponentIteratorSensorTest : public ::testing::Test {
protected:
void SetUp() override {
static sensor::Sensor sensor_a;
static sensor::Sensor sensor_b;
static bool registered = false;
if (!registered) {
App.register_sensor(&sensor_a);
App.register_sensor(&sensor_b);
registered = true;
}
// StaticVector drops silently when full; fail the fixture, not the contract
ASSERT_EQ(App.get_sensors().size(), 2u) << "benchmark.yaml sensor count too small";
}
};
TEST_F(ComponentIteratorSensorTest, RefusedItemIsReofferedNotSkipped) {
ItemRefusingIterator it;
it.sensor_refusals = 2;
it.begin();
// Runs until the first sensor refuses
it.try_advance(BIG_BUDGET);
EXPECT_EQ(it.sensor_calls, 1);
EXPECT_FALSE(it.completed());
// The refused item is re-offered, not skipped
it.try_advance(BIG_BUDGET);
EXPECT_EQ(it.sensor_calls, 2);
sensor::Sensor *refused = it.last_sensor;
// Once accepted, iteration continues through the second sensor to the end
it.try_advance(BIG_BUDGET);
EXPECT_TRUE(it.completed());
EXPECT_NE(it.last_sensor, refused);
EXPECT_EQ(it.sensor_calls, 4);
}
TEST_F(ComponentIteratorSensorTest, YieldAfterStepEndsPassAndResumes) {
ItemRefusingIterator it;
it.yield_on_sensor = true;
it.begin();
// The pass ends right after the first sensor despite a big budget
it.try_advance(BIG_BUDGET);
EXPECT_EQ(it.sensor_calls, 1);
EXPECT_FALSE(it.completed());
// The next pass ends after the second sensor
it.try_advance(BIG_BUDGET);
EXPECT_EQ(it.sensor_calls, 2);
// Remaining states then run to completion in one pass
it.try_advance(BIG_BUDGET);
EXPECT_TRUE(it.completed());
}
#endif // USE_SENSOR
} // namespace esphome::testing
+136
View File
@@ -0,0 +1,136 @@
#include "esphome/core/event_pool.h"
#include <gtest/gtest.h>
#include <set>
namespace esphome::core::testing {
struct PoolItem {
int value{0};
// EventPool contract: release() cleans up per-object state; nothing here.
void release() {}
};
TEST(EventPool, AllocateUpToCapacityThenNull) {
esphome::EventPool<PoolItem, 4> pool;
PoolItem *items[4];
for (auto *&item : items) {
item = pool.allocate();
ASSERT_NE(item, nullptr);
}
// At capacity: the pool refuses rather than growing past SIZE.
EXPECT_EQ(pool.allocate(), nullptr);
}
TEST(EventPool, FullDrainRetainsEveryObject) {
// Pins the SIZE + 1 free-list sizing: a fully returned pool must hold all
// SIZE objects. With a SIZE-slot ring (capacity SIZE - 1) the last release()
// of a full drain was dropped, permanently orphaning one object.
esphome::EventPool<PoolItem, 4> pool;
PoolItem *items[4];
for (auto *&item : items)
item = pool.allocate();
for (auto *item : items)
pool.release(item);
// Every object must be allocatable again — no orphan, no new creation
// (total_created_ is already at SIZE, so a lost object would surface as a
// nullptr on the fourth allocation).
std::set<PoolItem *> seen;
for (int i = 0; i < 4; i++) {
PoolItem *item = pool.allocate();
ASSERT_NE(item, nullptr);
seen.insert(item);
}
// And they are the same four objects, recycled rather than re-created.
for (auto *item : items)
EXPECT_TRUE(seen.count(item) == 1);
EXPECT_EQ(pool.allocate(), nullptr);
}
TEST(EventPool, RepeatedDrainCyclesAreStable) {
esphome::EventPool<PoolItem, 3> pool;
// Several full allocate/release cycles: capacity must not shrink over time.
for (int cycle = 0; cycle < 10; cycle++) {
PoolItem *items[3];
for (auto *&item : items) {
item = pool.allocate();
ASSERT_NE(item, nullptr);
}
EXPECT_EQ(pool.allocate(), nullptr);
for (auto *item : items)
pool.release(item);
}
}
TEST(EventPool, ReleaseNullptrIsSafe) {
esphome::EventPool<PoolItem, 2> pool;
pool.release(nullptr);
EXPECT_NE(pool.allocate(), nullptr);
}
TEST(EventPool, WarmFullyPopulatesThePool) {
// warm()'s guarantee is invisible at runtime: no later allocate() may touch
// malloc(). Fully populated means SIZE allocations succeed from the free
// list and the SIZE + 1-th refuses.
esphome::EventPool<PoolItem, 4> pool;
ASSERT_TRUE(pool.warm());
PoolItem *items[4];
for (auto *&item : items) {
item = pool.allocate();
ASSERT_NE(item, nullptr);
}
EXPECT_EQ(pool.allocate(), nullptr);
}
TEST(EventPool, WarmIsIdempotent) {
esphome::EventPool<PoolItem, 3> pool;
ASSERT_TRUE(pool.warm());
ASSERT_TRUE(pool.warm());
// Still exactly SIZE objects: no growth past capacity.
PoolItem *items[3];
for (auto *&item : items) {
item = pool.allocate();
ASSERT_NE(item, nullptr);
}
EXPECT_EQ(pool.allocate(), nullptr);
}
TEST(EventPool, AllocateAfterWarmRecyclesTheWarmedObjects) {
// The objects handed out after warm() are the ones warm() created,
// recycled rather than re-created.
esphome::EventPool<PoolItem, 4> pool;
ASSERT_TRUE(pool.warm());
std::set<PoolItem *> first_round;
PoolItem *items[4];
for (auto *&item : items) {
item = pool.allocate();
first_round.insert(item);
}
for (auto *item : items)
pool.release(item);
for (int i = 0; i < 4; i++) {
PoolItem *item = pool.allocate();
ASSERT_NE(item, nullptr);
EXPECT_TRUE(first_round.count(item) == 1);
}
}
TEST(EventPool, WarmTopsUpWithEntriesOutstanding) {
// warm() counts existing entries (free or checked out) instead of failing
// when some are outstanding: it tops the pool up from any state.
esphome::EventPool<PoolItem, 4> pool;
PoolItem *held = pool.allocate();
ASSERT_NE(held, nullptr);
ASSERT_TRUE(pool.warm());
// The held object plus three more accounts for all SIZE entries.
PoolItem *items[3];
for (auto *&item : items) {
item = pool.allocate();
ASSERT_NE(item, nullptr);
}
EXPECT_EQ(pool.allocate(), nullptr);
}
} // namespace esphome::core::testing
+206
View File
@@ -1,6 +1,8 @@
#include <gtest/gtest.h>
#include <cmath>
#include <cstring>
#include "esphome/core/alloc_helpers.h"
#include "esphome/core/helpers.h"
namespace esphome::core::testing {
@@ -213,4 +215,208 @@ TEST(BufAppendSepStr, Truncation) {
EXPECT_EQ(end - buf, 7);
}
// --- base64 encode/decode ---
static const char BASE64_ALPHABET[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
// Pack 6-bit indices 0..63 into 48 bytes so encoding yields the full alphabet in order
TEST(Base64, EncodeProducesCanonicalAlphabet) {
uint8_t bytes[48];
size_t n = 0;
for (uint8_t i = 0; i < 64; i += 4) {
bytes[n++] = (i << 2) | ((i + 1) >> 4);
bytes[n++] = ((i + 1) & 0x0F) << 4 | ((i + 2) >> 2);
bytes[n++] = ((i + 2) & 0x03) << 6 | (i + 3);
}
std::string encoded = base64_encode(bytes, sizeof(bytes)); // NOLINT(esphome-heap-allocation) - host test
EXPECT_EQ(encoded, BASE64_ALPHABET);
}
// Decode the alphabet then re-encode: locks the encode and decode mappings together
TEST(Base64, DecodeCanonicalAlphabetRoundTrip) {
uint8_t buf[48];
size_t len = base64_decode(std::string(BASE64_ALPHABET), buf, sizeof(buf));
EXPECT_EQ(len, 48u);
std::string reencoded = base64_encode(buf, len); // NOLINT(esphome-heap-allocation) - host test
EXPECT_EQ(reencoded, BASE64_ALPHABET);
}
TEST(Base64, DecodeBase64UrlMatchesStandard) {
std::string url = BASE64_ALPHABET;
for (char &c : url) {
if (c == '+')
c = '-';
if (c == '/')
c = '_';
}
uint8_t standard[48], urlsafe[48];
size_t len_standard = base64_decode(std::string(BASE64_ALPHABET), standard, sizeof(standard));
size_t len_url = base64_decode(url, urlsafe, sizeof(urlsafe));
EXPECT_EQ(len_standard, len_url);
EXPECT_EQ(memcmp(standard, urlsafe, len_standard), 0);
}
// RFC 4648 vectors cover both padding cases (len % 3 == 1 and len % 3 == 2)
TEST(Base64, Rfc4648Vectors) {
const struct {
const char *plain;
const char *encoded;
} vectors[] = {
{"", ""},
{"f", "Zg=="},
{"fo", "Zm8="},
{"foo", "Zm9v"},
{"foob", "Zm9vYg=="},
{"fooba", "Zm9vYmE="},
{"foobar", "Zm9vYmFy"},
};
for (const auto &v : vectors) {
const auto *plain = reinterpret_cast<const uint8_t *>(v.plain);
std::string encoded = base64_encode(plain, strlen(v.plain)); // NOLINT(esphome-heap-allocation) - host test
EXPECT_EQ(encoded, v.encoded);
uint8_t buf[8];
size_t len = base64_decode(reinterpret_cast<const uint8_t *>(v.encoded), strlen(v.encoded), buf, sizeof(buf));
EXPECT_EQ(len, strlen(v.plain));
EXPECT_EQ(memcmp(buf, v.plain, len), 0);
}
}
// --- step_to_accuracy_decimals() ---
TEST(StepToAccuracyDecimals, TypicalSteps) {
EXPECT_EQ(step_to_accuracy_decimals(0.001f), 3);
EXPECT_EQ(step_to_accuracy_decimals(0.005f), 3);
EXPECT_EQ(step_to_accuracy_decimals(0.01f), 2);
EXPECT_EQ(step_to_accuracy_decimals(0.025f), 3);
EXPECT_EQ(step_to_accuracy_decimals(0.05f), 2);
EXPECT_EQ(step_to_accuracy_decimals(0.1f), 1);
EXPECT_EQ(step_to_accuracy_decimals(0.25f), 2);
EXPECT_EQ(step_to_accuracy_decimals(0.5f), 1);
EXPECT_EQ(step_to_accuracy_decimals(1.5f), 1);
EXPECT_EQ(step_to_accuracy_decimals(2.5f), 1);
}
TEST(StepToAccuracyDecimals, WholeSteps) {
EXPECT_EQ(step_to_accuracy_decimals(1.0f), 0);
EXPECT_EQ(step_to_accuracy_decimals(2.0f), 0);
EXPECT_EQ(step_to_accuracy_decimals(5.0f), 0);
EXPECT_EQ(step_to_accuracy_decimals(10.0f), 0);
EXPECT_EQ(step_to_accuracy_decimals(100.0f), 0);
EXPECT_EQ(step_to_accuracy_decimals(1000.0f), 0);
}
TEST(StepToAccuracyDecimals, FiveSignificantDigits) {
EXPECT_EQ(step_to_accuracy_decimals(1.23456f), 4);
EXPECT_EQ(step_to_accuracy_decimals(12.345f), 3);
EXPECT_EQ(step_to_accuracy_decimals(123.45f), 2);
EXPECT_EQ(step_to_accuracy_decimals(1234.5f), 1);
EXPECT_EQ(step_to_accuracy_decimals(12345.0f), 0);
EXPECT_EQ(step_to_accuracy_decimals(0.33333f), 5);
EXPECT_EQ(step_to_accuracy_decimals(0.0001f), 4);
}
TEST(StepToAccuracyDecimals, TrailingZerosDropped) {
EXPECT_EQ(step_to_accuracy_decimals(0.3f), 1);
EXPECT_EQ(step_to_accuracy_decimals(0.7f), 1);
EXPECT_EQ(step_to_accuracy_decimals(0.125f), 3);
EXPECT_EQ(step_to_accuracy_decimals(0.0625f), 4);
}
TEST(StepToAccuracyDecimals, RoundsUpToWholeNumber) {
// Rounds to five significant digits first, so this becomes 10 with no decimals.
EXPECT_EQ(step_to_accuracy_decimals(9.999999f), 0);
}
TEST(StepToAccuracyDecimals, OutsideFixedNotationRange) {
// %.5g would print these in exponent form; the count is now the real one rather than a parse of "1e-05".
EXPECT_EQ(step_to_accuracy_decimals(0.00001f), 5);
EXPECT_EQ(step_to_accuracy_decimals(0.000125f), 6);
EXPECT_EQ(step_to_accuracy_decimals(123456.0f), 0);
EXPECT_EQ(step_to_accuracy_decimals(1000000.0f), 0);
}
TEST(StepToAccuracyDecimals, SignIgnored) {
EXPECT_EQ(step_to_accuracy_decimals(-0.1f), 1);
EXPECT_EQ(step_to_accuracy_decimals(-0.25f), 2);
EXPECT_EQ(step_to_accuracy_decimals(-1.0f), 0);
}
TEST(StepToAccuracyDecimals, NonFiniteAndZero) {
EXPECT_EQ(step_to_accuracy_decimals(0.0f), 0);
EXPECT_EQ(step_to_accuracy_decimals(NAN), 0);
EXPECT_EQ(step_to_accuracy_decimals(INFINITY), 0);
EXPECT_EQ(step_to_accuracy_decimals(-INFINITY), 0);
}
// --- FixedVector::try_init() ---
// Keeps the block observable, else the compiler may drop the malloc and free pair and fold the check
static void escape(const void *p) { asm volatile("" : : "g"(p) : "memory"); }
TEST(FixedVectorTryInit, ReportsExhaustionAndStaysEmpty) {
FixedVector<uint32_t> v;
const bool ok = v.try_init(SIZE_MAX / sizeof(uint32_t));
escape(&v);
EXPECT_FALSE(ok);
EXPECT_EQ(v.capacity(), 0u);
EXPECT_FALSE(v.try_init(SIZE_MAX / sizeof(uint32_t) + 1)); // byte count would wrap
EXPECT_EQ(v.capacity(), 0u);
EXPECT_TRUE(v.try_init(0));
EXPECT_TRUE(v.try_init(4));
v.push_back(7);
EXPECT_EQ(v.size(), 1u);
}
// --- RAMAllocator::make_unique() ---
namespace {
struct Probe {
static inline int live = 0;
int a;
int b;
Probe(int a, int b) : a(a), b(b) { live++; }
~Probe() { live--; }
};
} // namespace
static_assert(sizeof(RAMUniquePtr<Probe>) == sizeof(Probe *), "the deleter must not add storage");
TEST(RAMAllocatorMakeUnique, ForwardsArgsAndDestroysOnce) {
auto p = RAMAllocator<Probe>().make_unique(3, 4);
ASSERT_NE(p, nullptr);
EXPECT_EQ(p->a, 3);
EXPECT_EQ(p->b, 4);
EXPECT_EQ(Probe::live, 1);
p.reset();
EXPECT_EQ(Probe::live, 0);
}
TEST(RAMAllocatorMakeUnique, ValueInitializesLikeMakeUnique) {
struct Plain {
uint32_t words[8];
};
// Dirty a block of the same size first so a recycled allocation is not zero by chance
auto dirty = RAMAllocator<uint8_t>().make_unique_array_for_overwrite(sizeof(Plain));
std::memset(dirty.get(), 0xFF, sizeof(Plain));
dirty.reset();
auto p = RAMAllocator<Plain>().make_unique();
ASSERT_NE(p, nullptr);
// Under ASan fresh blocks are filled with 0xbe, so this holds even when the dirtied block is not reused
EXPECT_TRUE(std::all_of(std::begin(p->words), std::end(p->words), [](uint32_t w) { return w == 0; }));
}
TEST(RAMAllocatorMakeUnique, ArrayFormRejectsOverflowAndZero) {
EXPECT_EQ(RAMAllocator<uint32_t>().make_unique_array_for_overwrite(SIZE_MAX / sizeof(uint32_t) + 1), nullptr);
EXPECT_EQ(RAMAllocator<uint32_t>().make_unique_array_for_overwrite(0), nullptr);
EXPECT_NE(RAMAllocator<uint32_t>().make_unique_array_for_overwrite(1), nullptr);
}
TEST(RAMAllocatorMakeUnique, ArrayFormAllocatesElements) {
RAMUniquePtr<uint8_t[]> buf = RAMAllocator<uint8_t>().make_unique_array_for_overwrite(256);
ASSERT_NE(buf, nullptr);
std::memset(buf.get(), 0xA5, 256);
EXPECT_EQ(buf[0], 0xA5);
EXPECT_EQ(buf[255], 0xA5);
}
} // namespace esphome::core::testing
@@ -0,0 +1,115 @@
// Exercises the LockFreeQueue PlainAtomic path under ESPHOME_THREAD_SINGLE —
// the gate added for single-threaded platforms whose only concurrency is
// same-core interrupt preemption (RP2: BTstack packet handler in the CYW43
// async-context IRQ). The define is forced before the include so this TU
// deterministically compiles that path regardless of the host's default
// thread model. The instantiations here deliberately differ from
// test_lock_free_queue.cpp's (uint32_t elements, non-power-of-2 sizes): no
// template instantiation is shared between the two TUs, so the differing
// AtomicIndex definitions can never collide under the one-definition rule,
// and the non-power-of-2 sizes cover next_index()'s comparison branch, which
// the other TU's power-of-2 sizes never reach.
#define ESPHOME_THREAD_SINGLE
#include "esphome/core/lock_free_queue.h"
#include <gtest/gtest.h>
#include <atomic>
#include <type_traits>
namespace esphome::core::testing {
// Pin the gate itself: under ESPHOME_THREAD_SINGLE the index type must be the
// PlainAtomic fallback, not std::atomic — otherwise the RP2040 build silently
// pulls __atomic_* library calls back in.
static_assert(!std::is_same_v<esphome::lockfree_internal::AtomicIndex<uint8_t>, std::atomic<uint8_t>>,
"ESPHOME_THREAD_SINGLE must select the PlainAtomic index path");
TEST(LockFreeQueueThreadSingle, EmptyPopReturnsNull) {
esphome::LockFreeQueue<uint32_t, 5> q;
EXPECT_EQ(q.pop(), nullptr);
EXPECT_TRUE(q.empty());
EXPECT_FALSE(q.full());
EXPECT_EQ(q.size(), 0u);
}
TEST(LockFreeQueueThreadSingle, FifoOrder) {
esphome::LockFreeQueue<uint32_t, 5> q;
uint32_t a = 1, b = 2, c = 3, d = 4;
EXPECT_TRUE(q.push(&a));
EXPECT_TRUE(q.push(&b));
EXPECT_TRUE(q.push(&c));
EXPECT_TRUE(q.push(&d));
EXPECT_EQ(q.size(), 4u);
EXPECT_EQ(q.pop(), &a);
EXPECT_EQ(q.pop(), &b);
EXPECT_EQ(q.pop(), &c);
EXPECT_EQ(q.pop(), &d);
EXPECT_EQ(q.pop(), nullptr);
}
TEST(LockFreeQueueThreadSingle, CapacityIsSizeMinusOne) {
esphome::LockFreeQueue<uint32_t, 5> q;
uint32_t v[5] = {0, 1, 2, 3, 4};
EXPECT_TRUE(q.push(&v[0]));
EXPECT_TRUE(q.push(&v[1]));
EXPECT_TRUE(q.push(&v[2]));
EXPECT_TRUE(q.push(&v[3]));
EXPECT_TRUE(q.full());
// Ring reserves one slot: the SIZEth push fails and is counted as dropped.
EXPECT_FALSE(q.push(&v[4]));
EXPECT_EQ(q.get_and_reset_dropped_count(), 1u);
EXPECT_EQ(q.get_and_reset_dropped_count(), 0u); // reset is sticky
}
TEST(LockFreeQueueThreadSingle, NullPushRejected) {
esphome::LockFreeQueue<uint32_t, 5> q;
EXPECT_FALSE(q.push(nullptr));
EXPECT_TRUE(q.empty());
}
TEST(LockFreeQueueThreadSingle, WrapAround) {
// Non-power-of-2 SIZE: next_index() wraps via the comparison branch here.
esphome::LockFreeQueue<uint32_t, 5> q;
uint32_t v[4] = {10, 20, 30, 40};
// Cycle several times the ring size to cross the wrap boundary repeatedly.
for (int cycle = 0; cycle < 10; cycle++) {
for (auto &value : v)
ASSERT_TRUE(q.push(&value));
EXPECT_TRUE(q.full());
for (auto &value : v)
ASSERT_EQ(q.pop(), &value);
EXPECT_TRUE(q.empty());
}
EXPECT_EQ(q.get_and_reset_dropped_count(), 0u);
}
TEST(LockFreeQueueThreadSingle, IncrementDroppedCount) {
esphome::LockFreeQueue<uint32_t, 5> q;
// Producer-side external drop accounting (pool exhausted before push).
q.increment_dropped_count();
q.increment_dropped_count();
EXPECT_EQ(q.get_and_reset_dropped_count(), 2u);
}
TEST(LockFreeQueueThreadSingle, InterleavedPushPop) {
esphome::LockFreeQueue<uint32_t, 7> q;
uint32_t v[64];
uint32_t popped = 0;
for (uint32_t i = 0; i < 64; i++) {
v[i] = i;
ASSERT_TRUE(q.push(&v[i]));
if (i % 2 == 1) {
uint32_t *first = q.pop();
ASSERT_NE(first, nullptr);
EXPECT_EQ(*first, popped++);
uint32_t *second = q.pop();
ASSERT_NE(second, nullptr);
EXPECT_EQ(*second, popped++);
}
}
EXPECT_TRUE(q.empty());
EXPECT_EQ(popped, 64u);
}
} // namespace esphome::core::testing
@@ -0,0 +1,91 @@
// Pins the preferences contract concepts so the surface they enforce cannot
// drift unnoticed: a minimal conforming type must satisfy each concept, and a
// type missing a method or returning the wrong type must not.
#include <gtest/gtest.h>
#include "esphome/core/preference_backend.h"
namespace esphome::core::testing {
struct MinimalBackend {
bool save(const uint8_t *, size_t) { return true; }
bool load(uint8_t *, size_t) { return true; }
};
static_assert(PreferenceBackendContract<MinimalBackend>);
struct BackendMissingLoad {
bool save(const uint8_t *, size_t) { return true; }
};
static_assert(!PreferenceBackendContract<BackendMissingLoad>);
struct BackendWrongReturn {
void save(const uint8_t *, size_t) {}
bool load(uint8_t *, size_t) { return true; }
};
static_assert(!PreferenceBackendContract<BackendWrongReturn>);
struct MinimalPreferences : public PreferencesMixin<MinimalPreferences> {
using PreferencesMixin<MinimalPreferences>::make_preference;
ESPPreferenceObject make_preference(size_t, uint32_t, bool) { return {}; }
ESPPreferenceObject make_preference(size_t, uint32_t) { return {}; }
bool sync() { return true; }
bool reset() { return true; }
};
static_assert(PreferencesContract<MinimalPreferences>);
struct PreferencesMissingTwoArgForm : public PreferencesMixin<PreferencesMissingTwoArgForm> {
using PreferencesMixin<PreferencesMissingTwoArgForm>::make_preference;
ESPPreferenceObject make_preference(size_t, uint32_t, bool) { return {}; }
bool sync() { return true; }
bool reset() { return true; }
};
static_assert(!PreferencesContract<PreferencesMissingTwoArgForm>);
struct PreferencesMissingReset : public PreferencesMixin<PreferencesMissingReset> {
using PreferencesMixin<PreferencesMissingReset>::make_preference;
ESPPreferenceObject make_preference(size_t, uint32_t, bool) { return {}; }
ESPPreferenceObject make_preference(size_t, uint32_t) { return {}; }
bool sync() { return true; }
};
static_assert(!PreferencesContract<PreferencesMissingReset>);
struct PreferencesWrongSyncReturn : public PreferencesMixin<PreferencesWrongSyncReturn> {
using PreferencesMixin<PreferencesWrongSyncReturn>::make_preference;
ESPPreferenceObject make_preference(size_t, uint32_t, bool) { return {}; }
ESPPreferenceObject make_preference(size_t, uint32_t) { return {}; }
void sync() {}
bool reset() { return true; }
};
static_assert(!PreferencesContract<PreferencesWrongSyncReturn>);
// Forgot `using PreferencesMixin<X>::make_preference;`, so the derived
// overloads hide the template forms (see the PreferencesContract note in
// preference_backend.h); the concept must reject the class.
struct PreferencesForgotUsingDeclaration : public PreferencesMixin<PreferencesForgotUsingDeclaration> {
ESPPreferenceObject make_preference(size_t, uint32_t, bool) { return {}; }
ESPPreferenceObject make_preference(size_t, uint32_t) { return {}; }
bool sync() { return true; }
bool reset() { return true; }
};
static_assert(!PreferencesContract<PreferencesForgotUsingDeclaration>);
struct MinimalKeyLookup {
bool load_from_key(uint32_t, uint8_t *, size_t) { return true; }
};
static_assert(PreferencesKeyLookupContract<MinimalKeyLookup>);
struct KeyLookupMissingMethod {};
static_assert(!PreferencesKeyLookupContract<KeyLookupMissingMethod>);
TEST(PreferenceContract, NullBackendRefusesBothOperations) {
// ESPPreferenceObject forwards to whichever backend the platform binds; a
// default-constructed object has no backend and must refuse both operations
// instead of crashing.
ESPPreferenceObject without_backend;
uint32_t value = 42;
EXPECT_FALSE(without_backend.save(&value));
EXPECT_FALSE(without_backend.load(&value));
}
} // namespace esphome::core::testing
+62
View File
@@ -0,0 +1,62 @@
#include <gtest/gtest.h>
#include "esphome/core/string_ref.h"
namespace esphome::core::testing {
TEST(StringRefStartsWith, ProperPrefixMatches) {
StringRef ref("FR:R20:12345", 12);
EXPECT_TRUE(ref.starts_with("FR:"));
}
TEST(StringRefStartsWith, WholeStringIsAPrefixOfItself) {
StringRef ref("TP96", 4);
EXPECT_TRUE(ref.starts_with("TP96"));
}
TEST(StringRefStartsWith, PrefixLongerThanViewFails) {
StringRef ref("TP", 2);
EXPECT_FALSE(ref.starts_with("TP96"));
}
TEST(StringRefStartsWith, DifferentContentFails) {
StringRef ref("TP96", 4);
EXPECT_FALSE(ref.starts_with("FR:"));
}
TEST(StringRefStartsWith, EmptyPrefixAlwaysMatches) {
StringRef ref("abc", 3);
EXPECT_TRUE(ref.starts_with(""));
StringRef empty;
EXPECT_TRUE(empty.starts_with(""));
}
TEST(StringRefStartsWith, EmptyViewOnlyMatchesEmptyPrefix) {
StringRef empty;
EXPECT_FALSE(empty.starts_with("a"));
}
TEST(StringRefStartsWith, WorksOnANonTerminatedBuffer) {
// The reason the helper exists: a bounded view over a buffer with no
// terminator anywhere near the viewed bytes.
const char raw[] = {'R', 'a', 'd', 'o', 'n', 'X'};
StringRef ref(raw, 5);
EXPECT_TRUE(ref.starts_with("Radon"));
EXPECT_FALSE(ref.starts_with("RadonEye"));
EXPECT_FALSE(ref.starts_with("adon"));
}
TEST(StringRefStartsWith, StdStringOverload) {
StringRef ref("TP96", 4);
EXPECT_TRUE(ref.starts_with(std::string("TP")));
EXPECT_FALSE(ref.starts_with(std::string("96")));
}
TEST(StringRefStartsWith, RefOverloadComparesOnlyTheViewedLength) {
// The prefix is a bounded view: bytes past its length must not be compared.
StringRef ref("FR:123", 6);
StringRef prefix("FR:xyz", 3);
EXPECT_TRUE(ref.starts_with(prefix));
}
} // namespace esphome::core::testing
+1 -1
View File
@@ -3,6 +3,6 @@ substitutions:
rx_pin: GPIO14
packages:
uart_38400: !include ../../test_build_components/common/uart_38400/esp32-idf.yaml
uart_38400_even: !include ../../test_build_components/common/uart_38400_even/esp32-idf.yaml
<<: !include common.yaml
@@ -3,6 +3,6 @@ substitutions:
rx_pin: GPIO3
packages:
uart_38400: !include ../../test_build_components/common/uart_38400/esp8266-ard.yaml
uart_38400_even: !include ../../test_build_components/common/uart_38400_even/esp8266-ard.yaml
<<: !include common.yaml

Some files were not shown because too many files have changed in this diff Show More