mirror of
https://github.com/esphome/esphome.git
synced 2026-10-07 11:26:39 +00:00
Merge remote-tracking branch 'upstream/dev' into esp32-sram1-as-iram
# Conflicts: # esphome/components/esp32/__init__.py
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
/.esphome/
|
||||
/secrets.yaml
|
||||
@@ -0,0 +1,93 @@
|
||||
#include <benchmark/benchmark.h>
|
||||
|
||||
#include "esphome/components/api/api_pb2.h"
|
||||
#include "esphome/components/api/api_buffer.h"
|
||||
|
||||
namespace esphome::api::benchmarks {
|
||||
|
||||
// Inner iteration count to amortize CodSpeed instrumentation overhead.
|
||||
// Without this, the ~60ns per-iteration valgrind start/stop cost dominates
|
||||
// sub-microsecond benchmarks.
|
||||
static constexpr int kInnerIterations = 2000;
|
||||
|
||||
// Helper: encode a message into a buffer and return it.
|
||||
// Benchmarks encode once in setup, then decode the resulting bytes in a loop.
|
||||
// This keeps decode benchmarks in sync with the actual protobuf schema —
|
||||
// hand-encoded byte arrays would silently break when fields change.
|
||||
template<typename T> static APIBuffer encode_message(const T &msg) {
|
||||
APIBuffer buffer;
|
||||
uint32_t size = msg.calculate_size();
|
||||
buffer.resize(size);
|
||||
ProtoWriteBuffer writer(&buffer, 0);
|
||||
msg.encode(writer);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
// --- HelloRequest decode (string + varint fields) ---
|
||||
|
||||
static void Decode_HelloRequest(benchmark::State &state) {
|
||||
HelloRequest source;
|
||||
source.client_info = StringRef::from_lit("aioesphomeapi");
|
||||
source.api_version_major = 1;
|
||||
source.api_version_minor = 10;
|
||||
auto encoded = encode_message(source);
|
||||
|
||||
for (auto _ : state) {
|
||||
HelloRequest msg;
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
msg.decode(encoded.data(), encoded.size());
|
||||
}
|
||||
benchmark::DoNotOptimize(msg.api_version_major);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(Decode_HelloRequest);
|
||||
|
||||
// --- SwitchCommandRequest decode (simple command) ---
|
||||
|
||||
static void Decode_SwitchCommandRequest(benchmark::State &state) {
|
||||
SwitchCommandRequest source;
|
||||
source.key = 0x12345678;
|
||||
source.state = true;
|
||||
auto encoded = encode_message(source);
|
||||
|
||||
for (auto _ : state) {
|
||||
SwitchCommandRequest msg;
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
msg.decode(encoded.data(), encoded.size());
|
||||
}
|
||||
benchmark::DoNotOptimize(msg.state);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(Decode_SwitchCommandRequest);
|
||||
|
||||
// --- LightCommandRequest decode (complex command with many fields) ---
|
||||
|
||||
static void Decode_LightCommandRequest(benchmark::State &state) {
|
||||
LightCommandRequest source;
|
||||
source.key = 0x11223344;
|
||||
source.has_state = true;
|
||||
source.state = true;
|
||||
source.has_brightness = true;
|
||||
source.brightness = 0.8f;
|
||||
source.has_rgb = true;
|
||||
source.red = 1.0f;
|
||||
source.green = 0.5f;
|
||||
source.blue = 0.2f;
|
||||
source.has_effect = true;
|
||||
source.effect = StringRef::from_lit("rainbow");
|
||||
auto encoded = encode_message(source);
|
||||
|
||||
for (auto _ : state) {
|
||||
LightCommandRequest msg;
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
msg.decode(encoded.data(), encoded.size());
|
||||
}
|
||||
benchmark::DoNotOptimize(msg.brightness);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(Decode_LightCommandRequest);
|
||||
|
||||
} // namespace esphome::api::benchmarks
|
||||
@@ -0,0 +1,298 @@
|
||||
#include <benchmark/benchmark.h>
|
||||
|
||||
#include "esphome/components/api/api_pb2.h"
|
||||
#include "esphome/components/api/api_buffer.h"
|
||||
|
||||
namespace esphome::api::benchmarks {
|
||||
|
||||
// Inner iteration count to amortize CodSpeed instrumentation overhead.
|
||||
// Without this, the ~60ns per-iteration valgrind start/stop cost dominates
|
||||
// sub-microsecond benchmarks.
|
||||
static constexpr int kInnerIterations = 2000;
|
||||
|
||||
// --- SensorStateResponse (highest frequency message) ---
|
||||
|
||||
static void Encode_SensorStateResponse(benchmark::State &state) {
|
||||
APIBuffer buffer;
|
||||
SensorStateResponse msg;
|
||||
msg.key = 0x12345678;
|
||||
msg.state = 23.5f;
|
||||
msg.missing_state = false;
|
||||
uint32_t size = msg.calculate_size();
|
||||
buffer.resize(size);
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
ProtoWriteBuffer writer(&buffer, 0);
|
||||
msg.encode(writer);
|
||||
}
|
||||
benchmark::DoNotOptimize(buffer.data());
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(Encode_SensorStateResponse);
|
||||
|
||||
static void CalculateSize_SensorStateResponse(benchmark::State &state) {
|
||||
SensorStateResponse msg;
|
||||
msg.key = 0x12345678;
|
||||
msg.state = 23.5f;
|
||||
msg.missing_state = false;
|
||||
|
||||
for (auto _ : state) {
|
||||
uint32_t result = 0;
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
result += msg.calculate_size();
|
||||
}
|
||||
benchmark::DoNotOptimize(result);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(CalculateSize_SensorStateResponse);
|
||||
|
||||
// Steady state: buffer already allocated from previous iteration
|
||||
static void CalcAndEncode_SensorStateResponse(benchmark::State &state) {
|
||||
APIBuffer buffer;
|
||||
SensorStateResponse msg;
|
||||
msg.key = 0x12345678;
|
||||
msg.state = 23.5f;
|
||||
msg.missing_state = false;
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
uint32_t size = msg.calculate_size();
|
||||
buffer.resize(size);
|
||||
ProtoWriteBuffer writer(&buffer, 0);
|
||||
msg.encode(writer);
|
||||
}
|
||||
benchmark::DoNotOptimize(buffer.data());
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(CalcAndEncode_SensorStateResponse);
|
||||
|
||||
// Cold path: fresh buffer each iteration (measures heap allocation cost).
|
||||
// Inner loop still needed to amortize CodSpeed instrumentation overhead.
|
||||
// Each inner iteration creates a fresh buffer, so this measures
|
||||
// alloc+calc+encode per item.
|
||||
static void CalcAndEncode_SensorStateResponse_Fresh(benchmark::State &state) {
|
||||
SensorStateResponse msg;
|
||||
msg.key = 0x12345678;
|
||||
msg.state = 23.5f;
|
||||
msg.missing_state = false;
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
APIBuffer buffer;
|
||||
uint32_t size = msg.calculate_size();
|
||||
buffer.resize(size);
|
||||
ProtoWriteBuffer writer(&buffer, 0);
|
||||
msg.encode(writer);
|
||||
benchmark::DoNotOptimize(buffer.data());
|
||||
}
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(CalcAndEncode_SensorStateResponse_Fresh);
|
||||
|
||||
// --- BinarySensorStateResponse ---
|
||||
|
||||
static void Encode_BinarySensorStateResponse(benchmark::State &state) {
|
||||
APIBuffer buffer;
|
||||
BinarySensorStateResponse msg;
|
||||
msg.key = 0xAABBCCDD;
|
||||
msg.state = true;
|
||||
msg.missing_state = false;
|
||||
uint32_t size = msg.calculate_size();
|
||||
buffer.resize(size);
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
ProtoWriteBuffer writer(&buffer, 0);
|
||||
msg.encode(writer);
|
||||
}
|
||||
benchmark::DoNotOptimize(buffer.data());
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(Encode_BinarySensorStateResponse);
|
||||
|
||||
// --- HelloResponse (string fields) ---
|
||||
|
||||
static void Encode_HelloResponse(benchmark::State &state) {
|
||||
APIBuffer buffer;
|
||||
HelloResponse msg;
|
||||
msg.api_version_major = 1;
|
||||
msg.api_version_minor = 10;
|
||||
msg.server_info = StringRef::from_lit("esphome v2026.3.0");
|
||||
msg.name = StringRef::from_lit("living-room-sensor");
|
||||
uint32_t size = msg.calculate_size();
|
||||
buffer.resize(size);
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
ProtoWriteBuffer writer(&buffer, 0);
|
||||
msg.encode(writer);
|
||||
}
|
||||
benchmark::DoNotOptimize(buffer.data());
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(Encode_HelloResponse);
|
||||
|
||||
// --- LightStateResponse (complex multi-field message) ---
|
||||
|
||||
static void Encode_LightStateResponse(benchmark::State &state) {
|
||||
APIBuffer buffer;
|
||||
LightStateResponse msg;
|
||||
msg.key = 0x11223344;
|
||||
msg.state = true;
|
||||
msg.brightness = 0.8f;
|
||||
msg.color_mode = enums::COLOR_MODE_RGB_WHITE;
|
||||
msg.color_brightness = 1.0f;
|
||||
msg.red = 1.0f;
|
||||
msg.green = 0.5f;
|
||||
msg.blue = 0.2f;
|
||||
msg.white = 0.0f;
|
||||
msg.color_temperature = 4000.0f;
|
||||
msg.cold_white = 0.0f;
|
||||
msg.warm_white = 0.0f;
|
||||
msg.effect = StringRef::from_lit("rainbow");
|
||||
uint32_t size = msg.calculate_size();
|
||||
buffer.resize(size);
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
ProtoWriteBuffer writer(&buffer, 0);
|
||||
msg.encode(writer);
|
||||
}
|
||||
benchmark::DoNotOptimize(buffer.data());
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(Encode_LightStateResponse);
|
||||
|
||||
static void CalculateSize_LightStateResponse(benchmark::State &state) {
|
||||
LightStateResponse msg;
|
||||
msg.key = 0x11223344;
|
||||
msg.state = true;
|
||||
msg.brightness = 0.8f;
|
||||
msg.color_mode = enums::COLOR_MODE_RGB_WHITE;
|
||||
msg.color_brightness = 1.0f;
|
||||
msg.red = 1.0f;
|
||||
msg.green = 0.5f;
|
||||
msg.blue = 0.2f;
|
||||
msg.white = 0.0f;
|
||||
msg.color_temperature = 4000.0f;
|
||||
msg.cold_white = 0.0f;
|
||||
msg.warm_white = 0.0f;
|
||||
msg.effect = StringRef::from_lit("rainbow");
|
||||
|
||||
for (auto _ : state) {
|
||||
uint32_t result = 0;
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
result += msg.calculate_size();
|
||||
}
|
||||
benchmark::DoNotOptimize(result);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(CalculateSize_LightStateResponse);
|
||||
|
||||
// --- DeviceInfoResponse (nested submessages: 20 devices + 20 areas) ---
|
||||
|
||||
static DeviceInfoResponse make_device_info_response() {
|
||||
DeviceInfoResponse msg;
|
||||
msg.name = StringRef::from_lit("living-room-sensor");
|
||||
msg.mac_address = StringRef::from_lit("AA:BB:CC:DD:EE:FF");
|
||||
msg.esphome_version = StringRef::from_lit("2026.3.0");
|
||||
msg.compilation_time = StringRef::from_lit("Mar 16 2026, 12:00:00");
|
||||
msg.model = StringRef::from_lit("esp32-poe-iso");
|
||||
msg.manufacturer = StringRef::from_lit("Olimex");
|
||||
msg.friendly_name = StringRef::from_lit("Living Room Sensor");
|
||||
#ifdef USE_DEVICES
|
||||
for (uint32_t i = 0; i < ESPHOME_DEVICE_COUNT && i < 20; i++) {
|
||||
msg.devices[i].device_id = i + 1;
|
||||
msg.devices[i].name = StringRef::from_lit("device");
|
||||
msg.devices[i].area_id = (i % 20) + 1;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_AREAS
|
||||
for (uint32_t i = 0; i < ESPHOME_AREA_COUNT && i < 20; i++) {
|
||||
msg.areas[i].area_id = i + 1;
|
||||
msg.areas[i].name = StringRef::from_lit("area");
|
||||
}
|
||||
#endif
|
||||
return msg;
|
||||
}
|
||||
|
||||
static void CalculateSize_DeviceInfoResponse(benchmark::State &state) {
|
||||
auto msg = make_device_info_response();
|
||||
|
||||
for (auto _ : state) {
|
||||
uint32_t result = 0;
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
result += msg.calculate_size();
|
||||
}
|
||||
benchmark::DoNotOptimize(result);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(CalculateSize_DeviceInfoResponse);
|
||||
|
||||
static void Encode_DeviceInfoResponse(benchmark::State &state) {
|
||||
auto msg = make_device_info_response();
|
||||
APIBuffer buffer;
|
||||
uint32_t total_size = msg.calculate_size();
|
||||
buffer.resize(total_size);
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
ProtoWriteBuffer writer(&buffer, 0);
|
||||
msg.encode(writer);
|
||||
}
|
||||
benchmark::DoNotOptimize(buffer.data());
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(Encode_DeviceInfoResponse);
|
||||
|
||||
// Steady state: buffer already allocated from previous iteration
|
||||
static void CalcAndEncode_DeviceInfoResponse(benchmark::State &state) {
|
||||
auto msg = make_device_info_response();
|
||||
APIBuffer buffer;
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
uint32_t size = msg.calculate_size();
|
||||
buffer.resize(size);
|
||||
ProtoWriteBuffer writer(&buffer, 0);
|
||||
msg.encode(writer);
|
||||
}
|
||||
benchmark::DoNotOptimize(buffer.data());
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(CalcAndEncode_DeviceInfoResponse);
|
||||
|
||||
// Cold path: fresh buffer each iteration (measures heap allocation cost).
|
||||
// Inner loop still needed to amortize CodSpeed instrumentation overhead.
|
||||
// Each inner iteration creates a fresh buffer, so this measures
|
||||
// alloc+calc+encode per item.
|
||||
static void CalcAndEncode_DeviceInfoResponse_Fresh(benchmark::State &state) {
|
||||
auto msg = make_device_info_response();
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
APIBuffer buffer;
|
||||
uint32_t size = msg.calculate_size();
|
||||
buffer.resize(size);
|
||||
ProtoWriteBuffer writer(&buffer, 0);
|
||||
msg.encode(writer);
|
||||
benchmark::DoNotOptimize(buffer.data());
|
||||
}
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(CalcAndEncode_DeviceInfoResponse_Fresh);
|
||||
|
||||
} // namespace esphome::api::benchmarks
|
||||
@@ -0,0 +1,133 @@
|
||||
#include <benchmark/benchmark.h>
|
||||
|
||||
#include "esphome/components/api/proto.h"
|
||||
#include "esphome/components/api/api_buffer.h"
|
||||
|
||||
namespace esphome::api::benchmarks {
|
||||
|
||||
// Inner iteration count to amortize CodSpeed instrumentation overhead.
|
||||
// Without this, the ~60ns per-iteration valgrind start/stop cost dominates
|
||||
// sub-microsecond benchmarks.
|
||||
static constexpr int kInnerIterations = 2000;
|
||||
|
||||
// --- ProtoVarInt::parse() benchmarks ---
|
||||
|
||||
static void ProtoVarInt_Parse_SingleByte(benchmark::State &state) {
|
||||
uint8_t buf[] = {0x42}; // value = 66
|
||||
|
||||
for (auto _ : state) {
|
||||
ProtoVarIntResult result{};
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
result = ProtoVarInt::parse(buf, sizeof(buf));
|
||||
}
|
||||
benchmark::DoNotOptimize(result);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(ProtoVarInt_Parse_SingleByte);
|
||||
|
||||
static void ProtoVarInt_Parse_TwoByte(benchmark::State &state) {
|
||||
uint8_t buf[] = {0x80, 0x01}; // value = 128
|
||||
|
||||
for (auto _ : state) {
|
||||
ProtoVarIntResult result{};
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
result = ProtoVarInt::parse(buf, sizeof(buf));
|
||||
}
|
||||
benchmark::DoNotOptimize(result);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(ProtoVarInt_Parse_TwoByte);
|
||||
|
||||
static void ProtoVarInt_Parse_FiveByte(benchmark::State &state) {
|
||||
uint8_t buf[] = {0xFF, 0xFF, 0xFF, 0xFF, 0x0F};
|
||||
|
||||
for (auto _ : state) {
|
||||
ProtoVarIntResult result{};
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
result = ProtoVarInt::parse(buf, sizeof(buf));
|
||||
}
|
||||
benchmark::DoNotOptimize(result);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(ProtoVarInt_Parse_FiveByte);
|
||||
|
||||
// --- Varint encoding benchmarks ---
|
||||
|
||||
static void Encode_Varint_Small(benchmark::State &state) {
|
||||
APIBuffer buffer;
|
||||
buffer.resize(16);
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
ProtoWriteBuffer writer(&buffer, 0);
|
||||
writer.encode_varint_raw(42);
|
||||
}
|
||||
benchmark::DoNotOptimize(buffer.data());
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(Encode_Varint_Small);
|
||||
|
||||
static void Encode_Varint_Large(benchmark::State &state) {
|
||||
APIBuffer buffer;
|
||||
buffer.resize(16);
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
ProtoWriteBuffer writer(&buffer, 0);
|
||||
writer.encode_varint_raw(300);
|
||||
}
|
||||
benchmark::DoNotOptimize(buffer.data());
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(Encode_Varint_Large);
|
||||
|
||||
static void Encode_Varint_MaxUint32(benchmark::State &state) {
|
||||
APIBuffer buffer;
|
||||
buffer.resize(16);
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
ProtoWriteBuffer writer(&buffer, 0);
|
||||
writer.encode_varint_raw(0xFFFFFFFF);
|
||||
}
|
||||
benchmark::DoNotOptimize(buffer.data());
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(Encode_Varint_MaxUint32);
|
||||
|
||||
// --- ProtoSize::varint() benchmarks ---
|
||||
|
||||
static void ProtoSize_Varint_Small(benchmark::State &state) {
|
||||
// Use varying input to prevent constant folding.
|
||||
// Values 0-127 all take 1 byte but the compiler can't prove that.
|
||||
for (auto _ : state) {
|
||||
uint32_t result = 0;
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
result += ProtoSize::varint(static_cast<uint32_t>(i) & 0x7F);
|
||||
}
|
||||
benchmark::DoNotOptimize(result);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(ProtoSize_Varint_Small);
|
||||
|
||||
static void ProtoSize_Varint_Large(benchmark::State &state) {
|
||||
// Use varying input to prevent constant folding.
|
||||
for (auto _ : state) {
|
||||
uint32_t result = 0;
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
result += ProtoSize::varint(0xFFFF0000 | static_cast<uint32_t>(i));
|
||||
}
|
||||
benchmark::DoNotOptimize(result);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(ProtoSize_Varint_Large);
|
||||
|
||||
} // namespace esphome::api::benchmarks
|
||||
@@ -0,0 +1,114 @@
|
||||
# Components needed for API protobuf benchmarks.
|
||||
# Merged into the base config before validation so all
|
||||
# dependencies get proper defaults.
|
||||
#
|
||||
# esphome: sub-keys are merged into the base config.
|
||||
esphome:
|
||||
areas:
|
||||
- id: area_1
|
||||
name: "Area 1"
|
||||
- id: area_2
|
||||
name: "Area 2"
|
||||
- id: area_3
|
||||
name: "Area 3"
|
||||
- id: area_4
|
||||
name: "Area 4"
|
||||
- id: area_5
|
||||
name: "Area 5"
|
||||
- id: area_6
|
||||
name: "Area 6"
|
||||
- id: area_7
|
||||
name: "Area 7"
|
||||
- id: area_8
|
||||
name: "Area 8"
|
||||
- id: area_9
|
||||
name: "Area 9"
|
||||
- id: area_10
|
||||
name: "Area 10"
|
||||
- id: area_11
|
||||
name: "Area 11"
|
||||
- id: area_12
|
||||
name: "Area 12"
|
||||
- id: area_13
|
||||
name: "Area 13"
|
||||
- id: area_14
|
||||
name: "Area 14"
|
||||
- id: area_15
|
||||
name: "Area 15"
|
||||
- id: area_16
|
||||
name: "Area 16"
|
||||
- id: area_17
|
||||
name: "Area 17"
|
||||
- id: area_18
|
||||
name: "Area 18"
|
||||
- id: area_19
|
||||
name: "Area 19"
|
||||
- id: area_20
|
||||
name: "Area 20"
|
||||
devices:
|
||||
- id: device_1
|
||||
name: "Device 1"
|
||||
area_id: area_1
|
||||
- id: device_2
|
||||
name: "Device 2"
|
||||
area_id: area_2
|
||||
- id: device_3
|
||||
name: "Device 3"
|
||||
area_id: area_3
|
||||
- id: device_4
|
||||
name: "Device 4"
|
||||
area_id: area_4
|
||||
- id: device_5
|
||||
name: "Device 5"
|
||||
area_id: area_5
|
||||
- id: device_6
|
||||
name: "Device 6"
|
||||
area_id: area_6
|
||||
- id: device_7
|
||||
name: "Device 7"
|
||||
area_id: area_7
|
||||
- id: device_8
|
||||
name: "Device 8"
|
||||
area_id: area_8
|
||||
- id: device_9
|
||||
name: "Device 9"
|
||||
area_id: area_9
|
||||
- id: device_10
|
||||
name: "Device 10"
|
||||
area_id: area_10
|
||||
- id: device_11
|
||||
name: "Device 11"
|
||||
area_id: area_11
|
||||
- id: device_12
|
||||
name: "Device 12"
|
||||
area_id: area_12
|
||||
- id: device_13
|
||||
name: "Device 13"
|
||||
area_id: area_13
|
||||
- id: device_14
|
||||
name: "Device 14"
|
||||
area_id: area_14
|
||||
- id: device_15
|
||||
name: "Device 15"
|
||||
area_id: area_15
|
||||
- id: device_16
|
||||
name: "Device 16"
|
||||
area_id: area_16
|
||||
- id: device_17
|
||||
name: "Device 17"
|
||||
area_id: area_17
|
||||
- id: device_18
|
||||
name: "Device 18"
|
||||
area_id: area_18
|
||||
- id: device_19
|
||||
name: "Device 19"
|
||||
area_id: area_19
|
||||
- id: device_20
|
||||
name: "Device 20"
|
||||
area_id: area_20
|
||||
|
||||
api:
|
||||
sensor:
|
||||
binary_sensor:
|
||||
light:
|
||||
switch:
|
||||
@@ -0,0 +1,7 @@
|
||||
from tests.testing_helpers import ComponentManifestOverride
|
||||
|
||||
|
||||
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
# core (esphome/core/config.py) must run its to_code during builds
|
||||
# because it bootstraps the fundamental application infrastructure.
|
||||
manifest.enable_codegen()
|
||||
@@ -0,0 +1,7 @@
|
||||
from tests.testing_helpers import ComponentManifestOverride
|
||||
|
||||
|
||||
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
# host must run its to_code during builds because it sets up
|
||||
# the host platform target execution environment.
|
||||
manifest.enable_codegen()
|
||||
@@ -0,0 +1,7 @@
|
||||
from tests.testing_helpers import ComponentManifestOverride
|
||||
|
||||
|
||||
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
# json must run its to_code during benchmark builds because it
|
||||
# adds the ArduinoJson library dependency needed by the API component.
|
||||
manifest.enable_codegen()
|
||||
@@ -0,0 +1,7 @@
|
||||
from tests.testing_helpers import ComponentManifestOverride
|
||||
|
||||
|
||||
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
# logger must run its to_code during builds because it configures
|
||||
# the logging subsystem used by ESP_LOG* macros.
|
||||
manifest.enable_codegen()
|
||||
@@ -0,0 +1,42 @@
|
||||
#include <benchmark/benchmark.h>
|
||||
|
||||
#include "esphome/components/logger/logger.h"
|
||||
|
||||
/*
|
||||
This special main.cpp provides the entry point for Google Benchmark.
|
||||
It replaces the default ESPHome main with a benchmark runner.
|
||||
|
||||
*/
|
||||
|
||||
// Auto generated code by esphome
|
||||
// ========== AUTO GENERATED INCLUDE BLOCK BEGIN ===========
|
||||
// ========== AUTO GENERATED INCLUDE BLOCK END ===========
|
||||
|
||||
void original_setup() {
|
||||
// Code-generated App initialization (pre_setup, area/device registration, etc.)
|
||||
|
||||
// ========== AUTO GENERATED CODE BEGIN ===========
|
||||
// =========== AUTO GENERATED CODE END ============
|
||||
}
|
||||
|
||||
void setup() {
|
||||
// Run auto-generated initialization (App.pre_setup, area/device registration,
|
||||
// looping_components_.init, etc.) so benchmarks that use App work correctly.
|
||||
original_setup();
|
||||
|
||||
// Log functions call global_logger->log_vprintf_() without a null check,
|
||||
// so we must set up a Logger before any test that triggers logging.
|
||||
static esphome::logger::Logger test_logger(0);
|
||||
test_logger.set_log_level(ESPHOME_LOG_LEVEL);
|
||||
test_logger.pre_setup();
|
||||
|
||||
int argc = 1;
|
||||
char arg0[] = "benchmark";
|
||||
char *argv[] = {arg0, nullptr};
|
||||
::benchmark::Initialize(&argc, argv);
|
||||
::benchmark::RunSpecifiedBenchmarks();
|
||||
::benchmark::Shutdown();
|
||||
exit(0);
|
||||
}
|
||||
|
||||
void loop() {}
|
||||
@@ -0,0 +1,9 @@
|
||||
import esphome.codegen as cg
|
||||
from tests.testing_helpers import ComponentManifestOverride
|
||||
|
||||
|
||||
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
async def to_code(config):
|
||||
cg.add_build_flag("-DUSE_TIME_TIMEZONE")
|
||||
|
||||
manifest.to_code = to_code
|
||||
@@ -0,0 +1,22 @@
|
||||
#include <benchmark/benchmark.h>
|
||||
|
||||
#include "esphome/core/application.h"
|
||||
|
||||
namespace esphome::benchmarks {
|
||||
|
||||
// Benchmark Application::loop() with no registered components.
|
||||
// App is initialized by original_setup() in main.cpp (code-generated
|
||||
// pre_setup, area/device registration, looping_components_.init).
|
||||
// This measures the baseline overhead of the main loop: scheduler,
|
||||
// timing, before/after loop tasks, and yield_with_select_.
|
||||
static void ApplicationLoop_Empty(benchmark::State &state) {
|
||||
// Set loop interval to 0 so yield_with_select_ returns immediately
|
||||
// instead of sleeping. This benchmarks the loop overhead, not the sleep.
|
||||
App.set_loop_interval(0);
|
||||
for (auto _ : state) {
|
||||
App.loop();
|
||||
}
|
||||
}
|
||||
BENCHMARK(ApplicationLoop_Empty);
|
||||
|
||||
} // namespace esphome::benchmarks
|
||||
@@ -0,0 +1,41 @@
|
||||
#include <benchmark/benchmark.h>
|
||||
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome::benchmarks {
|
||||
|
||||
// Inner iteration count to amortize CodSpeed instrumentation overhead.
|
||||
// Without this, the ~60ns per-iteration valgrind start/stop cost dominates
|
||||
// sub-microsecond benchmarks.
|
||||
static constexpr int kInnerIterations = 2000;
|
||||
|
||||
// --- random_float() ---
|
||||
// Ported from ol.yaml:148 "Random Float Benchmark"
|
||||
|
||||
static void RandomFloat(benchmark::State &state) {
|
||||
for (auto _ : state) {
|
||||
float result = 0.0f;
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
result += random_float();
|
||||
}
|
||||
benchmark::DoNotOptimize(result);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(RandomFloat);
|
||||
|
||||
// --- random_uint32() ---
|
||||
|
||||
static void RandomUint32(benchmark::State &state) {
|
||||
for (auto _ : state) {
|
||||
uint32_t result = 0;
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
result += random_uint32();
|
||||
}
|
||||
benchmark::DoNotOptimize(result);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(RandomUint32);
|
||||
|
||||
} // namespace esphome::benchmarks
|
||||
@@ -0,0 +1,54 @@
|
||||
#include <benchmark/benchmark.h>
|
||||
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::benchmarks {
|
||||
|
||||
// Inner iteration count to amortize CodSpeed instrumentation overhead.
|
||||
// Without this, the ~60ns per-iteration valgrind start/stop cost dominates
|
||||
// sub-microsecond benchmarks.
|
||||
static constexpr int kInnerIterations = 2000;
|
||||
|
||||
static const char *const TAG = "bench";
|
||||
|
||||
// --- Log a message with no format specifiers (fastest path) ---
|
||||
|
||||
static void Logger_NoFormat(benchmark::State &state) {
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
ESP_LOGW(TAG, "Something happened");
|
||||
}
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(Logger_NoFormat);
|
||||
|
||||
// --- Log a message with 3 uint32_t format specifiers ---
|
||||
|
||||
static void Logger_3Uint32(benchmark::State &state) {
|
||||
uint32_t a = 12345, b = 67890, c = 99999;
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
ESP_LOGW(TAG, "Values: %" PRIu32 " %" PRIu32 " %" PRIu32, a, b, c);
|
||||
}
|
||||
benchmark::DoNotOptimize(a);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(Logger_3Uint32);
|
||||
|
||||
// --- Log a message with 3 floats (common for sensor values) ---
|
||||
|
||||
static void Logger_3Float(benchmark::State &state) {
|
||||
float temp = 23.456f, humidity = 67.89f, pressure = 1013.25f;
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
ESP_LOGW(TAG, "Sensor: %.2f %.1f %.2f", temp, humidity, pressure);
|
||||
}
|
||||
benchmark::DoNotOptimize(temp);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(Logger_3Float);
|
||||
|
||||
} // namespace esphome::benchmarks
|
||||
@@ -0,0 +1,143 @@
|
||||
#include <benchmark/benchmark.h>
|
||||
|
||||
#include "esphome/core/scheduler.h"
|
||||
#include "esphome/core/hal.h"
|
||||
|
||||
namespace esphome::benchmarks {
|
||||
|
||||
// Inner iteration count to amortize CodSpeed instrumentation overhead.
|
||||
// Without this, the ~60ns per-iteration valgrind start/stop cost dominates
|
||||
// sub-microsecond benchmarks.
|
||||
static constexpr int kInnerIterations = 2000;
|
||||
|
||||
// --- Scheduler fast path: no work to do ---
|
||||
|
||||
static void Scheduler_Call_NoWork(benchmark::State &state) {
|
||||
Scheduler scheduler;
|
||||
uint32_t now = millis();
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
scheduler.call(now);
|
||||
}
|
||||
benchmark::DoNotOptimize(now);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(Scheduler_Call_NoWork);
|
||||
|
||||
// --- Scheduler with timers: call() when timers exist but aren't due ---
|
||||
|
||||
static void Scheduler_Call_TimersNotDue(benchmark::State &state) {
|
||||
Scheduler scheduler;
|
||||
Component dummy_component;
|
||||
|
||||
// Add some timeouts far in the future
|
||||
for (int i = 0; i < 10; i++) {
|
||||
scheduler.set_timeout(&dummy_component, static_cast<uint32_t>(i), 1000000, []() {});
|
||||
}
|
||||
scheduler.process_to_add();
|
||||
|
||||
uint32_t now = millis();
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
scheduler.call(now);
|
||||
}
|
||||
benchmark::DoNotOptimize(now);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(Scheduler_Call_TimersNotDue);
|
||||
|
||||
// --- Scheduler with 5 intervals firing every call ---
|
||||
|
||||
static void Scheduler_Call_5IntervalsFiring(benchmark::State &state) {
|
||||
Scheduler scheduler;
|
||||
Component dummy_component;
|
||||
int fire_count = 0;
|
||||
|
||||
// Benchmarks the heap-based scheduler dispatch with 5 callbacks firing.
|
||||
// Uses monotonically increasing fake time so intervals reliably fire every call.
|
||||
// USE_BENCHMARK ifdef in component.h disables WarnIfComponentBlockingGuard
|
||||
// (fake now > real millis() would cause underflow in finish()).
|
||||
// interval=0 would cause an infinite loop (reschedules at same now).
|
||||
for (int i = 0; i < 5; i++) {
|
||||
scheduler.set_interval(&dummy_component, static_cast<uint32_t>(i), 1, [&fire_count]() { fire_count++; });
|
||||
}
|
||||
scheduler.process_to_add();
|
||||
|
||||
uint32_t now = millis() + 100;
|
||||
|
||||
for (auto _ : state) {
|
||||
scheduler.call(now);
|
||||
now++;
|
||||
benchmark::DoNotOptimize(fire_count);
|
||||
}
|
||||
}
|
||||
BENCHMARK(Scheduler_Call_5IntervalsFiring);
|
||||
|
||||
// --- Scheduler: set_timeout registration ---
|
||||
|
||||
static void Scheduler_SetTimeout(benchmark::State &state) {
|
||||
Scheduler scheduler;
|
||||
Component dummy_component;
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
scheduler.set_timeout(&dummy_component, static_cast<uint32_t>(i % 5), 1000, []() {});
|
||||
}
|
||||
scheduler.process_to_add();
|
||||
benchmark::DoNotOptimize(scheduler);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(Scheduler_SetTimeout);
|
||||
|
||||
// --- Scheduler: set_interval registration ---
|
||||
|
||||
static void Scheduler_SetInterval(benchmark::State &state) {
|
||||
Scheduler scheduler;
|
||||
Component dummy_component;
|
||||
// Number of distinct interval keys; controls how many unique timers exist
|
||||
// simultaneously and the drain cadence for process_to_add().
|
||||
static constexpr int kKeyCount = 5;
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
scheduler.set_interval(&dummy_component, static_cast<uint32_t>(i % kKeyCount), 1000, []() {});
|
||||
// Drain to_add_ periodically to reflect production behavior where
|
||||
// process_to_add() runs each main loop iteration. Without this,
|
||||
// cancelled items accumulate in to_add_ causing O(n²) scan cost.
|
||||
if ((i + 1) % kKeyCount == 0) {
|
||||
scheduler.process_to_add();
|
||||
}
|
||||
}
|
||||
// Final drain in case kInnerIterations is not a multiple of 5
|
||||
scheduler.process_to_add();
|
||||
benchmark::DoNotOptimize(scheduler);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(Scheduler_SetInterval);
|
||||
|
||||
// --- Scheduler: defer registration (set_timeout with delay=0) ---
|
||||
|
||||
static void Scheduler_Defer(benchmark::State &state) {
|
||||
Scheduler scheduler;
|
||||
Component dummy_component;
|
||||
|
||||
// defer() is Component::defer which calls set_timeout(delay=0).
|
||||
// Call set_timeout directly since defer() is protected.
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
scheduler.set_timeout(&dummy_component, static_cast<uint32_t>(i % 5), 0, []() {});
|
||||
}
|
||||
scheduler.process_to_add();
|
||||
benchmark::DoNotOptimize(scheduler);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(Scheduler_Defer);
|
||||
|
||||
} // namespace esphome::benchmarks
|
||||
@@ -14,11 +14,63 @@ include the relevant `.cpp` and `.h` test files there.
|
||||
|
||||
### Override component code generation for testing
|
||||
|
||||
When generating code for testing, ESPHome won't invoke the component's `to_code` function, since most components do not
|
||||
need to generate configuration code for testing.
|
||||
During C++ test builds, `to_code` is suppressed for every component by default — most components do not
|
||||
need to generate configuration code for a unit test binary.
|
||||
|
||||
If you do need to generate code to for example configure compilation flags or add libraries,
|
||||
add the component name to the `CPP_TESTING_CODEGEN_COMPONENTS` allowlist in `script/cpp_unit_test.py`.
|
||||
#### Manifest overrides
|
||||
|
||||
If your component needs to customise code generation behavior for testing — for example to re-enable
|
||||
`to_code`, supply a lightweight stub, add a test-only dependency, or change any other manifest attribute —
|
||||
create an `__init__.py` in your component's test directory and define `override_manifest`:
|
||||
|
||||
**Top-level component** (`tests/components/<component>/__init__.py`):
|
||||
|
||||
```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).
|
||||
manifest.enable_codegen()
|
||||
```
|
||||
|
||||
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
|
||||
pass
|
||||
|
||||
manifest.to_code = to_code_testing
|
||||
manifest.dependencies = manifest.dependencies + ["some_test_only_dep"]
|
||||
```
|
||||
|
||||
**Platform component** (`tests/components/<component>/<domain>/__init__.py`,
|
||||
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()
|
||||
```
|
||||
|
||||
`override_manifest` receives a `ComponentManifestOverride` that wraps the real manifest.
|
||||
Attribute assignments store an override; reads fall back to the real manifest when no
|
||||
override is present.
|
||||
|
||||
Key methods:
|
||||
|
||||
| Method | Effect |
|
||||
|---|---|
|
||||
| `manifest.enable_codegen()` | Remove the `to_code` suppression, re-enabling code generation |
|
||||
| `manifest.restore()` | Clear **all** overrides, reverting every attribute to the original |
|
||||
|
||||
The function is called after `to_code` has already been set to `None`, so calling
|
||||
`enable_codegen()` is a deliberate opt-in.
|
||||
|
||||
## Running component unit tests
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
from tests.testing_helpers import ComponentManifestOverride
|
||||
|
||||
|
||||
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
# core (esphome/core/config.py) must run its to_code during C++ test builds
|
||||
# because it bootstraps the fundamental application infrastructure that all
|
||||
# components depend on (component registration, event loop, etc.).
|
||||
manifest.enable_codegen()
|
||||
@@ -1,5 +1,10 @@
|
||||
esp32:
|
||||
variant: esp32s3
|
||||
partitions:
|
||||
- name: my_data
|
||||
type: data
|
||||
subtype: spiffs
|
||||
size: 0x1000
|
||||
framework:
|
||||
type: esp-idf
|
||||
advanced:
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
from tests.testing_helpers import ComponentManifestOverride
|
||||
|
||||
|
||||
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
# host must run its to_code during C++ test builds because it sets up the
|
||||
# host platform target, which is the execution environment for all unit tests.
|
||||
manifest.enable_codegen()
|
||||
@@ -1,16 +0,0 @@
|
||||
substitutions:
|
||||
i2s_bclk_pin: GPIO15
|
||||
i2s_lrclk_pin: GPIO4
|
||||
i2s_mclk_pin: GPIO5
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
wifi:
|
||||
ssid: test
|
||||
password: test1234
|
||||
|
||||
media_player:
|
||||
- platform: i2s_audio
|
||||
name: Test Media Player
|
||||
dac_type: internal
|
||||
mode: stereo
|
||||
@@ -0,0 +1,7 @@
|
||||
from tests.testing_helpers import ComponentManifestOverride
|
||||
|
||||
|
||||
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
# logger must run its to_code during C++ test builds because it configures
|
||||
# the logging subsystem used by ESP_LOG* macros throughout component code.
|
||||
manifest.enable_codegen()
|
||||
@@ -42,6 +42,11 @@ lvgl:
|
||||
disp_bg_color: color_id
|
||||
disp_bg_image: cat_image
|
||||
disp_bg_opa: cover
|
||||
bottom_layer:
|
||||
widgets:
|
||||
- obj:
|
||||
bg_color: 0x000000
|
||||
bg_opa: cover
|
||||
theme:
|
||||
obj:
|
||||
border_width: 1
|
||||
@@ -49,12 +54,14 @@ lvgl:
|
||||
gradients:
|
||||
- id: color_bar
|
||||
direction: hor
|
||||
dither: err_diff
|
||||
# dither: err_diff
|
||||
stops:
|
||||
- color: 0xFF0000
|
||||
position: 0
|
||||
opa: 100%
|
||||
- color: 0xFFFF00
|
||||
position: 42
|
||||
opa: 80%
|
||||
- color: 0x00FF00
|
||||
position: 84
|
||||
- color: 0x00FFFF
|
||||
@@ -156,6 +163,16 @@ lvgl:
|
||||
offset_x: !lambda return 20;
|
||||
offset_y: !lambda return 20;
|
||||
antialias: !lambda return true;
|
||||
- id: msgbox_with_header_buttons
|
||||
title: Header Buttons Test
|
||||
body:
|
||||
text: Testing header buttons
|
||||
header_buttons:
|
||||
- src: cat_image
|
||||
on_click:
|
||||
logger.log: Header button clicked
|
||||
buttons:
|
||||
- text: OK
|
||||
- id: simple_msgbox
|
||||
title: Simple
|
||||
|
||||
@@ -199,11 +216,11 @@ lvgl:
|
||||
text: Unloaded
|
||||
- lvgl.label.update:
|
||||
id: msgbox_label
|
||||
text: "" # Empty text
|
||||
text: "" # Empty text
|
||||
on_all_events:
|
||||
logger.log:
|
||||
format: "Event %s"
|
||||
args: ['lv_event_code_name_for(event->code).c_str()']
|
||||
args: ['lv_event_code_name_for(event).c_str()']
|
||||
skip: true
|
||||
layout:
|
||||
type: Flex
|
||||
@@ -241,7 +258,7 @@ lvgl:
|
||||
on_all_events:
|
||||
- logger.log:
|
||||
format: "Event %s"
|
||||
args: ['lv_event_code_name_for(event->code).c_str()']
|
||||
args: ['lv_event_code_name_for(event).c_str()']
|
||||
- lvgl.animimg.update:
|
||||
id: anim_img
|
||||
src: !lambda "return {dog_image, cat_image};"
|
||||
@@ -306,7 +323,6 @@ lvgl:
|
||||
anim_time: 1s
|
||||
bg_color: light_blue
|
||||
bg_grad_color: light_blue
|
||||
bg_dither_mode: ordered
|
||||
bg_grad_dir: hor
|
||||
bg_grad_stop: 128
|
||||
bg_image_opa: transp
|
||||
@@ -354,10 +370,18 @@ lvgl:
|
||||
text_line_space: 4
|
||||
text_opa: cover
|
||||
transform_angle: 180
|
||||
transform_rotation: 90
|
||||
transform_height: 100
|
||||
transform_pivot_x: 50%
|
||||
transform_pivot_y: 50%
|
||||
transform_zoom: 0.5
|
||||
transform_scale: 2.0
|
||||
transform_scale_x: 1.5
|
||||
transform_scale_y: 0.8
|
||||
transform_skew_x: 10
|
||||
transform_skew_y: 20
|
||||
shadow_offset_x: 3
|
||||
shadow_offset_y: 3
|
||||
translate_x: 10
|
||||
translate_y: 10
|
||||
max_height: 100
|
||||
@@ -549,14 +573,16 @@ lvgl:
|
||||
arc_length: 120
|
||||
spin_time: 2s
|
||||
align: left_mid
|
||||
- spinner:
|
||||
align: right_mid
|
||||
send_draw_task_events: true
|
||||
- image:
|
||||
id: lv_image
|
||||
src: cat_image
|
||||
align: top_left
|
||||
y: "50"
|
||||
mode: real
|
||||
zoom: 2.0
|
||||
angle: 45
|
||||
scale: 2.0
|
||||
rotation: 45
|
||||
- tileview:
|
||||
id: tileview_id
|
||||
scrollbar_mode: active
|
||||
@@ -661,8 +687,11 @@ lvgl:
|
||||
src: cat_image
|
||||
x: 100
|
||||
y: 100
|
||||
angle: 90
|
||||
zoom: 2.0
|
||||
rotation: 90
|
||||
scale_x: 2.0
|
||||
scale_y: 1.5
|
||||
skew_x: 10
|
||||
skew_y: 5
|
||||
pivot_x: 25
|
||||
pivot_y: 25
|
||||
- lvgl.canvas.draw_line:
|
||||
@@ -710,6 +739,9 @@ lvgl:
|
||||
text:
|
||||
format: "A string with a number %d"
|
||||
args: ['(int)(random_uint32() % 1000)']
|
||||
size: 120
|
||||
dark_color: navy
|
||||
light_color: white
|
||||
|
||||
- slider:
|
||||
min_value: 0
|
||||
@@ -928,7 +960,6 @@ lvgl:
|
||||
grid_cell_row_pos: 0
|
||||
grid_cell_column_pos: 0
|
||||
src: !lambda return dog_image;
|
||||
mode: virtual
|
||||
on_click:
|
||||
then:
|
||||
- lvgl.tabview.select:
|
||||
@@ -1023,10 +1054,18 @@ lvgl:
|
||||
text_color: 0xFFFFFF
|
||||
scales:
|
||||
- ticks:
|
||||
width: !lambda return 1;
|
||||
width: 1
|
||||
count: 61
|
||||
length: 20%
|
||||
radial_offset: 5
|
||||
color: 0xFFFFFF
|
||||
major:
|
||||
stride: 5
|
||||
width: 2
|
||||
length: 8
|
||||
color: 0xC0C0C0
|
||||
radial_offset: 3
|
||||
label_gap: 6
|
||||
range_from: 0
|
||||
range_to: 60
|
||||
angle_range: 360
|
||||
@@ -1037,15 +1076,15 @@ lvgl:
|
||||
end_value: 60
|
||||
color_start: 0x0000bd
|
||||
color_end: 0xbd0000
|
||||
width: !lambda return 1;
|
||||
width: 1
|
||||
- line:
|
||||
opa: 50%
|
||||
id: minute_hand
|
||||
color: 0xFF0000
|
||||
r_mod: !lambda return -1;
|
||||
width: !lambda return 3;
|
||||
-
|
||||
angle_range: 330
|
||||
length: 99%
|
||||
radial_offset: 2
|
||||
width: 1
|
||||
- angle_range: 330
|
||||
rotation: 300
|
||||
range_from: 1
|
||||
range_to: 12
|
||||
@@ -1069,7 +1108,7 @@ lvgl:
|
||||
value: 180
|
||||
width: 4
|
||||
color: 0xA0A0A0
|
||||
r_mod: -20
|
||||
length: 80%
|
||||
opa: 0%
|
||||
- id: page3
|
||||
layout: Horizontal
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
wifi:
|
||||
ssid: MySSID
|
||||
password: password1
|
||||
|
||||
i2s_audio:
|
||||
i2s_lrclk_pin: 13
|
||||
i2s_bclk_pin: 14
|
||||
i2s_mclk_pin: 15
|
||||
|
||||
media_player:
|
||||
speaker:
|
||||
- platform: i2s_audio
|
||||
name: None
|
||||
dac_type: external
|
||||
id: test_speaker
|
||||
i2s_dout_pin: 18
|
||||
mute_pin: 19
|
||||
dac_type: external
|
||||
|
||||
media_player:
|
||||
- platform: speaker
|
||||
name: None
|
||||
speaker: test_speaker
|
||||
on_state:
|
||||
- media_player.play:
|
||||
- media_player.play_media: http://localhost/media.mp3
|
||||
|
||||
@@ -6,7 +6,7 @@ i2s_audio:
|
||||
microphone:
|
||||
- platform: i2s_audio
|
||||
id: mic_id_external
|
||||
i2s_din_pin: ${i2s_din_pin}
|
||||
i2s_din_pin: ${i2s_din_pin1}
|
||||
adc_type: external
|
||||
pdm: false
|
||||
mclk_multiple: 384
|
||||
@@ -15,7 +15,15 @@ microphone:
|
||||
- if:
|
||||
condition:
|
||||
- microphone.is_muted:
|
||||
id: mic_id_external
|
||||
then:
|
||||
- microphone.unmute:
|
||||
id: mic_id_external
|
||||
else:
|
||||
- microphone.mute:
|
||||
id: mic_id_external
|
||||
- platform: i2s_audio
|
||||
id: mic_id_pdm
|
||||
i2s_din_pin: ${i2s_din_pin2}
|
||||
adc_type: external
|
||||
pdm: true
|
||||
|
||||
@@ -2,21 +2,7 @@ substitutions:
|
||||
i2s_bclk_pin: GPIO15
|
||||
i2s_lrclk_pin: GPIO4
|
||||
i2s_mclk_pin: GPIO5
|
||||
i2s_din_pin: GPIO33
|
||||
i2s_din_pin1: GPIO33
|
||||
i2s_din_pin2: GPIO34
|
||||
|
||||
i2s_audio:
|
||||
i2s_bclk_pin: ${i2s_bclk_pin}
|
||||
i2s_lrclk_pin: ${i2s_lrclk_pin}
|
||||
i2s_mclk_pin: ${i2s_mclk_pin}
|
||||
use_legacy: true
|
||||
|
||||
microphone:
|
||||
- platform: i2s_audio
|
||||
id: mic_id_external
|
||||
i2s_din_pin: ${i2s_din_pin}
|
||||
adc_type: external
|
||||
pdm: false
|
||||
- platform: i2s_audio
|
||||
id: mic_id_adc
|
||||
adc_pin: 32
|
||||
adc_type: internal
|
||||
<<: !include common.yaml
|
||||
|
||||
@@ -8,6 +8,11 @@ on_beo4:
|
||||
- logger.log:
|
||||
format: "on_beo4: %u %u"
|
||||
args: ["x.source", "x.command"]
|
||||
on_brennenstuhl:
|
||||
then:
|
||||
- logger.log:
|
||||
format: "on_brennenstuhl: %u"
|
||||
args: ["x.code"]
|
||||
on_aeha:
|
||||
then:
|
||||
- logger.log:
|
||||
|
||||
@@ -14,6 +14,12 @@ button:
|
||||
remote_transmitter.transmit_beo4:
|
||||
source: 0x01
|
||||
command: 0x0C
|
||||
- platform: template
|
||||
name: brennenstuhl
|
||||
id: button_a_on
|
||||
on_press:
|
||||
remote_transmitter.transmit_brennenstuhl:
|
||||
code: 0xBD2E2C
|
||||
- platform: template
|
||||
name: Dyson fan up
|
||||
id: dyson_fan_up
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import esphome.codegen as cg
|
||||
from tests.testing_helpers import ComponentManifestOverride
|
||||
|
||||
|
||||
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
async def to_code(config):
|
||||
cg.add_build_flag("-DUSE_TIME_TIMEZONE")
|
||||
|
||||
manifest.to_code = to_code
|
||||
@@ -1821,3 +1821,151 @@ def test_component_batching_beta_branch_40_per_batch(
|
||||
all_components.extend(batch_str.split())
|
||||
assert len(all_components) == 120
|
||||
assert set(all_components) == set(component_names)
|
||||
|
||||
|
||||
# --- should_run_benchmarks tests ---
|
||||
|
||||
|
||||
def test_should_run_benchmarks_core_change() -> None:
|
||||
"""Test benchmarks trigger on core C++ file changes."""
|
||||
with patch.object(
|
||||
determine_jobs, "changed_files", return_value=["esphome/core/scheduler.cpp"]
|
||||
):
|
||||
assert determine_jobs.should_run_benchmarks() is True
|
||||
|
||||
|
||||
def test_should_run_benchmarks_core_header_change() -> None:
|
||||
"""Test benchmarks trigger on core header changes."""
|
||||
with patch.object(
|
||||
determine_jobs, "changed_files", return_value=["esphome/core/helpers.h"]
|
||||
):
|
||||
assert determine_jobs.should_run_benchmarks() is True
|
||||
|
||||
|
||||
def test_should_run_benchmarks_benchmark_infra_change() -> None:
|
||||
"""Test benchmarks trigger on benchmark infrastructure changes."""
|
||||
for infra_file in [
|
||||
"script/cpp_benchmark.py",
|
||||
"script/build_helpers.py",
|
||||
"script/setup_codspeed_lib.py",
|
||||
]:
|
||||
with patch.object(determine_jobs, "changed_files", return_value=[infra_file]):
|
||||
assert determine_jobs.should_run_benchmarks() is True, (
|
||||
f"Expected benchmarks to run for {infra_file}"
|
||||
)
|
||||
|
||||
|
||||
def test_should_run_benchmarks_benchmark_file_change() -> None:
|
||||
"""Test benchmarks trigger on benchmark file changes."""
|
||||
with patch.object(
|
||||
determine_jobs,
|
||||
"changed_files",
|
||||
return_value=["tests/benchmarks/components/api/bench_proto_encode.cpp"],
|
||||
):
|
||||
assert determine_jobs.should_run_benchmarks() is True
|
||||
|
||||
|
||||
def test_should_run_benchmarks_core_benchmark_file_change() -> None:
|
||||
"""Test benchmarks trigger on core benchmark file changes."""
|
||||
with patch.object(
|
||||
determine_jobs,
|
||||
"changed_files",
|
||||
return_value=["tests/benchmarks/core/bench_scheduler.cpp"],
|
||||
):
|
||||
assert determine_jobs.should_run_benchmarks() is True
|
||||
|
||||
|
||||
def test_should_run_benchmarks_benchmarked_component_change(tmp_path: Path) -> None:
|
||||
"""Test benchmarks trigger when a benchmarked component changes."""
|
||||
# Create a fake benchmarks directory with an 'api' component
|
||||
benchmarks_dir = tmp_path / "tests" / "benchmarks" / "components" / "api"
|
||||
benchmarks_dir.mkdir(parents=True)
|
||||
(benchmarks_dir / "bench_proto_encode.cpp").write_text("// benchmark")
|
||||
|
||||
with (
|
||||
patch.object(
|
||||
determine_jobs,
|
||||
"changed_files",
|
||||
return_value=["esphome/components/api/proto.h"],
|
||||
),
|
||||
patch.object(determine_jobs, "root_path", str(tmp_path)),
|
||||
patch.object(
|
||||
determine_jobs,
|
||||
"BENCHMARKS_COMPONENTS_PATH",
|
||||
"tests/benchmarks/components",
|
||||
),
|
||||
):
|
||||
assert determine_jobs.should_run_benchmarks() is True
|
||||
|
||||
|
||||
def test_should_run_benchmarks_non_benchmarked_component_change(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""Test benchmarks do NOT trigger for non-benchmarked component changes."""
|
||||
# Create a fake benchmarks directory with only 'api'
|
||||
benchmarks_dir = tmp_path / "tests" / "benchmarks" / "components" / "api"
|
||||
benchmarks_dir.mkdir(parents=True)
|
||||
(benchmarks_dir / "bench_proto_encode.cpp").write_text("// benchmark")
|
||||
|
||||
with (
|
||||
patch.object(
|
||||
determine_jobs,
|
||||
"changed_files",
|
||||
return_value=["esphome/components/sensor/__init__.py"],
|
||||
),
|
||||
patch.object(determine_jobs, "root_path", str(tmp_path)),
|
||||
patch.object(
|
||||
determine_jobs,
|
||||
"BENCHMARKS_COMPONENTS_PATH",
|
||||
"tests/benchmarks/components",
|
||||
),
|
||||
):
|
||||
assert determine_jobs.should_run_benchmarks() is False
|
||||
|
||||
|
||||
def test_should_run_benchmarks_no_dependency_expansion(tmp_path: Path) -> None:
|
||||
"""Test benchmarks do NOT expand to dependent components.
|
||||
|
||||
Changing 'sensor' should not trigger 'api' benchmarks even if api
|
||||
depends on sensor. This is intentional — benchmark runs should be
|
||||
targeted to directly changed components only.
|
||||
"""
|
||||
benchmarks_dir = tmp_path / "tests" / "benchmarks" / "components" / "api"
|
||||
benchmarks_dir.mkdir(parents=True)
|
||||
(benchmarks_dir / "bench_proto_encode.cpp").write_text("// benchmark")
|
||||
|
||||
with (
|
||||
patch.object(
|
||||
determine_jobs,
|
||||
"changed_files",
|
||||
# sensor is a dependency of api, but benchmarks don't expand
|
||||
return_value=["esphome/components/sensor/sensor.cpp"],
|
||||
),
|
||||
patch.object(determine_jobs, "root_path", str(tmp_path)),
|
||||
patch.object(
|
||||
determine_jobs,
|
||||
"BENCHMARKS_COMPONENTS_PATH",
|
||||
"tests/benchmarks/components",
|
||||
),
|
||||
):
|
||||
assert determine_jobs.should_run_benchmarks() is False
|
||||
|
||||
|
||||
def test_should_run_benchmarks_unrelated_change() -> None:
|
||||
"""Test benchmarks do NOT trigger for unrelated changes."""
|
||||
with patch.object(determine_jobs, "changed_files", return_value=["README.md"]):
|
||||
assert determine_jobs.should_run_benchmarks() is False
|
||||
|
||||
|
||||
def test_should_run_benchmarks_no_changes() -> None:
|
||||
"""Test benchmarks do NOT trigger with no changes."""
|
||||
with patch.object(determine_jobs, "changed_files", return_value=[]):
|
||||
assert determine_jobs.should_run_benchmarks() is False
|
||||
|
||||
|
||||
def test_should_run_benchmarks_with_branch() -> None:
|
||||
"""Test should_run_benchmarks passes branch to changed_files."""
|
||||
with patch.object(determine_jobs, "changed_files") as mock_changed:
|
||||
mock_changed.return_value = []
|
||||
determine_jobs.should_run_benchmarks("release")
|
||||
mock_changed.assert_called_with("release")
|
||||
|
||||
@@ -1073,28 +1073,6 @@ def test_get_all_dependencies_platform_component_with_dependencies() -> None:
|
||||
assert result == {"sensor.bthome", "sensor"}
|
||||
|
||||
|
||||
def test_get_all_dependencies_cpp_testing_flag() -> None:
|
||||
"""cpp_testing=True propagates to CORE.cpp_testing during resolution."""
|
||||
from esphome.core import CORE
|
||||
|
||||
with (
|
||||
patch("esphome.loader.get_component") as mock_get_component,
|
||||
patch("esphome.loader.get_platform"),
|
||||
):
|
||||
observed: list[bool] = []
|
||||
|
||||
def capturing_get_component(name: str):
|
||||
observed.append(CORE.cpp_testing)
|
||||
|
||||
mock_get_component.side_effect = capturing_get_component
|
||||
|
||||
helpers.get_all_dependencies({"some_comp"}, cpp_testing=True)
|
||||
|
||||
assert observed and all(observed), (
|
||||
"CORE.cpp_testing should be True during resolution"
|
||||
)
|
||||
|
||||
|
||||
def test_get_components_from_integration_fixtures() -> None:
|
||||
"""Test extraction of components from fixture YAML files."""
|
||||
yaml_content = {
|
||||
|
||||
@@ -0,0 +1,260 @@
|
||||
"""Unit tests for script/build_helpers.py manifest override and build helpers."""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import textwrap
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
# Add the script directory to Python path so we can import build_helpers
|
||||
sys.path.insert(
|
||||
0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "script"))
|
||||
)
|
||||
|
||||
import build_helpers # noqa: E402
|
||||
|
||||
from esphome.loader import ComponentManifest # noqa: E402
|
||||
from tests.testing_helpers import ComponentManifestOverride # noqa: E402
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _make_component_manifest(*, to_code=None, dependencies=None) -> ComponentManifest:
|
||||
mod = MagicMock()
|
||||
mod.to_code = to_code
|
||||
mod.DEPENDENCIES = dependencies or []
|
||||
return ComponentManifest(mod)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# filter_components_with_files
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_filter_keeps_components_with_cpp_files(tmp_path: Path) -> None:
|
||||
comp_dir = tmp_path / "mycomp"
|
||||
comp_dir.mkdir()
|
||||
(comp_dir / "mycomp_test.cpp").write_text("")
|
||||
|
||||
result = build_helpers.filter_components_with_files(["mycomp"], tmp_path)
|
||||
|
||||
assert result == ["mycomp"]
|
||||
|
||||
|
||||
def test_filter_keeps_components_with_h_files(tmp_path: Path) -> None:
|
||||
comp_dir = tmp_path / "mycomp"
|
||||
comp_dir.mkdir()
|
||||
(comp_dir / "helpers.h").write_text("")
|
||||
|
||||
result = build_helpers.filter_components_with_files(["mycomp"], tmp_path)
|
||||
|
||||
assert result == ["mycomp"]
|
||||
|
||||
|
||||
def test_filter_drops_components_without_test_dir(tmp_path: Path) -> None:
|
||||
result = build_helpers.filter_components_with_files(["nodir"], tmp_path)
|
||||
|
||||
assert result == []
|
||||
|
||||
|
||||
def test_filter_drops_components_with_no_cpp_or_h(tmp_path: Path) -> None:
|
||||
comp_dir = tmp_path / "mycomp"
|
||||
comp_dir.mkdir()
|
||||
(comp_dir / "README.md").write_text("")
|
||||
|
||||
result = build_helpers.filter_components_with_files(["mycomp"], tmp_path)
|
||||
|
||||
assert result == []
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# get_platform_components
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_get_platform_components_discovers_subdirectory(tmp_path: Path) -> None:
|
||||
(tmp_path / "bthome" / "sensor").mkdir(parents=True)
|
||||
|
||||
sensor_mod = MagicMock()
|
||||
sensor_mod.IS_PLATFORM_COMPONENT = True
|
||||
|
||||
with patch(
|
||||
"build_helpers.get_component", return_value=ComponentManifest(sensor_mod)
|
||||
):
|
||||
result = build_helpers.get_platform_components(["bthome"], tmp_path)
|
||||
|
||||
assert result == ["sensor.bthome"]
|
||||
|
||||
|
||||
def test_get_platform_components_skips_pycache(tmp_path: Path) -> None:
|
||||
(tmp_path / "bthome" / "__pycache__").mkdir(parents=True)
|
||||
|
||||
result = build_helpers.get_platform_components(["bthome"], tmp_path)
|
||||
|
||||
assert result == []
|
||||
|
||||
|
||||
def test_get_platform_components_raises_for_invalid_domain(tmp_path: Path) -> None:
|
||||
(tmp_path / "bthome" / "notadomain").mkdir(parents=True)
|
||||
|
||||
with (
|
||||
patch("build_helpers.get_component", return_value=None),
|
||||
pytest.raises(ValueError, match="notadomain"),
|
||||
):
|
||||
build_helpers.get_platform_components(["bthome"], tmp_path)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# load_test_manifest_overrides
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_load_suppresses_to_code(tmp_path: Path) -> None:
|
||||
"""to_code is always set to None before the override is called."""
|
||||
|
||||
async def real_to_code(config):
|
||||
pass
|
||||
|
||||
inner = _make_component_manifest(to_code=real_to_code)
|
||||
|
||||
with (
|
||||
patch("build_helpers.get_component", return_value=inner),
|
||||
patch("build_helpers.set_testing_manifest") as mock_set,
|
||||
):
|
||||
build_helpers.load_test_manifest_overrides(["mycomp"], tmp_path)
|
||||
installed: ComponentManifestOverride = mock_set.call_args[0][1]
|
||||
|
||||
assert installed.to_code is None
|
||||
|
||||
|
||||
def test_load_calls_override_fn(tmp_path: Path) -> None:
|
||||
"""override_manifest() in test_init is called with the ComponentManifestOverride."""
|
||||
comp_dir = tmp_path / "mycomp"
|
||||
comp_dir.mkdir()
|
||||
init_py = comp_dir / "__init__.py"
|
||||
init_py.write_text(
|
||||
textwrap.dedent("""\
|
||||
def override_manifest(manifest):
|
||||
manifest.dependencies = ["injected"]
|
||||
""")
|
||||
)
|
||||
|
||||
inner = _make_component_manifest()
|
||||
override = ComponentManifestOverride(inner)
|
||||
override.to_code = None
|
||||
|
||||
with (
|
||||
patch("build_helpers.get_component", return_value=inner),
|
||||
patch("build_helpers.set_testing_manifest") as mock_set,
|
||||
):
|
||||
build_helpers.load_test_manifest_overrides(["mycomp"], tmp_path)
|
||||
installed: ComponentManifestOverride = mock_set.call_args[0][1]
|
||||
|
||||
assert installed.dependencies == ["injected"]
|
||||
|
||||
|
||||
def test_load_enable_codegen_in_override(tmp_path: Path) -> None:
|
||||
"""An override_manifest that calls enable_codegen() restores to_code."""
|
||||
|
||||
async def real_to_code(config):
|
||||
pass
|
||||
|
||||
comp_dir = tmp_path / "mycomp"
|
||||
comp_dir.mkdir()
|
||||
init_py = comp_dir / "__init__.py"
|
||||
init_py.write_text(
|
||||
textwrap.dedent("""\
|
||||
def override_manifest(manifest):
|
||||
manifest.enable_codegen()
|
||||
""")
|
||||
)
|
||||
|
||||
inner = _make_component_manifest(to_code=real_to_code)
|
||||
|
||||
with (
|
||||
patch("build_helpers.get_component", return_value=inner),
|
||||
patch("build_helpers.set_testing_manifest") as mock_set,
|
||||
):
|
||||
build_helpers.load_test_manifest_overrides(["mycomp"], tmp_path)
|
||||
installed: ComponentManifestOverride = mock_set.call_args[0][1]
|
||||
|
||||
assert installed.to_code is real_to_code
|
||||
|
||||
|
||||
def test_load_no_override_file(tmp_path: Path) -> None:
|
||||
"""No override file: manifest is wrapped and to_code suppressed, nothing else."""
|
||||
inner = _make_component_manifest()
|
||||
|
||||
with (
|
||||
patch("build_helpers.get_component", return_value=inner),
|
||||
patch("build_helpers.set_testing_manifest") as mock_set,
|
||||
):
|
||||
build_helpers.load_test_manifest_overrides(["mycomp"], tmp_path)
|
||||
|
||||
mock_set.assert_called_once()
|
||||
key, installed = mock_set.call_args[0]
|
||||
assert key == "mycomp"
|
||||
assert isinstance(installed, ComponentManifestOverride)
|
||||
|
||||
|
||||
def test_load_skips_already_wrapped(tmp_path: Path) -> None:
|
||||
"""Components already wrapped as ComponentManifestOverride are not double-wrapped."""
|
||||
inner = _make_component_manifest()
|
||||
already_wrapped = ComponentManifestOverride(inner)
|
||||
|
||||
with (
|
||||
patch("build_helpers.get_component", return_value=already_wrapped),
|
||||
patch("build_helpers.set_testing_manifest") as mock_set,
|
||||
):
|
||||
build_helpers.load_test_manifest_overrides(["mycomp"], tmp_path)
|
||||
|
||||
mock_set.assert_not_called()
|
||||
|
||||
|
||||
def test_load_skips_platform_component_already_wrapped(tmp_path: Path) -> None:
|
||||
inner = _make_component_manifest()
|
||||
already_wrapped = ComponentManifestOverride(inner)
|
||||
|
||||
with (
|
||||
patch("build_helpers.get_platform", return_value=already_wrapped),
|
||||
patch("build_helpers.set_testing_manifest") as mock_set,
|
||||
):
|
||||
build_helpers.load_test_manifest_overrides(["sensor.bthome"], tmp_path)
|
||||
|
||||
mock_set.assert_not_called()
|
||||
|
||||
|
||||
def test_load_wraps_top_level_component(tmp_path: Path) -> None:
|
||||
inner = _make_component_manifest()
|
||||
|
||||
with (
|
||||
patch("build_helpers.get_component", return_value=inner),
|
||||
patch("build_helpers.set_testing_manifest") as mock_set,
|
||||
):
|
||||
build_helpers.load_test_manifest_overrides(["mycomp"], tmp_path)
|
||||
|
||||
mock_set.assert_called_once()
|
||||
key, installed = mock_set.call_args[0]
|
||||
assert key == "mycomp"
|
||||
assert isinstance(installed, ComponentManifestOverride)
|
||||
assert installed.to_code is None
|
||||
|
||||
|
||||
def test_load_wraps_platform_component(tmp_path: Path) -> None:
|
||||
inner = _make_component_manifest()
|
||||
|
||||
with (
|
||||
patch("build_helpers.get_platform", return_value=inner),
|
||||
patch("build_helpers.set_testing_manifest") as mock_set,
|
||||
):
|
||||
build_helpers.load_test_manifest_overrides(["sensor.bthome"], tmp_path)
|
||||
|
||||
mock_set.assert_called_once()
|
||||
key, installed = mock_set.call_args[0]
|
||||
assert key == "bthome.sensor"
|
||||
assert isinstance(installed, ComponentManifestOverride)
|
||||
assert installed.to_code is None
|
||||
@@ -0,0 +1,63 @@
|
||||
from typing import Any
|
||||
|
||||
from esphome.loader import ComponentManifest, _replace_component_manifest
|
||||
|
||||
|
||||
class ComponentManifestOverride:
|
||||
"""Mutable wrapper around ComponentManifest for test-specific attribute overrides.
|
||||
|
||||
When ``tests/components/<name>/__init__.py`` defines::
|
||||
|
||||
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
...
|
||||
|
||||
the function receives an instance of this class wrapping the real component
|
||||
manifest. Any attribute assignment stores an override; reads fall back to
|
||||
the underlying ``ComponentManifest`` when no override has been set.
|
||||
|
||||
Example::
|
||||
|
||||
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
async def to_code_testing(config):
|
||||
pass # lightweight no-op stub for C++ unit tests
|
||||
|
||||
manifest.to_code = to_code_testing
|
||||
manifest.dependencies = manifest.dependencies + ["extra_dep_for_tests"]
|
||||
"""
|
||||
|
||||
def __init__(self, wrapped: "ComponentManifest") -> None:
|
||||
object.__setattr__(self, "_wrapped", wrapped)
|
||||
object.__setattr__(self, "_overrides", {})
|
||||
|
||||
def __getattr__(self, name: str) -> Any:
|
||||
overrides: dict[str, Any] = object.__getattribute__(self, "_overrides")
|
||||
if name in overrides:
|
||||
return overrides[name]
|
||||
wrapped: ComponentManifest = object.__getattribute__(self, "_wrapped")
|
||||
return getattr(wrapped, name)
|
||||
|
||||
def __setattr__(self, name: str, value: Any) -> None:
|
||||
overrides: dict[str, Any] = object.__getattribute__(self, "_overrides")
|
||||
overrides[name] = value
|
||||
|
||||
def enable_codegen(self) -> None:
|
||||
"""Remove the to_code suppression, re-enabling code generation for this component.
|
||||
|
||||
Call this from ``override_manifest`` when the component needs its real (or a
|
||||
custom stub) ``to_code`` to run during C++ unit test builds.
|
||||
"""
|
||||
overrides: dict[str, Any] = object.__getattribute__(self, "_overrides")
|
||||
overrides.pop("to_code", None)
|
||||
|
||||
def restore(self) -> None:
|
||||
"""Clear all overrides, reverting to the wrapped manifest's values."""
|
||||
object.__getattribute__(self, "_overrides").clear()
|
||||
|
||||
|
||||
def set_testing_manifest(domain: str, manifest: ComponentManifestOverride) -> None:
|
||||
"""Install a testing manifest override into the component cache.
|
||||
|
||||
Called from the C++ unit test infrastructure when a component's test
|
||||
directory provides an ``override_manifest`` function.
|
||||
"""
|
||||
_replace_component_manifest(domain, manifest)
|
||||
@@ -2,7 +2,104 @@
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from esphome.loader import ComponentManifest
|
||||
from esphome.loader import ComponentManifest, _replace_component_manifest, get_component
|
||||
from tests.testing_helpers import ComponentManifestOverride
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ComponentManifestOverride
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _make_manifest(*, to_code=None, dependencies=None) -> ComponentManifest:
|
||||
"""Return a ComponentManifest backed by a minimal mock module."""
|
||||
mod = MagicMock()
|
||||
mod.to_code = to_code
|
||||
mod.DEPENDENCIES = dependencies or []
|
||||
return ComponentManifest(mod)
|
||||
|
||||
|
||||
def test_testing_manifest_delegates_to_wrapped() -> None:
|
||||
"""Unoverridden attributes fall through to the wrapped manifest."""
|
||||
inner = _make_manifest(dependencies=["wifi"])
|
||||
tm = ComponentManifestOverride(inner)
|
||||
assert tm.dependencies == ["wifi"]
|
||||
|
||||
|
||||
def test_testing_manifest_override_shadows_wrapped() -> None:
|
||||
"""An assigned attribute shadows the wrapped value."""
|
||||
inner = _make_manifest(dependencies=["wifi"])
|
||||
tm = ComponentManifestOverride(inner)
|
||||
tm.dependencies = ["ble"]
|
||||
assert tm.dependencies == ["ble"]
|
||||
# Wrapped value unchanged
|
||||
assert inner.dependencies == ["wifi"]
|
||||
|
||||
|
||||
def test_testing_manifest_to_code_suppression() -> None:
|
||||
"""Setting to_code=None suppresses code generation."""
|
||||
|
||||
async def real_to_code(config):
|
||||
pass
|
||||
|
||||
inner = _make_manifest(to_code=real_to_code)
|
||||
tm = ComponentManifestOverride(inner)
|
||||
tm.to_code = None
|
||||
assert tm.to_code is None
|
||||
|
||||
|
||||
def test_testing_manifest_enable_codegen_removes_suppression() -> None:
|
||||
"""enable_codegen() removes the to_code override, restoring the original."""
|
||||
|
||||
async def real_to_code(config):
|
||||
pass
|
||||
|
||||
inner = _make_manifest(to_code=real_to_code)
|
||||
tm = ComponentManifestOverride(inner)
|
||||
tm.to_code = None
|
||||
assert tm.to_code is None
|
||||
|
||||
tm.enable_codegen()
|
||||
assert tm.to_code is real_to_code
|
||||
|
||||
|
||||
def test_testing_manifest_enable_codegen_preserves_other_overrides() -> None:
|
||||
"""enable_codegen() only removes to_code; other overrides survive."""
|
||||
inner = _make_manifest(dependencies=["wifi"])
|
||||
tm = ComponentManifestOverride(inner)
|
||||
tm.to_code = None
|
||||
tm.dependencies = ["ble"]
|
||||
|
||||
tm.enable_codegen()
|
||||
|
||||
assert tm.to_code is inner.to_code
|
||||
assert tm.dependencies == ["ble"]
|
||||
|
||||
|
||||
def test_testing_manifest_restore_clears_all_overrides() -> None:
|
||||
"""restore() removes every override, reverting all attributes to wrapped values."""
|
||||
|
||||
async def real_to_code(config):
|
||||
pass
|
||||
|
||||
inner = _make_manifest(to_code=real_to_code, dependencies=["wifi"])
|
||||
tm = ComponentManifestOverride(inner)
|
||||
tm.to_code = None
|
||||
tm.dependencies = ["ble"]
|
||||
|
||||
tm.restore()
|
||||
|
||||
assert tm.to_code is real_to_code
|
||||
assert tm.dependencies == ["wifi"]
|
||||
|
||||
|
||||
def test_replace_component_manifest_installs_override() -> None:
|
||||
"""_replace_component_manifest replaces the cached manifest for a domain."""
|
||||
inner = _make_manifest()
|
||||
override = ComponentManifestOverride(inner)
|
||||
|
||||
_replace_component_manifest("_test_dummy_domain", override)
|
||||
|
||||
assert get_component("_test_dummy_domain") is override
|
||||
|
||||
|
||||
def test_component_manifest_resources_with_filter_source_files() -> None:
|
||||
|
||||
@@ -167,6 +167,13 @@ def mock_run_miniterm() -> Generator[Mock]:
|
||||
yield mock
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_wait_for_serial_port() -> Generator[Mock]:
|
||||
"""Mock _wait_for_serial_port for testing."""
|
||||
with patch("esphome.__main__._wait_for_serial_port") as mock:
|
||||
yield mock
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_upload_using_esptool() -> Generator[Mock]:
|
||||
"""Mock upload_using_esptool for testing."""
|
||||
@@ -1706,6 +1713,7 @@ def test_show_logs_serial(
|
||||
mock_get_port_type: Mock,
|
||||
mock_check_permissions: Mock,
|
||||
mock_run_miniterm: Mock,
|
||||
mock_wait_for_serial_port: Mock,
|
||||
) -> None:
|
||||
"""Test show_logs with serial port."""
|
||||
setup_core(config={"logger": {}}, platform=PLATFORM_ESP32)
|
||||
|
||||
@@ -6,6 +6,7 @@ import pytest
|
||||
|
||||
from esphome import core, yaml_util
|
||||
from esphome.components import substitutions
|
||||
from esphome.config_helpers import Extend, Remove
|
||||
from esphome.core import EsphomeError
|
||||
from esphome.util import OrderedDict
|
||||
|
||||
@@ -306,3 +307,128 @@ def test_dump_sort_keys() -> None:
|
||||
# nested keys should also be sorted
|
||||
assert "a_key:" in sorted_dump
|
||||
assert sorted_dump.index("a_key:") < sorted_dump.index("z_key:")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"data",
|
||||
[
|
||||
{
|
||||
"key1": "value1",
|
||||
"key2": 42,
|
||||
},
|
||||
[1, 2, 3],
|
||||
"simple string",
|
||||
],
|
||||
)
|
||||
def test_config_context_mixin(data) -> None:
|
||||
"""Test that ConfigContext mixin correctly stores and retrieves context vars in a dict."""
|
||||
|
||||
context_vars = {
|
||||
"var1": "context_value1",
|
||||
"var2": 100,
|
||||
}
|
||||
|
||||
# Add context to the data
|
||||
tagged_data = yaml_util.add_context(data, context_vars)
|
||||
|
||||
# Check that tagged_data has ConfigContext and correct vars
|
||||
assert isinstance(tagged_data, type(data))
|
||||
assert isinstance(tagged_data, yaml_util.ConfigContext)
|
||||
assert tagged_data.vars == context_vars
|
||||
|
||||
# Check that original data is preserved
|
||||
assert tagged_data == data
|
||||
|
||||
|
||||
def test_config_context_mixin_no_context() -> None:
|
||||
"""Test that add_context does not tag data when no context vars are provided."""
|
||||
data = {"key": "value"}
|
||||
|
||||
# Add context with None
|
||||
tagged_data = yaml_util.add_context(data, None)
|
||||
|
||||
# Should return original data without tagging
|
||||
assert tagged_data is data
|
||||
assert not isinstance(tagged_data, yaml_util.ConfigContext)
|
||||
|
||||
|
||||
def test_config_context_mixin_merge_contexts() -> None:
|
||||
"""Test that add_context merges new context vars with existing ones."""
|
||||
data = {"key": "value"}
|
||||
|
||||
initial_context = {
|
||||
"var1": "initial_value",
|
||||
}
|
||||
|
||||
# First, add initial context
|
||||
tagged_data = yaml_util.add_context(data, initial_context)
|
||||
|
||||
assert isinstance(tagged_data, yaml_util.ConfigContext)
|
||||
assert tagged_data.vars == initial_context
|
||||
|
||||
# Now, add more context vars
|
||||
new_context = {
|
||||
"var2": "new_value",
|
||||
"var1": "overridden_value", # This should override the initial var1
|
||||
}
|
||||
|
||||
merged_tagged_data = yaml_util.add_context(tagged_data, new_context)
|
||||
|
||||
# Check that merged_tagged_data has merged context vars
|
||||
expected_context = {
|
||||
"var1": "overridden_value",
|
||||
"var2": "new_value",
|
||||
}
|
||||
assert isinstance(merged_tagged_data, yaml_util.ConfigContext)
|
||||
assert merged_tagged_data.vars == expected_context
|
||||
|
||||
# Check that original data is preserved
|
||||
assert merged_tagged_data == data
|
||||
|
||||
|
||||
@pytest.mark.parametrize("data", [42, 3.14, True, None])
|
||||
def test_config_context_non_taggable(data) -> None:
|
||||
"""Test that add_context ignores non-string scalar values."""
|
||||
|
||||
context_vars = {
|
||||
"var1": "context_value",
|
||||
}
|
||||
|
||||
# Add context to the scalar data
|
||||
tagged_data = yaml_util.add_context(data, context_vars)
|
||||
|
||||
# Check that tagged_data has ConfigContext and correct vars
|
||||
assert not isinstance(tagged_data, yaml_util.ConfigContext)
|
||||
|
||||
# Check that original data is preserved
|
||||
assert tagged_data == data
|
||||
|
||||
|
||||
def test_config_context_defaults_only() -> None:
|
||||
"""Test that defaults: key is popped and used as context vars when no explicit vars given."""
|
||||
data = {"defaults": {"x": "1", "y": "2"}, "key": "value"}
|
||||
tagged = yaml_util.add_context(data, None)
|
||||
|
||||
assert isinstance(tagged, yaml_util.ConfigContext)
|
||||
assert tagged.vars == {"x": "1", "y": "2"}
|
||||
assert "defaults" not in tagged
|
||||
|
||||
|
||||
def test_config_context_defaults_explicit_vars_override() -> None:
|
||||
"""Test that explicit vars take precedence over defaults: values."""
|
||||
data = {"defaults": {"x": "default_x", "z": "default_z"}, "key": "value"}
|
||||
tagged = yaml_util.add_context(data, {"x": "explicit_x", "w": "explicit_w"})
|
||||
|
||||
assert isinstance(tagged, yaml_util.ConfigContext)
|
||||
assert tagged.vars == {"x": "explicit_x", "z": "default_z", "w": "explicit_w"}
|
||||
assert "defaults" not in tagged
|
||||
|
||||
|
||||
def test_represent_extend() -> None:
|
||||
"""Test that Extend objects are dumped as plain !extend scalars."""
|
||||
assert yaml_util.dump({"key": Extend("my_id")}) == "key: !extend 'my_id'\n"
|
||||
|
||||
|
||||
def test_represent_remove() -> None:
|
||||
"""Test that Remove objects are dumped as plain !remove scalars."""
|
||||
assert yaml_util.dump({"key": Remove("my_id")}) == "key: !remove 'my_id'\n"
|
||||
|
||||
Reference in New Issue
Block a user