mirror of
https://github.com/esphome/esphome.git
synced 2026-10-04 02:05:59 +00:00
Merge remote-tracking branch 'upstream/dev' into integration
This commit is contained in:
@@ -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
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
@@ -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(®);
|
||||
|
||||
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(®);
|
||||
|
||||
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(®);
|
||||
|
||||
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(®);
|
||||
|
||||
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(®);
|
||||
|
||||
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(®);
|
||||
|
||||
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(®);
|
||||
|
||||
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(®);
|
||||
|
||||
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(®);
|
||||
|
||||
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(®);
|
||||
|
||||
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
-1
@@ -1,4 +1,4 @@
|
||||
rp2040:
|
||||
rp2:
|
||||
variant: rp2040
|
||||
enable_full_printf: false
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
rp2040:
|
||||
rp2:
|
||||
variant: rp2350
|
||||
enable_full_printf: false
|
||||
|
||||
Reference in New Issue
Block a user