Merge remote-tracking branch 'upstream/dev' into integration

This commit is contained in:
J. Nick Koston
2026-07-06 20:38:00 -05:00
417 changed files with 2433 additions and 1684 deletions
+1 -2
View File
@@ -5,5 +5,4 @@ substitutions:
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
<<: !include common.yaml
cst328: !include common.yaml
@@ -0,0 +1,22 @@
esphome:
project:
name: esphome.downgrade_test
version: "1.2.3"
esp32:
variant: esp32s3
framework:
type: esp-idf
advanced:
enable_ota_downgrade_protection: true
signed_ota_verification:
signing_key: ../../components/esp32/dummy_signing_key.pem
signing_scheme: rsa3072
# wifi + ota so the IDF OTA backend compiles with USE_OTA_DOWNGRADE_PROTECTION.
wifi:
ssid: MySSID
password: password1
ota:
- platform: esphome
+1
View File
@@ -2,6 +2,7 @@ espnow:
id: espnow_component
auto_add_peer: false
channel: 1
max_payload_size: 1470
peers:
- 11:22:33:44:55:66
on_receive:
+9
View File
@@ -0,0 +1,9 @@
from tests.testing_helpers import ComponentManifestOverride
def override_manifest(manifest: ComponentManifestOverride) -> None:
# md5's to_code calls cg.add_define("USE_MD5"), which gates md5.h. C++ unit
# test builds that pull md5 in transitively (e.g. ota's host backend, which
# has an md5::MD5Digest member) need that define, otherwise md5.h compiles to
# nothing and the dependent headers fail to find md5::MD5Digest.
manifest.enable_codegen()
+106
View File
@@ -0,0 +1,106 @@
#include <gtest/gtest.h>
#include <atomic>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <span>
#include <vector>
#include "esphome/components/modbus/modbus.h"
// The allocation counters rely on AddressSanitizer's malloc hooks. The cpp_unit_test harness always
// builds with ASan, so this is exercised in CI; the fallback only applies to out-of-harness builds.
#ifndef __has_feature
#define __has_feature(x) 0
#endif
#if defined(__SANITIZE_ADDRESS__) || __has_feature(address_sanitizer)
#define HEAP_PROBE_HAS_ASAN
#endif
#ifdef HEAP_PROBE_HAS_ASAN
// Allocation counters fed by ASan's malloc hooks; sampled tightly around the calls under test.
static std::atomic<size_t> g_alloc_count{0};
static std::atomic<size_t> g_alloc_bytes{0};
static void malloc_hook(const volatile void *, size_t size) {
g_alloc_count++;
g_alloc_bytes += size;
}
static void free_hook(const volatile void *) {}
extern "C" int __sanitizer_install_malloc_and_free_hooks(void (*malloc_hook)(const volatile void *, size_t),
void (*free_hook)(const volatile void *));
[[maybe_unused]] static const int g_hooks_installed = __sanitizer_install_malloc_and_free_hooks(malloc_hook, free_hook);
namespace esphome::modbus::testing {
namespace {
struct Sample {
size_t count;
size_t bytes;
};
template<typename F> Sample sample(F &&f) {
size_t c0 = g_alloc_count.load(), b0 = g_alloc_bytes.load();
f();
return {g_alloc_count.load() - c0, g_alloc_bytes.load() - b0};
}
} // namespace
// Typical frames (reads and single-register/coil writes are exactly address + 5-byte PDU + CRC = 8
// bytes) fit the SmallInlineBuffer and are built with zero heap allocations; only larger frames spill
// to a single allocation.
TEST(HeapProbe, TypicalFrameConstructionIsAllocationFree) {
const uint8_t read_pdu[] = {0x03, 0x01, 0x00, 0x00, 0x02}; // 5 bytes -> 8-byte frame, inline
Sample typical = sample([&] {
ModbusFrame frame(0x02, read_pdu, sizeof(read_pdu));
(void) frame;
});
printf("HEAPPROBE frame_typical count=%zu bytes=%zu\n", typical.count, typical.bytes);
EXPECT_EQ(typical.count, 0u);
uint8_t large_pdu[250] = {0x10}; // multi-register write -> 253-byte frame, spills once
Sample large = sample([&] {
ModbusFrame frame(0x02, large_pdu, sizeof(large_pdu));
(void) frame;
});
printf("HEAPPROBE frame_large count=%zu bytes=%zu\n", large.count, large.bytes);
EXPECT_EQ(large.count, 1u);
}
// Queueing typical commands is fully allocation-free: the frame fits the inline buffer and the tx
// deque's first block is already allocated when the hub is constructed. (A queue deeper than one
// deque block - roughly a dozen commands - would allocate further blocks.)
TEST(HeapProbe, QueueingTypicalCommandsIsAllocationFree) {
ModbusClientHub hub;
ModbusClientDevice device(&hub, 0x02);
StaticVector<uint8_t, MAX_PDU_SIZE> req;
const uint8_t read_pdu[] = {0x03, 0x01, 0x00, 0x00, 0x02};
req.assign(read_pdu, read_pdu + sizeof(read_pdu));
constexpr int n = 12;
size_t total = 0;
for (int i = 0; i != n; i++) {
total += sample([&] { device.send_pdu(req); }).count;
}
printf("HEAPPROBE queue_%d_typical_commands total_allocs=%zu\n", n, total);
EXPECT_EQ(total, 0u);
}
} // namespace esphome::modbus::testing
#else // !HEAP_PROBE_HAS_ASAN
namespace esphome::modbus::testing {
TEST(HeapProbe, TypicalFrameConstructionIsAllocationFree) {
GTEST_SKIP() << "allocation counting requires an AddressSanitizer build";
}
} // namespace esphome::modbus::testing
#endif // HEAP_PROBE_HAS_ASAN
@@ -29,7 +29,7 @@ TEST(ModbusServerWrite, SingleWordSucceeds) {
};
server.add_server_register(&reg);
auto status = server.on_modbus_write_registers(0x0000, make_registers({0x1234}));
auto status = server.on_write_registers(0x0000, make_registers({0x1234}));
EXPECT_FALSE(status.has_value()); // nullopt == success
EXPECT_EQ(written, 0x1234);
}
@@ -45,7 +45,7 @@ TEST(ModbusServerWrite, DwordSucceeds) {
};
server.add_server_register(&reg);
auto status = server.on_modbus_write_registers(0x0000, make_registers({0x1234, 0x5678}));
auto status = server.on_write_registers(0x0000, make_registers({0x1234, 0x5678}));
EXPECT_FALSE(status.has_value());
EXPECT_EQ(written, 0x12345678);
}
@@ -70,7 +70,7 @@ TEST(ModbusServerWrite, UnderSuppliedValueAppliesNothing) {
server.add_server_register(&dword_reg);
// Two words supplied: one for the WORD at 0x0000, but only one of the two the DWORD at 0x0001 needs.
auto status = server.on_modbus_write_registers(0x0000, make_registers({0x1111, 0x2222}));
auto status = server.on_write_registers(0x0000, make_registers({0x1111, 0x2222}));
ASSERT_TRUE(status.has_value());
if (status.has_value())
EXPECT_EQ(status.value(), ModbusExceptionCode::ILLEGAL_DATA_VALUE);
@@ -84,7 +84,7 @@ TEST(ModbusServerWrite, UnwritableRegisterRejected) {
ServerRegister read_only(0x0000, SensorValueType::U_WORD, 1); // no write_lambda set
server.add_server_register(&read_only);
auto status = server.on_modbus_write_registers(0x0000, make_registers({0x1234}));
auto status = server.on_write_registers(0x0000, make_registers({0x1234}));
ASSERT_TRUE(status.has_value());
if (status.has_value())
EXPECT_EQ(status.value(), ModbusExceptionCode::ILLEGAL_DATA_ADDRESS);
@@ -93,7 +93,7 @@ TEST(ModbusServerWrite, UnwritableRegisterRejected) {
// An address with no registered register yields ILLEGAL_DATA_ADDRESS.
TEST(ModbusServerWrite, UnmatchedAddressRejected) {
ModbusServer server;
auto status = server.on_modbus_write_registers(0x0005, make_registers({0x1234}));
auto status = server.on_write_registers(0x0005, make_registers({0x1234}));
ASSERT_TRUE(status.has_value());
if (status.has_value())
EXPECT_EQ(status.value(), ModbusExceptionCode::ILLEGAL_DATA_ADDRESS);
@@ -114,14 +114,14 @@ TEST(ModbusServerWrite, CallbackFailureIsServiceDeviceFailure) {
server.add_server_register(&first);
server.add_server_register(&second);
auto status = server.on_modbus_write_registers(0x0000, make_registers({0xAAAA, 0xBBBB}));
auto status = server.on_write_registers(0x0000, make_registers({0xAAAA, 0xBBBB}));
ASSERT_TRUE(status.has_value());
if (status.has_value())
EXPECT_EQ(status.value(), ModbusExceptionCode::SERVICE_DEVICE_FAILURE);
EXPECT_TRUE(first_written); // pre-validation passed, so the first write applied before the failure
}
// --- on_modbus_read_registers --------------------------------------------------
// --- on_read_registers --------------------------------------------------
TEST(ModbusServerRead, SingleWordSucceeds) {
ModbusServer server;
@@ -130,7 +130,7 @@ TEST(ModbusServerRead, SingleWordSucceeds) {
server.add_server_register(&reg);
RegisterValues out;
auto status = server.on_modbus_read_registers(0x0000, 1, out);
auto status = server.on_read_registers(0x0000, 1, out);
EXPECT_FALSE(status.has_value());
ASSERT_EQ(out.size(), 1u);
EXPECT_EQ(out[0], 0x1234);
@@ -143,7 +143,7 @@ TEST(ModbusServerRead, DwordReturnsTwoWordsHighFirst) {
server.add_server_register(&reg);
RegisterValues out;
auto status = server.on_modbus_read_registers(0x0000, 2, out);
auto status = server.on_read_registers(0x0000, 2, out);
EXPECT_FALSE(status.has_value());
ASSERT_EQ(out.size(), 2u);
EXPECT_EQ(out[0], 0x1234);
@@ -165,7 +165,7 @@ TEST(ModbusServerRead, StartInsideValueRejected) {
server.add_server_register(&reg);
RegisterValues out;
auto status = server.on_modbus_read_registers(0x0011, 1, out); // the second cell of the DWORD
auto status = server.on_read_registers(0x0011, 1, out); // the second cell of the DWORD
ASSERT_TRUE(status.has_value());
if (status.has_value())
EXPECT_EQ(status.value(), ModbusExceptionCode::ILLEGAL_DATA_ADDRESS);
@@ -184,7 +184,7 @@ TEST(ModbusServerRead, ClippedTailRejected) {
server.add_server_register(&reg);
RegisterValues out;
auto status = server.on_modbus_read_registers(0x0000, 1, out); // only 1 of the DWORD's 2 registers
auto status = server.on_read_registers(0x0000, 1, out); // only 1 of the DWORD's 2 registers
ASSERT_TRUE(status.has_value());
if (status.has_value())
EXPECT_EQ(status.value(), ModbusExceptionCode::ILLEGAL_DATA_ADDRESS);
@@ -200,7 +200,7 @@ TEST(ModbusServerRead, WriteOnlyRegisterRejected) {
server.add_server_register(&reg);
RegisterValues out;
auto status = server.on_modbus_read_registers(0x0000, 1, out);
auto status = server.on_read_registers(0x0000, 1, out);
ASSERT_TRUE(status.has_value());
if (status.has_value())
EXPECT_EQ(status.value(), ModbusExceptionCode::ILLEGAL_DATA_ADDRESS);
@@ -213,7 +213,7 @@ TEST(ModbusServerRead, CourtesyDefaultForUnregistered) {
ServerCourtesyResponse{.enabled = true, .register_last_address = 0xFFFF, .register_value = 0xABCD});
RegisterValues out;
auto status = server.on_modbus_read_registers(0x0005, 2, out);
auto status = server.on_read_registers(0x0005, 2, out);
EXPECT_FALSE(status.has_value());
ASSERT_EQ(out.size(), 2u);
EXPECT_EQ(out[0], 0xABCD);
@@ -224,7 +224,7 @@ TEST(ModbusServerRead, CourtesyDefaultForUnregistered) {
TEST(ModbusServerRead, UnregisteredRejectedWithoutCourtesy) {
ModbusServer server;
RegisterValues out;
auto status = server.on_modbus_read_registers(0x0005, 1, out);
auto status = server.on_read_registers(0x0005, 1, out);
ASSERT_TRUE(status.has_value());
if (status.has_value())
EXPECT_EQ(status.value(), ModbusExceptionCode::ILLEGAL_DATA_ADDRESS);
@@ -241,7 +241,7 @@ TEST(ModbusServerRead, PartialReadHighWord) {
server.add_server_register(&reg);
RegisterValues out;
auto status = server.on_modbus_read_registers(0x0010, 1, out);
auto status = server.on_read_registers(0x0010, 1, out);
EXPECT_FALSE(status.has_value());
ASSERT_EQ(out.size(), 1u);
EXPECT_EQ(out[0], 0x1234);
@@ -256,7 +256,7 @@ TEST(ModbusServerRead, PartialReadLowWordFromInterior) {
server.add_server_register(&reg);
RegisterValues out;
auto status = server.on_modbus_read_registers(0x0011, 1, out);
auto status = server.on_read_registers(0x0011, 1, out);
EXPECT_FALSE(status.has_value());
ASSERT_EQ(out.size(), 1u);
EXPECT_EQ(out[0], 0x5678);
@@ -272,12 +272,12 @@ TEST(ModbusServerRead, PartialReadReversedType) {
server.add_server_register(&reg);
RegisterValues first;
ASSERT_FALSE(server.on_modbus_read_registers(0x0010, 1, first).has_value());
ASSERT_FALSE(server.on_read_registers(0x0010, 1, first).has_value());
ASSERT_EQ(first.size(), 1u);
EXPECT_EQ(first[0], 0x5678);
RegisterValues second;
ASSERT_FALSE(server.on_modbus_read_registers(0x0011, 1, second).has_value());
ASSERT_FALSE(server.on_read_registers(0x0011, 1, second).has_value());
ASSERT_EQ(second.size(), 1u);
EXPECT_EQ(second[0], 0x1234);
}
@@ -0,0 +1 @@
nrf52:
@@ -0,0 +1,52 @@
#include <gtest/gtest.h>
#include "esphome/components/ota/ota_backend.h"
namespace esphome::ota::testing {
// version_is_older(candidate, reference) == true means candidate is a downgrade
// and should be rejected.
TEST(VersionIsOlder, PatchOlder) {
EXPECT_TRUE(version_is_older("1.2.3", "1.2.4"));
EXPECT_FALSE(version_is_older("1.2.4", "1.2.3"));
}
TEST(VersionIsOlder, NumericNotLexical) {
// "1.10.0" is newer than "1.9.0" even though '1' < '9' lexically.
EXPECT_TRUE(version_is_older("1.9.0", "1.10.0"));
EXPECT_FALSE(version_is_older("1.10.0", "1.9.0"));
}
TEST(VersionIsOlder, MajorMinor) {
EXPECT_TRUE(version_is_older("1.9.9", "2.0.0"));
EXPECT_TRUE(version_is_older("1.2.9", "1.3.0"));
EXPECT_FALSE(version_is_older("2.0.0", "1.9.9"));
}
TEST(VersionIsOlder, EqualVersionsAllowed) {
// Re-flashing the same version must be permitted.
EXPECT_FALSE(version_is_older("1.2.3", "1.2.3"));
EXPECT_FALSE(version_is_older("2024.1.0", "2024.1.0"));
}
TEST(VersionIsOlder, DifferingComponentCounts) {
// Missing trailing components count as 0.
EXPECT_FALSE(version_is_older("1.2", "1.2.0"));
EXPECT_FALSE(version_is_older("1.2.0", "1.2"));
EXPECT_TRUE(version_is_older("1.2", "1.2.1"));
EXPECT_FALSE(version_is_older("1.2.1", "1.2"));
}
TEST(VersionIsOlder, CalendarVersions) {
EXPECT_TRUE(version_is_older("2024.12.0", "2025.1.0"));
EXPECT_FALSE(version_is_older("2025.1.0", "2024.12.0"));
}
TEST(VersionIsOlder, NullInputsAreSafe) {
EXPECT_FALSE(version_is_older(nullptr, "1.2.3"));
EXPECT_FALSE(version_is_older("1.2.3", nullptr));
EXPECT_FALSE(version_is_older(nullptr, nullptr));
}
} // namespace esphome::ota::testing
@@ -1,4 +1,4 @@
rp2040:
rp2:
variant: rp2040
enable_full_printf: false
@@ -1,4 +1,4 @@
rp2040:
rp2:
variant: rp2350
enable_full_printf: false