mirror of
https://github.com/esphome/esphome.git
synced 2026-09-01 18:46:02 +00:00
Merge remote-tracking branch 'origin/rp2040-upload-improvements' into integration
This commit is contained in:
@@ -7,10 +7,23 @@
|
||||
testing binaries that combine many components. By convention, this unique namespace is `esphome::component::testing`
|
||||
(where "component" is the component under test), for example: `esphome::uart::testing`.
|
||||
|
||||
### Platform components
|
||||
|
||||
For components that expose to a platform component, create a folder under your component test folder with the platform component name, e.g. `binary_sensor` and
|
||||
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.
|
||||
|
||||
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`.
|
||||
|
||||
## Running component unit tests
|
||||
|
||||
(from the repository root)
|
||||
|
||||
```bash
|
||||
./script/cpp_unit_test.py component1 component2 ...
|
||||
```
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
#include "../common.h"
|
||||
|
||||
namespace esphome::packet_transport::testing {
|
||||
|
||||
TEST(PacketTransportBinarySensorTest, AddBinarySensor) {
|
||||
TestablePacketTransport transport;
|
||||
binary_sensor::BinarySensor bs;
|
||||
transport.add_binary_sensor("motion", &bs);
|
||||
ASSERT_EQ(transport.binary_sensors_.size(), 1u);
|
||||
EXPECT_STREQ(transport.binary_sensors_[0].id, "motion");
|
||||
EXPECT_EQ(transport.binary_sensors_[0].sensor, &bs);
|
||||
}
|
||||
|
||||
TEST(PacketTransportBinarySensorTest, AddRemoteBinarySensor) {
|
||||
TestablePacketTransport transport;
|
||||
binary_sensor::BinarySensor bs;
|
||||
transport.add_remote_binary_sensor("host1", "remote_motion", &bs);
|
||||
EXPECT_TRUE(transport.providers_.contains("host1"));
|
||||
EXPECT_EQ(transport.remote_binary_sensors_["host1"]["remote_motion"], &bs);
|
||||
}
|
||||
|
||||
TEST(PacketTransportBinarySensorTest, UnencryptedBinarySensorRoundTrip) {
|
||||
TestablePacketTransport encoder;
|
||||
encoder.init_for_test("sender");
|
||||
binary_sensor::BinarySensor local_bs;
|
||||
local_bs.state = true;
|
||||
encoder.add_binary_sensor("motion", &local_bs);
|
||||
|
||||
encoder.send_data_(true);
|
||||
ASSERT_EQ(encoder.sent_packets.size(), 1u);
|
||||
|
||||
TestablePacketTransport decoder;
|
||||
decoder.init_for_test("receiver");
|
||||
binary_sensor::BinarySensor remote_bs;
|
||||
decoder.add_remote_binary_sensor("sender", "motion", &remote_bs);
|
||||
|
||||
auto &packet = encoder.sent_packets[0];
|
||||
decoder.process_({packet.data(), packet.size()});
|
||||
EXPECT_TRUE(remote_bs.state);
|
||||
}
|
||||
|
||||
TEST(PacketTransportBinarySensorTest, MultipleSensorsRoundTrip) {
|
||||
TestablePacketTransport encoder;
|
||||
encoder.init_for_test("sender");
|
||||
|
||||
sensor::Sensor s1, s2;
|
||||
s1.state = 10.0f;
|
||||
s2.state = 20.0f;
|
||||
encoder.add_sensor("s1", &s1);
|
||||
encoder.add_sensor("s2", &s2);
|
||||
|
||||
binary_sensor::BinarySensor bs1;
|
||||
bs1.state = true;
|
||||
encoder.add_binary_sensor("bs1", &bs1);
|
||||
|
||||
encoder.send_data_(true);
|
||||
ASSERT_EQ(encoder.sent_packets.size(), 1u);
|
||||
|
||||
TestablePacketTransport decoder;
|
||||
decoder.init_for_test("receiver");
|
||||
sensor::Sensor rs1, rs2;
|
||||
binary_sensor::BinarySensor rbs1;
|
||||
rs1.state = -999.0f;
|
||||
rs2.state = -999.0f;
|
||||
decoder.add_remote_sensor("sender", "s1", &rs1);
|
||||
decoder.add_remote_sensor("sender", "s2", &rs2);
|
||||
decoder.add_remote_binary_sensor("sender", "bs1", &rbs1);
|
||||
|
||||
auto &packet = encoder.sent_packets[0];
|
||||
decoder.process_({packet.data(), packet.size()});
|
||||
|
||||
EXPECT_FLOAT_EQ(rs1.state, 10.0f);
|
||||
EXPECT_FLOAT_EQ(rs2.state, 20.0f);
|
||||
EXPECT_TRUE(rbs1.state);
|
||||
}
|
||||
|
||||
} // namespace esphome::packet_transport::testing
|
||||
@@ -1,11 +0,0 @@
|
||||
# Extra component configuration required by C++ unit tests.
|
||||
# Loaded by cpp_unit_test.py and merged into the test build config
|
||||
# before validation, so that platform defines (USE_SENSOR, etc.) are generated.
|
||||
|
||||
sensor:
|
||||
- platform: template
|
||||
id: test_cpp_sensor
|
||||
|
||||
binary_sensor:
|
||||
- platform: template
|
||||
id: test_cpp_binary_sensor
|
||||
@@ -65,198 +65,6 @@ TEST(PacketTransportTest, SetProviderEncryption) {
|
||||
EXPECT_EQ(transport.providers_["host1"].encryption_key, key);
|
||||
}
|
||||
|
||||
// --- Sensor management (requires USE_SENSOR / USE_BINARY_SENSOR) ---
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
TEST(PacketTransportTest, AddSensor) {
|
||||
TestablePacketTransport transport;
|
||||
sensor::Sensor s;
|
||||
transport.add_sensor("temp", &s);
|
||||
ASSERT_EQ(transport.sensors_.size(), 1u);
|
||||
EXPECT_STREQ(transport.sensors_[0].id, "temp");
|
||||
EXPECT_EQ(transport.sensors_[0].sensor, &s);
|
||||
EXPECT_TRUE(transport.sensors_[0].updated);
|
||||
}
|
||||
|
||||
TEST(PacketTransportTest, AddRemoteSensor) {
|
||||
TestablePacketTransport transport;
|
||||
sensor::Sensor s;
|
||||
transport.add_remote_sensor("host1", "remote_temp", &s);
|
||||
EXPECT_TRUE(transport.providers_.contains("host1"));
|
||||
EXPECT_EQ(transport.remote_sensors_["host1"]["remote_temp"], &s);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
TEST(PacketTransportTest, AddBinarySensor) {
|
||||
TestablePacketTransport transport;
|
||||
binary_sensor::BinarySensor bs;
|
||||
transport.add_binary_sensor("motion", &bs);
|
||||
ASSERT_EQ(transport.binary_sensors_.size(), 1u);
|
||||
EXPECT_STREQ(transport.binary_sensors_[0].id, "motion");
|
||||
EXPECT_EQ(transport.binary_sensors_[0].sensor, &bs);
|
||||
}
|
||||
|
||||
TEST(PacketTransportTest, AddRemoteBinarySensor) {
|
||||
TestablePacketTransport transport;
|
||||
binary_sensor::BinarySensor bs;
|
||||
transport.add_remote_binary_sensor("host1", "remote_motion", &bs);
|
||||
EXPECT_TRUE(transport.providers_.contains("host1"));
|
||||
EXPECT_EQ(transport.remote_binary_sensors_["host1"]["remote_motion"], &bs);
|
||||
}
|
||||
#endif
|
||||
|
||||
// --- Unencrypted round-trip tests (require USE_SENSOR / USE_BINARY_SENSOR) ---
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
TEST(PacketTransportTest, UnencryptedSensorRoundTrip) {
|
||||
// Encoder
|
||||
TestablePacketTransport encoder;
|
||||
encoder.init_for_test("sender");
|
||||
sensor::Sensor local_sensor;
|
||||
local_sensor.state = 42.5f;
|
||||
encoder.add_sensor("temp", &local_sensor);
|
||||
|
||||
encoder.send_data_(true);
|
||||
ASSERT_EQ(encoder.sent_packets.size(), 1u);
|
||||
|
||||
// Decoder
|
||||
TestablePacketTransport decoder;
|
||||
decoder.init_for_test("receiver");
|
||||
sensor::Sensor remote_sensor;
|
||||
remote_sensor.state = -999.0f; // sentinel
|
||||
decoder.add_remote_sensor("sender", "temp", &remote_sensor);
|
||||
|
||||
auto &packet = encoder.sent_packets[0];
|
||||
decoder.process_({packet.data(), packet.size()});
|
||||
EXPECT_FLOAT_EQ(remote_sensor.state, 42.5f);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
TEST(PacketTransportTest, UnencryptedBinarySensorRoundTrip) {
|
||||
TestablePacketTransport encoder;
|
||||
encoder.init_for_test("sender");
|
||||
binary_sensor::BinarySensor local_bs;
|
||||
local_bs.state = true;
|
||||
encoder.add_binary_sensor("motion", &local_bs);
|
||||
|
||||
encoder.send_data_(true);
|
||||
ASSERT_EQ(encoder.sent_packets.size(), 1u);
|
||||
|
||||
TestablePacketTransport decoder;
|
||||
decoder.init_for_test("receiver");
|
||||
binary_sensor::BinarySensor remote_bs;
|
||||
decoder.add_remote_binary_sensor("sender", "motion", &remote_bs);
|
||||
|
||||
auto &packet = encoder.sent_packets[0];
|
||||
decoder.process_({packet.data(), packet.size()});
|
||||
EXPECT_TRUE(remote_bs.state);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(USE_SENSOR) && defined(USE_BINARY_SENSOR)
|
||||
TEST(PacketTransportTest, MultipleSensorsRoundTrip) {
|
||||
TestablePacketTransport encoder;
|
||||
encoder.init_for_test("sender");
|
||||
|
||||
sensor::Sensor s1, s2;
|
||||
s1.state = 10.0f;
|
||||
s2.state = 20.0f;
|
||||
encoder.add_sensor("s1", &s1);
|
||||
encoder.add_sensor("s2", &s2);
|
||||
|
||||
binary_sensor::BinarySensor bs1;
|
||||
bs1.state = true;
|
||||
encoder.add_binary_sensor("bs1", &bs1);
|
||||
|
||||
encoder.send_data_(true);
|
||||
ASSERT_EQ(encoder.sent_packets.size(), 1u);
|
||||
|
||||
TestablePacketTransport decoder;
|
||||
decoder.init_for_test("receiver");
|
||||
sensor::Sensor rs1, rs2;
|
||||
binary_sensor::BinarySensor rbs1;
|
||||
rs1.state = -999.0f;
|
||||
rs2.state = -999.0f;
|
||||
decoder.add_remote_sensor("sender", "s1", &rs1);
|
||||
decoder.add_remote_sensor("sender", "s2", &rs2);
|
||||
decoder.add_remote_binary_sensor("sender", "bs1", &rbs1);
|
||||
|
||||
auto &packet = encoder.sent_packets[0];
|
||||
decoder.process_({packet.data(), packet.size()});
|
||||
|
||||
EXPECT_FLOAT_EQ(rs1.state, 10.0f);
|
||||
EXPECT_FLOAT_EQ(rs2.state, 20.0f);
|
||||
EXPECT_TRUE(rbs1.state);
|
||||
}
|
||||
#endif
|
||||
|
||||
// --- Encrypted round-trip ---
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
TEST(PacketTransportTest, EncryptedSensorRoundTrip) {
|
||||
std::vector<uint8_t> key(32);
|
||||
for (int i = 0; i < 32; i++)
|
||||
key[i] = i;
|
||||
|
||||
TestablePacketTransport encoder;
|
||||
encoder.init_for_test("sender");
|
||||
encoder.set_encryption_key(key);
|
||||
sensor::Sensor local_sensor;
|
||||
local_sensor.state = 99.9f;
|
||||
encoder.add_sensor("temp", &local_sensor);
|
||||
|
||||
encoder.send_data_(true);
|
||||
ASSERT_EQ(encoder.sent_packets.size(), 1u);
|
||||
|
||||
TestablePacketTransport decoder;
|
||||
decoder.init_for_test("receiver");
|
||||
sensor::Sensor remote_sensor;
|
||||
remote_sensor.state = -999.0f;
|
||||
decoder.add_remote_sensor("sender", "temp", &remote_sensor);
|
||||
decoder.set_provider_encryption("sender", key);
|
||||
|
||||
auto &packet = encoder.sent_packets[0];
|
||||
decoder.process_({packet.data(), packet.size()});
|
||||
EXPECT_FLOAT_EQ(remote_sensor.state, 99.9f);
|
||||
}
|
||||
|
||||
// --- Selective send ---
|
||||
|
||||
TEST(PacketTransportTest, SendDataOnlyUpdated) {
|
||||
TestablePacketTransport encoder;
|
||||
encoder.init_for_test("sender");
|
||||
|
||||
sensor::Sensor s1, s2;
|
||||
s1.state = 1.0f;
|
||||
s2.state = 2.0f;
|
||||
encoder.add_sensor("s1", &s1);
|
||||
encoder.add_sensor("s2", &s2);
|
||||
|
||||
// Mark s1 as not updated, only s2 as updated
|
||||
encoder.sensors_[0].updated = false;
|
||||
encoder.sensors_[1].updated = true;
|
||||
|
||||
encoder.send_data_(false);
|
||||
ASSERT_EQ(encoder.sent_packets.size(), 1u);
|
||||
|
||||
TestablePacketTransport decoder;
|
||||
decoder.init_for_test("receiver");
|
||||
sensor::Sensor rs1, rs2;
|
||||
rs1.state = -999.0f;
|
||||
rs2.state = -999.0f;
|
||||
decoder.add_remote_sensor("sender", "s1", &rs1);
|
||||
decoder.add_remote_sensor("sender", "s2", &rs2);
|
||||
|
||||
auto &packet = encoder.sent_packets[0];
|
||||
decoder.process_({packet.data(), packet.size()});
|
||||
|
||||
EXPECT_FLOAT_EQ(rs1.state, -999.0f); // not updated, not sent
|
||||
EXPECT_FLOAT_EQ(rs2.state, 2.0f); // updated, sent
|
||||
}
|
||||
#endif
|
||||
|
||||
// --- Ping key tests ---
|
||||
|
||||
TEST(PacketTransportTest, PingKeyStoredWhenEncrypted) {
|
||||
@@ -319,73 +127,6 @@ TEST(PacketTransportTest, PingKeyMaxLimit) {
|
||||
EXPECT_FALSE(transport.ping_keys_.contains("host4"));
|
||||
}
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
TEST(PacketTransportTest, PingKeyIncludedInTransmittedPacket) {
|
||||
std::vector<uint8_t> key(32, 0xBB);
|
||||
|
||||
// Responder: encrypted, owns a sensor
|
||||
TestablePacketTransport responder;
|
||||
responder.init_for_test("responder");
|
||||
responder.set_encryption_key(key);
|
||||
sensor::Sensor local_sensor;
|
||||
local_sensor.state = 77.7f;
|
||||
responder.add_sensor("temp", &local_sensor);
|
||||
|
||||
// Requester sends a MAGIC_PING that the responder processes
|
||||
auto ping = build_ping_packet("requester", 0xDEADBEEF);
|
||||
responder.process_({ping.data(), ping.size()});
|
||||
ASSERT_EQ(responder.ping_keys_.size(), 1u);
|
||||
|
||||
// Responder sends sensor data — ping key should be embedded
|
||||
responder.send_data_(true);
|
||||
ASSERT_EQ(responder.sent_packets.size(), 1u);
|
||||
|
||||
// Requester: encrypted provider, ping-pong enabled, expects key 0xDEADBEEF
|
||||
TestablePacketTransport requester;
|
||||
requester.init_for_test("requester");
|
||||
requester.set_ping_pong_enable(true);
|
||||
requester.ping_key_ = 0xDEADBEEF;
|
||||
sensor::Sensor remote_sensor;
|
||||
remote_sensor.state = -999.0f;
|
||||
requester.add_remote_sensor("responder", "temp", &remote_sensor);
|
||||
requester.set_provider_encryption("responder", key);
|
||||
|
||||
// The requester decrypts the packet and finds its ping key echoed back,
|
||||
// which gates the sensor data — if the key is missing, data is blocked.
|
||||
auto &packet = responder.sent_packets[0];
|
||||
requester.process_({packet.data(), packet.size()});
|
||||
EXPECT_FLOAT_EQ(remote_sensor.state, 77.7f);
|
||||
}
|
||||
|
||||
TEST(PacketTransportTest, MissingPingKeyBlocksSensorData) {
|
||||
std::vector<uint8_t> key(32, 0xBB);
|
||||
|
||||
// Responder sends data WITHOUT receiving any MAGIC_PING first — no ping keys
|
||||
TestablePacketTransport responder;
|
||||
responder.init_for_test("responder");
|
||||
responder.set_encryption_key(key);
|
||||
sensor::Sensor local_sensor;
|
||||
local_sensor.state = 77.7f;
|
||||
responder.add_sensor("temp", &local_sensor);
|
||||
responder.send_data_(true);
|
||||
ASSERT_EQ(responder.sent_packets.size(), 1u);
|
||||
|
||||
// Requester with ping-pong enabled expects a key that isn't in the packet
|
||||
TestablePacketTransport requester;
|
||||
requester.init_for_test("requester");
|
||||
requester.set_ping_pong_enable(true);
|
||||
requester.ping_key_ = 0xDEADBEEF;
|
||||
sensor::Sensor remote_sensor;
|
||||
remote_sensor.state = -999.0f;
|
||||
requester.add_remote_sensor("responder", "temp", &remote_sensor);
|
||||
requester.set_provider_encryption("responder", key);
|
||||
|
||||
auto &packet = responder.sent_packets[0];
|
||||
requester.process_({packet.data(), packet.size()});
|
||||
EXPECT_FLOAT_EQ(remote_sensor.state, -999.0f); // blocked — ping key not found
|
||||
}
|
||||
#endif
|
||||
|
||||
// --- Process error handling ---
|
||||
|
||||
TEST(PacketTransportTest, ProcessShortBuffer) {
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
#include "../common.h"
|
||||
|
||||
namespace esphome::packet_transport::testing {
|
||||
|
||||
TEST(PacketTransportSensorTest, AddSensor) {
|
||||
TestablePacketTransport transport;
|
||||
sensor::Sensor s;
|
||||
transport.add_sensor("temp", &s);
|
||||
ASSERT_EQ(transport.sensors_.size(), 1u);
|
||||
EXPECT_STREQ(transport.sensors_[0].id, "temp");
|
||||
EXPECT_EQ(transport.sensors_[0].sensor, &s);
|
||||
EXPECT_TRUE(transport.sensors_[0].updated);
|
||||
}
|
||||
|
||||
TEST(PacketTransportSensorTest, AddRemoteSensor) {
|
||||
TestablePacketTransport transport;
|
||||
sensor::Sensor s;
|
||||
transport.add_remote_sensor("host1", "remote_temp", &s);
|
||||
EXPECT_TRUE(transport.providers_.contains("host1"));
|
||||
EXPECT_EQ(transport.remote_sensors_["host1"]["remote_temp"], &s);
|
||||
}
|
||||
|
||||
TEST(PacketTransportSensorTest, UnencryptedSensorRoundTrip) {
|
||||
// Encoder
|
||||
TestablePacketTransport encoder;
|
||||
encoder.init_for_test("sender");
|
||||
sensor::Sensor local_sensor;
|
||||
local_sensor.state = 42.5f;
|
||||
encoder.add_sensor("temp", &local_sensor);
|
||||
|
||||
encoder.send_data_(true);
|
||||
ASSERT_EQ(encoder.sent_packets.size(), 1u);
|
||||
|
||||
// Decoder
|
||||
TestablePacketTransport decoder;
|
||||
decoder.init_for_test("receiver");
|
||||
sensor::Sensor remote_sensor;
|
||||
remote_sensor.state = -999.0f; // sentinel
|
||||
decoder.add_remote_sensor("sender", "temp", &remote_sensor);
|
||||
|
||||
auto &packet = encoder.sent_packets[0];
|
||||
decoder.process_({packet.data(), packet.size()});
|
||||
EXPECT_FLOAT_EQ(remote_sensor.state, 42.5f);
|
||||
}
|
||||
|
||||
TEST(PacketTransportSensorTest, EncryptedSensorRoundTrip) {
|
||||
std::vector<uint8_t> key(32);
|
||||
for (int i = 0; i < 32; i++)
|
||||
key[i] = i;
|
||||
|
||||
TestablePacketTransport encoder;
|
||||
encoder.init_for_test("sender");
|
||||
encoder.set_encryption_key(key);
|
||||
sensor::Sensor local_sensor;
|
||||
local_sensor.state = 99.9f;
|
||||
encoder.add_sensor("temp", &local_sensor);
|
||||
|
||||
encoder.send_data_(true);
|
||||
ASSERT_EQ(encoder.sent_packets.size(), 1u);
|
||||
|
||||
TestablePacketTransport decoder;
|
||||
decoder.init_for_test("receiver");
|
||||
sensor::Sensor remote_sensor;
|
||||
remote_sensor.state = -999.0f;
|
||||
decoder.add_remote_sensor("sender", "temp", &remote_sensor);
|
||||
decoder.set_provider_encryption("sender", key);
|
||||
|
||||
auto &packet = encoder.sent_packets[0];
|
||||
decoder.process_({packet.data(), packet.size()});
|
||||
EXPECT_FLOAT_EQ(remote_sensor.state, 99.9f);
|
||||
}
|
||||
|
||||
TEST(PacketTransportSensorTest, SendDataOnlyUpdated) {
|
||||
TestablePacketTransport encoder;
|
||||
encoder.init_for_test("sender");
|
||||
|
||||
sensor::Sensor s1, s2;
|
||||
s1.state = 1.0f;
|
||||
s2.state = 2.0f;
|
||||
encoder.add_sensor("s1", &s1);
|
||||
encoder.add_sensor("s2", &s2);
|
||||
|
||||
// Mark s1 as not updated, only s2 as updated
|
||||
encoder.sensors_[0].updated = false;
|
||||
encoder.sensors_[1].updated = true;
|
||||
|
||||
encoder.send_data_(false);
|
||||
ASSERT_EQ(encoder.sent_packets.size(), 1u);
|
||||
|
||||
TestablePacketTransport decoder;
|
||||
decoder.init_for_test("receiver");
|
||||
sensor::Sensor rs1, rs2;
|
||||
rs1.state = -999.0f;
|
||||
rs2.state = -999.0f;
|
||||
decoder.add_remote_sensor("sender", "s1", &rs1);
|
||||
decoder.add_remote_sensor("sender", "s2", &rs2);
|
||||
|
||||
auto &packet = encoder.sent_packets[0];
|
||||
decoder.process_({packet.data(), packet.size()});
|
||||
|
||||
EXPECT_FLOAT_EQ(rs1.state, -999.0f); // not updated, not sent
|
||||
EXPECT_FLOAT_EQ(rs2.state, 2.0f); // updated, sent
|
||||
}
|
||||
|
||||
TEST(PacketTransportSensorTest, PingKeyIncludedInTransmittedPacket) {
|
||||
std::vector<uint8_t> key(32, 0xBB);
|
||||
|
||||
// Responder: encrypted, owns a sensor
|
||||
TestablePacketTransport responder;
|
||||
responder.init_for_test("responder");
|
||||
responder.set_encryption_key(key);
|
||||
sensor::Sensor local_sensor;
|
||||
local_sensor.state = 77.7f;
|
||||
responder.add_sensor("temp", &local_sensor);
|
||||
|
||||
// Requester sends a MAGIC_PING that the responder processes
|
||||
auto ping = build_ping_packet("requester", 0xDEADBEEF);
|
||||
responder.process_({ping.data(), ping.size()});
|
||||
ASSERT_EQ(responder.ping_keys_.size(), 1u);
|
||||
|
||||
// Responder sends sensor data — ping key should be embedded
|
||||
responder.send_data_(true);
|
||||
ASSERT_EQ(responder.sent_packets.size(), 1u);
|
||||
|
||||
// Requester: encrypted provider, ping-pong enabled, expects key 0xDEADBEEF
|
||||
TestablePacketTransport requester;
|
||||
requester.init_for_test("requester");
|
||||
requester.set_ping_pong_enable(true);
|
||||
requester.ping_key_ = 0xDEADBEEF;
|
||||
sensor::Sensor remote_sensor;
|
||||
remote_sensor.state = -999.0f;
|
||||
requester.add_remote_sensor("responder", "temp", &remote_sensor);
|
||||
requester.set_provider_encryption("responder", key);
|
||||
|
||||
// The requester decrypts the packet and finds its ping key echoed back,
|
||||
// which gates the sensor data — if the key is missing, data is blocked.
|
||||
auto &packet = responder.sent_packets[0];
|
||||
requester.process_({packet.data(), packet.size()});
|
||||
EXPECT_FLOAT_EQ(remote_sensor.state, 77.7f);
|
||||
}
|
||||
|
||||
TEST(PacketTransportSensorTest, MissingPingKeyBlocksSensorData) {
|
||||
std::vector<uint8_t> key(32, 0xBB);
|
||||
|
||||
// Responder sends data WITHOUT receiving any MAGIC_PING first — no ping keys
|
||||
TestablePacketTransport responder;
|
||||
responder.init_for_test("responder");
|
||||
responder.set_encryption_key(key);
|
||||
sensor::Sensor local_sensor;
|
||||
local_sensor.state = 77.7f;
|
||||
responder.add_sensor("temp", &local_sensor);
|
||||
responder.send_data_(true);
|
||||
ASSERT_EQ(responder.sent_packets.size(), 1u);
|
||||
|
||||
// Requester with ping-pong enabled expects a key that isn't in the packet
|
||||
TestablePacketTransport requester;
|
||||
requester.init_for_test("requester");
|
||||
requester.set_ping_pong_enable(true);
|
||||
requester.ping_key_ = 0xDEADBEEF;
|
||||
sensor::Sensor remote_sensor;
|
||||
remote_sensor.state = -999.0f;
|
||||
requester.add_remote_sensor("responder", "temp", &remote_sensor);
|
||||
requester.set_provider_encryption("responder", key);
|
||||
|
||||
auto &packet = responder.sent_packets[0];
|
||||
requester.process_({packet.data(), packet.size()});
|
||||
EXPECT_FLOAT_EQ(remote_sensor.state, -999.0f); // blocked — ping key not found
|
||||
}
|
||||
|
||||
} // namespace esphome::packet_transport::testing
|
||||
@@ -102,6 +102,18 @@ uart_mock:
|
||||
0xF8, 0xF7, 0xF6, 0xF5,
|
||||
]
|
||||
|
||||
# Common filter definitions
|
||||
.sensor_filters: &sensor_filters
|
||||
filters:
|
||||
- timeout:
|
||||
timeout: 50ms
|
||||
value: last
|
||||
- throttle_with_priority: 50ms
|
||||
|
||||
.binary_filters: &binary_filters
|
||||
filters:
|
||||
- settle: 50ms
|
||||
|
||||
ld2412:
|
||||
id: ld2412_dev
|
||||
uart_id: mock_uart
|
||||
@@ -111,107 +123,56 @@ sensor:
|
||||
ld2412_id: ld2412_dev
|
||||
moving_distance:
|
||||
name: "Moving Distance"
|
||||
filters:
|
||||
- timeout:
|
||||
timeout: 50ms
|
||||
value: last
|
||||
- throttle_with_priority: 50ms
|
||||
<<: *sensor_filters
|
||||
still_distance:
|
||||
name: "Still Distance"
|
||||
filters:
|
||||
- timeout:
|
||||
timeout: 50ms
|
||||
value: last
|
||||
- throttle_with_priority: 50ms
|
||||
<<: *sensor_filters
|
||||
moving_energy:
|
||||
name: "Moving Energy"
|
||||
filters:
|
||||
- timeout:
|
||||
timeout: 50ms
|
||||
value: last
|
||||
- throttle_with_priority: 50ms
|
||||
<<: *sensor_filters
|
||||
still_energy:
|
||||
name: "Still Energy"
|
||||
filters:
|
||||
- timeout:
|
||||
timeout: 50ms
|
||||
value: last
|
||||
- throttle_with_priority: 50ms
|
||||
<<: *sensor_filters
|
||||
detection_distance:
|
||||
name: "Detection Distance"
|
||||
filters:
|
||||
- timeout:
|
||||
timeout: 50ms
|
||||
value: last
|
||||
- throttle_with_priority: 50ms
|
||||
<<: *sensor_filters
|
||||
light:
|
||||
name: "Light"
|
||||
filters:
|
||||
- timeout:
|
||||
timeout: 50ms
|
||||
value: last
|
||||
- throttle_with_priority: 50ms
|
||||
<<: *sensor_filters
|
||||
gate_0:
|
||||
move_energy:
|
||||
name: "Gate 0 Move Energy"
|
||||
filters:
|
||||
- timeout:
|
||||
timeout: 50ms
|
||||
value: last
|
||||
- throttle_with_priority: 50ms
|
||||
<<: *sensor_filters
|
||||
still_energy:
|
||||
name: "Gate 0 Still Energy"
|
||||
filters:
|
||||
- timeout:
|
||||
timeout: 50ms
|
||||
value: last
|
||||
- throttle_with_priority: 50ms
|
||||
<<: *sensor_filters
|
||||
gate_1:
|
||||
move_energy:
|
||||
name: "Gate 1 Move Energy"
|
||||
filters:
|
||||
- timeout:
|
||||
timeout: 50ms
|
||||
value: last
|
||||
- throttle_with_priority: 50ms
|
||||
<<: *sensor_filters
|
||||
still_energy:
|
||||
name: "Gate 1 Still Energy"
|
||||
filters:
|
||||
- timeout:
|
||||
timeout: 50ms
|
||||
value: last
|
||||
- throttle_with_priority: 50ms
|
||||
<<: *sensor_filters
|
||||
gate_2:
|
||||
move_energy:
|
||||
name: "Gate 2 Move Energy"
|
||||
filters:
|
||||
- timeout:
|
||||
timeout: 50ms
|
||||
value: last
|
||||
- throttle_with_priority: 50ms
|
||||
<<: *sensor_filters
|
||||
still_energy:
|
||||
name: "Gate 2 Still Energy"
|
||||
filters:
|
||||
- timeout:
|
||||
timeout: 50ms
|
||||
value: last
|
||||
- throttle_with_priority: 50ms
|
||||
<<: *sensor_filters
|
||||
|
||||
binary_sensor:
|
||||
- platform: ld2412
|
||||
ld2412_id: ld2412_dev
|
||||
has_target:
|
||||
name: "Has Target"
|
||||
filters:
|
||||
- settle: 50ms
|
||||
<<: *binary_filters
|
||||
has_moving_target:
|
||||
name: "Has Moving Target"
|
||||
filters:
|
||||
- settle: 50ms
|
||||
<<: *binary_filters
|
||||
has_still_target:
|
||||
name: "Has Still Target"
|
||||
filters:
|
||||
- settle: 50ms
|
||||
<<: *binary_filters
|
||||
|
||||
button:
|
||||
- platform: template
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
esphome:
|
||||
name: uart-mock-ld2412-eng-trunc
|
||||
|
||||
host:
|
||||
api:
|
||||
logger:
|
||||
level: VERBOSE
|
||||
|
||||
external_components:
|
||||
- source:
|
||||
type: local
|
||||
path: EXTERNAL_COMPONENT_PATH
|
||||
|
||||
# Dummy uart entry to satisfy ld2412's DEPENDENCIES = ["uart"]
|
||||
uart:
|
||||
baud_rate: 115200
|
||||
port: /dev/null
|
||||
|
||||
uart_mock:
|
||||
id: mock_uart
|
||||
baud_rate: 256000
|
||||
auto_start: false
|
||||
injections:
|
||||
# Phase 1 (t=100ms): Valid engineering mode frame (52 bytes, buffer_pos_=52)
|
||||
# Establishes baseline: gate_0_move=100, light=87
|
||||
- delay: 100ms
|
||||
inject_rx:
|
||||
[
|
||||
0xF4, 0xF3, 0xF2, 0xF1,
|
||||
0x2A, 0x00,
|
||||
0x01, 0xAA,
|
||||
0x03,
|
||||
0x1E, 0x00,
|
||||
0x64,
|
||||
0x1E, 0x00,
|
||||
0x64,
|
||||
0x00, 0x00,
|
||||
0x64, 0x41, 0x06, 0x0E, 0x2B, 0x16, 0x03, 0x03, 0x07, 0x05, 0x09, 0x08, 0x07, 0x06,
|
||||
0x00, 0x00, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x50, 0x40, 0x30, 0x20, 0x10,
|
||||
0x57,
|
||||
0x55, 0x00,
|
||||
0xF8, 0xF7, 0xF6, 0xF5,
|
||||
]
|
||||
|
||||
# Phase 2 (t=200ms): Truncated engineering mode frame (24 bytes, buffer_pos_=24)
|
||||
# This frame has data_type=0x01 (engineering) but only enough data for the
|
||||
# basic target fields, not the gate energies or light sensor.
|
||||
# buffer_pos_=24 passes the old check (>= 12) but fails the new check (< 46).
|
||||
# Without the fix, indices 17-45 would read stale buffer data from Phase 1.
|
||||
#
|
||||
# Layout (24 bytes):
|
||||
# [0-3] F4 F3 F2 F1 = data frame header
|
||||
# [4-5] 0E 00 = length 14
|
||||
# [6] 01 = data type (engineering mode)
|
||||
# [7] AA = data header marker
|
||||
# [8] 03 = target states (moving+still)
|
||||
# [9-10] 1E 00 = moving distance 30
|
||||
# [11] 50 = moving energy 80
|
||||
# [12-13] 1E 00 = still distance 30
|
||||
# [14] 50 = still energy 80
|
||||
# [15-16] FF FF = garbage detection distance bytes
|
||||
# [17] FF = padding (would be gate data in full frame)
|
||||
# [18] 55 = data footer marker (at buffer_pos_ - 6)
|
||||
# [19] 00 = check byte
|
||||
# [20-23] F8 F7 F6 F5 = data frame footer
|
||||
- delay: 100ms
|
||||
inject_rx:
|
||||
[
|
||||
0xF4, 0xF3, 0xF2, 0xF1,
|
||||
0x0E, 0x00,
|
||||
0x01, 0xAA,
|
||||
0x03,
|
||||
0x1E, 0x00,
|
||||
0x50,
|
||||
0x1E, 0x00,
|
||||
0x50,
|
||||
0xFF, 0xFF,
|
||||
0xFF,
|
||||
0x55, 0x00,
|
||||
0xF8, 0xF7, 0xF6, 0xF5,
|
||||
]
|
||||
|
||||
# Phase 3 (t=300ms): Valid recovery frame with different values
|
||||
# gate_0_move=50, light=42 — proves component recovered
|
||||
- delay: 100ms
|
||||
inject_rx:
|
||||
[
|
||||
0xF4, 0xF3, 0xF2, 0xF1,
|
||||
0x2A, 0x00,
|
||||
0x01, 0xAA,
|
||||
0x03,
|
||||
0x1E, 0x00,
|
||||
0x64,
|
||||
0x1E, 0x00,
|
||||
0x64,
|
||||
0x00, 0x00,
|
||||
0x32, 0x20, 0x06, 0x0E, 0x2B, 0x16, 0x03, 0x03, 0x07, 0x05, 0x09, 0x08, 0x07, 0x06,
|
||||
0x00, 0x00, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x28, 0x20, 0x18, 0x10, 0x08,
|
||||
0x2A,
|
||||
0x55, 0x00,
|
||||
0xF8, 0xF7, 0xF6, 0xF5,
|
||||
]
|
||||
|
||||
# Common filter definitions
|
||||
.sensor_filters: &sensor_filters
|
||||
filters:
|
||||
- timeout:
|
||||
timeout: 50ms
|
||||
value: last
|
||||
- throttle_with_priority: 50ms
|
||||
|
||||
.binary_filters: &binary_filters
|
||||
filters:
|
||||
- settle: 50ms
|
||||
|
||||
ld2412:
|
||||
id: ld2412_dev
|
||||
uart_id: mock_uart
|
||||
|
||||
sensor:
|
||||
- platform: ld2412
|
||||
ld2412_id: ld2412_dev
|
||||
moving_distance:
|
||||
name: "Moving Distance"
|
||||
<<: *sensor_filters
|
||||
still_distance:
|
||||
name: "Still Distance"
|
||||
<<: *sensor_filters
|
||||
moving_energy:
|
||||
name: "Moving Energy"
|
||||
<<: *sensor_filters
|
||||
still_energy:
|
||||
name: "Still Energy"
|
||||
<<: *sensor_filters
|
||||
detection_distance:
|
||||
name: "Detection Distance"
|
||||
<<: *sensor_filters
|
||||
light:
|
||||
name: "Light"
|
||||
<<: *sensor_filters
|
||||
gate_0:
|
||||
move_energy:
|
||||
name: "Gate 0 Move Energy"
|
||||
<<: *sensor_filters
|
||||
still_energy:
|
||||
name: "Gate 0 Still Energy"
|
||||
<<: *sensor_filters
|
||||
|
||||
binary_sensor:
|
||||
- platform: ld2412
|
||||
ld2412_id: ld2412_dev
|
||||
has_target:
|
||||
name: "Has Target"
|
||||
<<: *binary_filters
|
||||
has_moving_target:
|
||||
name: "Has Moving Target"
|
||||
<<: *binary_filters
|
||||
has_still_target:
|
||||
name: "Has Still Target"
|
||||
<<: *binary_filters
|
||||
|
||||
button:
|
||||
- platform: template
|
||||
name: "Start Scenario"
|
||||
id: start_scenario_btn
|
||||
on_press:
|
||||
- lambda: 'id(mock_uart).start_scenario();'
|
||||
@@ -14,6 +14,12 @@ test_uart_mock_ld2412_engineering (engineering mode):
|
||||
2. Multi-byte still distance (291cm) using high byte > 0
|
||||
3. Gate energy sensor values
|
||||
4. Detection distance computed from target state
|
||||
|
||||
test_uart_mock_ld2412_engineering_truncated (truncated engineering mode):
|
||||
1. Valid engineering frame establishes baseline sensor values
|
||||
2. Truncated engineering frame (24 bytes) is rejected — gate/light sensors
|
||||
must not receive garbage from stale buffer data or frame footer bytes
|
||||
3. Recovery frame with different values proves the component survived
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -273,3 +279,122 @@ async def test_uart_mock_ld2412_engineering(
|
||||
)
|
||||
|
||||
assert pytest.approx(291.0) in collector.sensor_states["detection_distance"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_uart_mock_ld2412_engineering_truncated(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Test that truncated engineering mode frames don't corrupt sensor values.
|
||||
|
||||
Without the fix, a 24-byte engineering mode frame passes the old buffer_pos_ >= 12
|
||||
check but reads indices 17-45 from stale buffer data, publishing garbage values
|
||||
(e.g. frame footer bytes 0xF8=248 as gate energy).
|
||||
"""
|
||||
external_components_path = str(
|
||||
Path(__file__).parent / "fixtures" / "external_components"
|
||||
)
|
||||
yaml_config = yaml_config.replace(
|
||||
"EXTERNAL_COMPONENT_PATH", external_components_path
|
||||
)
|
||||
|
||||
loop = asyncio.get_running_loop()
|
||||
|
||||
# Track the truncated frame warning
|
||||
truncated_warning_seen = loop.create_future()
|
||||
|
||||
def line_callback(line: str) -> None:
|
||||
if (
|
||||
"Engineering mode packet too short" in line
|
||||
and not truncated_warning_seen.done()
|
||||
):
|
||||
truncated_warning_seen.set_result(True)
|
||||
|
||||
collector = SensorStateCollector(
|
||||
sensor_names=[
|
||||
"moving_distance",
|
||||
"still_distance",
|
||||
"moving_energy",
|
||||
"still_energy",
|
||||
"detection_distance",
|
||||
"light",
|
||||
"gate_0_move_energy",
|
||||
"gate_0_still_energy",
|
||||
],
|
||||
binary_sensor_names=[
|
||||
"has_target",
|
||||
"has_moving_target",
|
||||
"has_still_target",
|
||||
],
|
||||
)
|
||||
|
||||
# Signal when we see Phase 3 recovery values (gate_0_move=50)
|
||||
recovery_received = collector.add_waiter(
|
||||
lambda: pytest.approx(50.0) in collector.sensor_states["gate_0_move_energy"]
|
||||
)
|
||||
|
||||
async with (
|
||||
run_compiled(yaml_config, line_callback=line_callback),
|
||||
api_client_connected() as client,
|
||||
):
|
||||
entities, _ = await client.list_entities_services()
|
||||
collector.build_key_mapping(entities)
|
||||
|
||||
initial_state_helper = InitialStateHelper(entities)
|
||||
client.subscribe_states(
|
||||
initial_state_helper.on_state_wrapper(collector.on_state)
|
||||
)
|
||||
|
||||
try:
|
||||
await initial_state_helper.wait_for_initial_states()
|
||||
except TimeoutError:
|
||||
pytest.fail("Timeout waiting for initial states")
|
||||
|
||||
start_btn = find_entity(entities, "start_scenario", ButtonInfo)
|
||||
assert start_btn is not None, "Start Scenario button not found"
|
||||
client.button_command(start_btn.key)
|
||||
|
||||
# Wait for Phase 1 — valid engineering frame establishes baseline
|
||||
try:
|
||||
await collector.wait_for_all(timeout=3.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(
|
||||
f"Timeout waiting for Phase 1 frame. Received:\n"
|
||||
f" sensor_states: {collector.sensor_states}\n"
|
||||
f" binary_states: {collector.binary_states}"
|
||||
)
|
||||
|
||||
# Phase 1 baseline: gate_0_move=100, light=87
|
||||
assert collector.sensor_states["gate_0_move_energy"][0] == pytest.approx(100.0)
|
||||
assert collector.sensor_states["light"][0] == pytest.approx(87.0)
|
||||
|
||||
# Wait for Phase 3 recovery frame (gate_0_move=50)
|
||||
try:
|
||||
await asyncio.wait_for(recovery_received, timeout=3.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(
|
||||
f"Timeout waiting for recovery frame. Received:\n"
|
||||
f" gate_0_move_energy: {collector.sensor_states['gate_0_move_energy']}\n"
|
||||
f" light: {collector.sensor_states['light']}"
|
||||
)
|
||||
|
||||
# Verify the truncated frame warning was logged
|
||||
assert truncated_warning_seen.done(), (
|
||||
"Expected 'Engineering mode packet too short' warning in logs"
|
||||
)
|
||||
|
||||
# Phase 3 recovery: gate_0_move=50, light=42
|
||||
assert pytest.approx(50.0) in collector.sensor_states["gate_0_move_energy"]
|
||||
assert pytest.approx(42.0) in collector.sensor_states["light"]
|
||||
|
||||
# The critical assertion: gate_0_move_energy must never have received
|
||||
# garbage values from the truncated frame. Without the fix,
|
||||
# buffer_data_[17] = 0xFF = 255 would be published as gate_0_move.
|
||||
for value in collector.sensor_states["gate_0_move_energy"]:
|
||||
assert value == pytest.approx(100.0) or value == pytest.approx(50.0), (
|
||||
f"gate_0_move_energy got unexpected value {value} — "
|
||||
f"truncated frame likely leaked stale buffer data. "
|
||||
f"All values: {collector.sensor_states['gate_0_move_energy']}"
|
||||
)
|
||||
|
||||
@@ -1027,6 +1027,73 @@ def test_get_all_dependencies_empty_set() -> None:
|
||||
assert result == set()
|
||||
|
||||
|
||||
def test_get_all_dependencies_platform_component() -> None:
|
||||
"""Platform components (domain.component) are looked up via get_platform,
|
||||
not get_component."""
|
||||
platform_comp = Mock()
|
||||
platform_comp.dependencies = []
|
||||
platform_comp.auto_load = []
|
||||
|
||||
with (
|
||||
patch("esphome.loader.get_component") as mock_get_component,
|
||||
patch("helpers.get_platform") as mock_get_platform,
|
||||
):
|
||||
mock_get_platform.return_value = platform_comp
|
||||
mock_get_component.return_value = None
|
||||
|
||||
result = helpers.get_all_dependencies({"sensor.bthome"})
|
||||
|
||||
mock_get_platform.assert_called_once_with("sensor", "bthome")
|
||||
mock_get_component.assert_not_called()
|
||||
assert result == {"sensor.bthome"}
|
||||
|
||||
|
||||
def test_get_all_dependencies_platform_component_with_dependencies() -> None:
|
||||
"""Dependencies of a platform component are resolved transitively."""
|
||||
platform_comp = Mock()
|
||||
platform_comp.dependencies = ["sensor"]
|
||||
platform_comp.auto_load = []
|
||||
|
||||
sensor_comp = Mock()
|
||||
sensor_comp.dependencies = []
|
||||
sensor_comp.auto_load = []
|
||||
|
||||
with (
|
||||
patch("esphome.loader.get_component") as mock_get_component,
|
||||
patch("helpers.get_platform") as mock_get_platform,
|
||||
):
|
||||
mock_get_platform.return_value = platform_comp
|
||||
mock_get_component.side_effect = lambda name: (
|
||||
sensor_comp if name == "sensor" else None
|
||||
)
|
||||
|
||||
result = helpers.get_all_dependencies({"sensor.bthome"})
|
||||
|
||||
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 = {
|
||||
@@ -1057,6 +1124,30 @@ def test_get_components_from_integration_fixtures() -> None:
|
||||
assert components == expected_components
|
||||
|
||||
|
||||
def test_get_components_from_integration_fixtures_skips_yaml_anchors() -> None:
|
||||
"""Test that YAML anchor keys (starting with '.') are excluded."""
|
||||
yaml_content = {
|
||||
"sensor": [{"platform": "template", "name": "test"}],
|
||||
"esphome": {"name": "test"},
|
||||
".sensor_filters": {"filters": [{"timeout": "50ms"}]},
|
||||
".binary_filters": {"filters": [{"settle": "50ms"}]},
|
||||
}
|
||||
|
||||
mock_yaml_file = Mock()
|
||||
|
||||
with (
|
||||
patch("pathlib.Path.glob") as mock_glob,
|
||||
patch("esphome.yaml_util.load_yaml", return_value=yaml_content),
|
||||
):
|
||||
mock_glob.return_value = [mock_yaml_file]
|
||||
|
||||
components = helpers.get_components_from_integration_fixtures()
|
||||
|
||||
assert ".sensor_filters" not in components
|
||||
assert ".binary_filters" not in components
|
||||
assert components == {"sensor", "esphome", "template"}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"output,expected",
|
||||
[
|
||||
|
||||
@@ -841,6 +841,18 @@ class TestEsphomeCore:
|
||||
|
||||
assert "WiFi" in target.platformio_libraries
|
||||
|
||||
def test_testing_ensure_platform_registered__sets_count(self, target):
|
||||
"""Test testing_ensure_platform_registered sets count to 1 for new platform."""
|
||||
assert target.platform_counts["sensor"] == 0
|
||||
target.testing_ensure_platform_registered("sensor")
|
||||
assert target.platform_counts["sensor"] == 1
|
||||
|
||||
def test_testing_ensure_platform_registered__does_not_overwrite(self, target):
|
||||
"""Test testing_ensure_platform_registered preserves existing count."""
|
||||
target.platform_counts["sensor"] = 3
|
||||
target.testing_ensure_platform_registered("sensor")
|
||||
assert target.platform_counts["sensor"] == 3
|
||||
|
||||
def test_add_library__extracts_short_name_from_path(self, target):
|
||||
"""Test add_library extracts short name from library paths like owner/lib."""
|
||||
target.data[const.KEY_CORE] = {
|
||||
|
||||
@@ -76,6 +76,7 @@ from esphome.const import (
|
||||
PLATFORM_RP2040,
|
||||
)
|
||||
from esphome.core import CORE, EsphomeError
|
||||
from esphome.util import BootselResult
|
||||
|
||||
|
||||
def strip_ansi_codes(text: str) -> str:
|
||||
@@ -875,7 +876,7 @@ def test_choose_upload_log_host_no_defaults_with_rp2040_bootsel(
|
||||
patch(
|
||||
"esphome.__main__._find_picotool", return_value=Path("/usr/bin/picotool")
|
||||
),
|
||||
patch("esphome.__main__.detect_rp2040_bootsel", return_value=1),
|
||||
patch("esphome.__main__.detect_rp2040_bootsel", return_value=BootselResult(1)),
|
||||
):
|
||||
result = choose_upload_log_host(
|
||||
default=None,
|
||||
@@ -898,7 +899,7 @@ def test_choose_upload_log_host_rp2040_no_device_shows_bootsel_help() -> None:
|
||||
patch(
|
||||
"esphome.__main__._find_picotool", return_value=Path("/usr/bin/picotool")
|
||||
),
|
||||
patch("esphome.__main__.detect_rp2040_bootsel", return_value=0),
|
||||
patch("esphome.__main__.detect_rp2040_bootsel", return_value=BootselResult(0)),
|
||||
pytest.raises(EsphomeError, match="BOOTSEL"),
|
||||
):
|
||||
choose_upload_log_host(
|
||||
@@ -923,7 +924,7 @@ def test_choose_upload_log_host_rp2040_bootsel_tip_with_ota(
|
||||
patch(
|
||||
"esphome.__main__._find_picotool", return_value=Path("/usr/bin/picotool")
|
||||
),
|
||||
patch("esphome.__main__.detect_rp2040_bootsel", return_value=0),
|
||||
patch("esphome.__main__.detect_rp2040_bootsel", return_value=BootselResult(0)),
|
||||
patch(
|
||||
"esphome.__main__.choose_prompt",
|
||||
return_value="192.168.1.100",
|
||||
@@ -952,7 +953,7 @@ def test_choose_upload_log_host_rp2040_bootsel_tip_with_serial_ports(
|
||||
"esphome.__main__._find_picotool",
|
||||
return_value=Path("/usr/bin/picotool"),
|
||||
),
|
||||
patch("esphome.__main__.detect_rp2040_bootsel", return_value=0),
|
||||
patch("esphome.__main__.detect_rp2040_bootsel", return_value=BootselResult(0)),
|
||||
caplog.at_level(logging.INFO, logger="esphome.__main__"),
|
||||
):
|
||||
choose_upload_log_host(
|
||||
@@ -963,6 +964,69 @@ def test_choose_upload_log_host_rp2040_bootsel_tip_with_serial_ports(
|
||||
assert "BOOTSEL" in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_no_serial_ports")
|
||||
def test_choose_upload_log_host_rp2040_permission_error_no_options(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test permission warning shown when BOOTSEL device found but not accessible."""
|
||||
setup_core(platform=PLATFORM_RP2040)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"esphome.__main__._find_picotool", return_value=Path("/usr/bin/picotool")
|
||||
),
|
||||
patch(
|
||||
"esphome.__main__.detect_rp2040_bootsel",
|
||||
return_value=BootselResult(0, permission_error=True),
|
||||
),
|
||||
patch("esphome.__main__.sys.platform", "linux"),
|
||||
pytest.raises(EsphomeError, match="BOOTSEL"),
|
||||
caplog.at_level(logging.WARNING, logger="esphome.__main__"),
|
||||
):
|
||||
choose_upload_log_host(
|
||||
default=None,
|
||||
check_default=None,
|
||||
purpose=Purpose.UPLOADING,
|
||||
)
|
||||
|
||||
assert "USB permissions" in caplog.text
|
||||
assert "udev" in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_no_serial_ports")
|
||||
def test_choose_upload_log_host_rp2040_permission_error_with_ota(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test permission warning shown with OTA fallback available."""
|
||||
setup_core(
|
||||
platform=PLATFORM_RP2040,
|
||||
config={CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}]},
|
||||
address="192.168.1.100",
|
||||
)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"esphome.__main__._find_picotool", return_value=Path("/usr/bin/picotool")
|
||||
),
|
||||
patch(
|
||||
"esphome.__main__.detect_rp2040_bootsel",
|
||||
return_value=BootselResult(0, permission_error=True),
|
||||
),
|
||||
patch(
|
||||
"esphome.__main__.choose_prompt",
|
||||
return_value="192.168.1.100",
|
||||
),
|
||||
caplog.at_level(logging.WARNING, logger="esphome.__main__"),
|
||||
):
|
||||
choose_upload_log_host(
|
||||
default=None,
|
||||
check_default=None,
|
||||
purpose=Purpose.UPLOADING,
|
||||
)
|
||||
|
||||
assert "USB permissions" in caplog.text
|
||||
|
||||
|
||||
def test_choose_upload_log_host_no_bootsel_for_non_rp2040(
|
||||
mock_no_serial_ports: Mock,
|
||||
) -> None:
|
||||
@@ -1000,7 +1064,7 @@ def test_choose_upload_log_host_rp2040_serial_and_bootsel(
|
||||
patch(
|
||||
"esphome.__main__._find_picotool", return_value=Path("/usr/bin/picotool")
|
||||
),
|
||||
patch("esphome.__main__.detect_rp2040_bootsel", return_value=1),
|
||||
patch("esphome.__main__.detect_rp2040_bootsel", return_value=BootselResult(1)),
|
||||
):
|
||||
choose_upload_log_host(
|
||||
default=None,
|
||||
|
||||
@@ -463,8 +463,9 @@ def test_detect_rp2040_bootsel_found() -> None:
|
||||
mock_result = MagicMock()
|
||||
mock_result.stdout = b"Device Information\n type: RP2040\n"
|
||||
with patch("esphome.util.subprocess.run", return_value=mock_result):
|
||||
count = util.detect_rp2040_bootsel("/usr/bin/picotool")
|
||||
assert count == 1
|
||||
result = util.detect_rp2040_bootsel("/usr/bin/picotool")
|
||||
assert result.device_count == 1
|
||||
assert result.permission_error is False
|
||||
|
||||
|
||||
def test_detect_rp2040_bootsel_multiple() -> None:
|
||||
@@ -472,8 +473,9 @@ def test_detect_rp2040_bootsel_multiple() -> None:
|
||||
mock_result = MagicMock()
|
||||
mock_result.stdout = b"type: RP2040\ntype: RP2350\n"
|
||||
with patch("esphome.util.subprocess.run", return_value=mock_result):
|
||||
count = util.detect_rp2040_bootsel("/usr/bin/picotool")
|
||||
assert count == 2
|
||||
result = util.detect_rp2040_bootsel("/usr/bin/picotool")
|
||||
assert result.device_count == 2
|
||||
assert result.permission_error is False
|
||||
|
||||
|
||||
def test_detect_rp2040_bootsel_none() -> None:
|
||||
@@ -482,16 +484,47 @@ def test_detect_rp2040_bootsel_none() -> None:
|
||||
mock_result.stdout = (
|
||||
b"No accessible RP2040/RP2350 devices in BOOTSEL mode were found.\n"
|
||||
)
|
||||
mock_result.stderr = b""
|
||||
with patch("esphome.util.subprocess.run", return_value=mock_result):
|
||||
count = util.detect_rp2040_bootsel("/usr/bin/picotool")
|
||||
assert count == 0
|
||||
result = util.detect_rp2040_bootsel("/usr/bin/picotool")
|
||||
assert result.device_count == 0
|
||||
assert result.permission_error is False
|
||||
|
||||
|
||||
def test_detect_rp2040_bootsel_permission_error() -> None:
|
||||
"""Test BOOTSEL detection with device found but not accessible."""
|
||||
mock_result = MagicMock()
|
||||
mock_result.stdout = (
|
||||
b"No accessible RP-series devices in BOOTSEL mode were found.\n"
|
||||
)
|
||||
mock_result.stderr = (
|
||||
b"RP2040 device at bus 5, address 24 appears to be in BOOTSEL mode, "
|
||||
b"but picotool was unable to connect. "
|
||||
b"Maybe try 'sudo' or check your permissions.\n"
|
||||
)
|
||||
with patch("esphome.util.subprocess.run", return_value=mock_result):
|
||||
result = util.detect_rp2040_bootsel("/usr/bin/picotool")
|
||||
assert result.device_count == 0
|
||||
assert result.permission_error is True
|
||||
|
||||
|
||||
def test_detect_rp2040_bootsel_libusb_access_error() -> None:
|
||||
"""Test BOOTSEL detection with LIBUSB_ERROR_ACCESS."""
|
||||
mock_result = MagicMock()
|
||||
mock_result.stdout = b""
|
||||
mock_result.stderr = b"LIBUSB_ERROR_ACCESS\n"
|
||||
with patch("esphome.util.subprocess.run", return_value=mock_result):
|
||||
result = util.detect_rp2040_bootsel("/usr/bin/picotool")
|
||||
assert result.device_count == 0
|
||||
assert result.permission_error is True
|
||||
|
||||
|
||||
def test_detect_rp2040_bootsel_oserror() -> None:
|
||||
"""Test BOOTSEL detection handles OSError."""
|
||||
with patch("esphome.util.subprocess.run", side_effect=OSError("not found")):
|
||||
count = util.detect_rp2040_bootsel("/usr/bin/picotool")
|
||||
assert count == 0
|
||||
result = util.detect_rp2040_bootsel("/usr/bin/picotool")
|
||||
assert result.device_count == 0
|
||||
assert result.permission_error is False
|
||||
|
||||
|
||||
def test_detect_rp2040_bootsel_timeout() -> None:
|
||||
@@ -500,8 +533,9 @@ def test_detect_rp2040_bootsel_timeout() -> None:
|
||||
"esphome.util.subprocess.run",
|
||||
side_effect=subprocess.TimeoutExpired("picotool", 10),
|
||||
):
|
||||
count = util.detect_rp2040_bootsel("/usr/bin/picotool")
|
||||
assert count == 0
|
||||
result = util.detect_rp2040_bootsel("/usr/bin/picotool")
|
||||
assert result.device_count == 0
|
||||
assert result.permission_error is False
|
||||
|
||||
|
||||
def _make_redirect(
|
||||
|
||||
Reference in New Issue
Block a user