Files
esphome/tests/components/packet_interface/basic_fragmentation.cpp
T
clydebarrowandClaude Opus 5.5 592d1820ba [packet_interface] Add addressing and fragmentation layers
The addressing layer lets several devices share a channel such as an
RS485 bus or a radio, with 16-bit addresses. A link can carry several
peers, each with its own stack of layers and its own id. Layers that work
between two devices only are rejected with broadcast addressing. The
fragmentation layer splits packets larger than the layers below allow,
and joins them again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 08:41:59 +11:00

195 lines
6.9 KiB
C++

#include <gtest/gtest.h>
#include <cstdint>
#include <memory>
#include <vector>
#include "esphome/components/packet_interface/basic_fragmentation.h"
#include "esphome/components/packet_interface/hdlc_reliability.h"
#include "esphome/core/application.h"
namespace esphome::packet_interface::fragmentation_testing {
using Packet = std::vector<uint8_t>;
// A link layer that records what is sent, with a limited send capacity
class MockLink : public PacketInterface {
public:
MockLink() { this->set_max_packet_size(12); }
bool send_to_interface(const PacketBuffer &data, const PacketMetaData &) override {
this->sent.push_back(data.to_vector());
return true;
}
size_t get_send_capacity() const override { return this->capacity; }
void inject(const Packet &packet, uint16_t source = 0) {
PacketMetaData meta_data;
meta_data.source_address = source;
this->on_receive_from_interface_(PacketBuffer(packet), meta_data);
}
std::vector<Packet> sent;
size_t capacity{SIZE_MAX};
};
struct Peer {
explicit Peer(PacketInterface *lower) : fragmentation(lower, 100) {
this->fragmentation.add_packet_interface_listener(
[this](const PacketBuffer &data, const PacketMetaData &) { this->received.push_back(data.to_vector()); });
this->fragmentation.setup();
}
bool send(const Packet &data) { return this->fragmentation.send_to_interface(PacketBuffer(data), {}); }
BasicFragmentation fragmentation;
std::vector<Packet> received;
};
static Packet make_packet(size_t size) {
Packet packet(size);
for (size_t i = 0; i != size; i++)
packet[i] = i * 7 + 1;
return packet;
}
class TestHdlc : public HdlcReliability {
public:
using HdlcReliability::HdlcReliability;
using HdlcReliability::on_timeout_;
};
// The test main does not construct App; the reliability layer's timers need its scheduler
class BasicFragmentationTest : public ::testing::Test {
protected:
void SetUp() override {
new (&App) Application;
App.pre_setup("fragmentation-tests", 19, "", 0);
}
void TearDown() override { App.~Application(); }
// Deliver what a has sent to b
static void deliver(MockLink &from, MockLink &to) {
for (auto &packet : from.sent)
to.inject(packet);
from.sent.clear();
}
MockLink link_a, link_b;
};
TEST_F(BasicFragmentationTest, SizesAndCapability) {
Peer a(&this->link_a);
EXPECT_EQ(a.fragmentation.get_max_packet_size(), 100);
EXPECT_TRUE(a.fragmentation.has_capability(CAPABILITY_FRAGMENTATION));
EXPECT_EQ(this->link_a.top(), &a.fragmentation);
}
TEST_F(BasicFragmentationTest, SmallPacketIsOneFragment) {
Peer a(&this->link_a), b(&this->link_b);
a.send({1, 2, 3});
ASSERT_EQ(this->link_a.sent.size(), 1);
EXPECT_EQ(this->link_a.sent[0].size(), 3 + BasicFragmentation::HEADER_SIZE);
EXPECT_EQ(this->link_a.sent[0][1], 0x80); // index 0, last
deliver(this->link_a, this->link_b);
EXPECT_EQ(b.received, (std::vector<Packet>{{1, 2, 3}}));
}
TEST_F(BasicFragmentationTest, LargePacketSplitAndJoined) {
Peer a(&this->link_a), b(&this->link_b);
for (size_t size : {10, 11, 50, 100}) {
Packet packet = make_packet(size);
ASSERT_TRUE(a.send(packet));
EXPECT_EQ(this->link_a.sent.size(), (size + 9) / 10); // 10 bytes of payload in each 12 byte fragment
EXPECT_EQ(this->link_a.sent.back()[1] & 0x80, 0x80);
deliver(this->link_a, this->link_b);
EXPECT_EQ(b.received.back(), packet);
}
EXPECT_EQ(b.received.size(), 4);
}
TEST_F(BasicFragmentationTest, CompositePacketSplitAcrossFragments) {
Peer a(&this->link_a), b(&this->link_b);
Packet pre = make_packet(7), mid(13, 0xAA), post(9, 0x55);
PacketBuffer body(mid.data(), mid.size());
ASSERT_TRUE(
a.fragmentation.send_to_interface(PacketBuffer(pre.data(), pre.size(), &body, post.data(), post.size()), {}));
deliver(this->link_a, this->link_b);
Packet expected = pre;
expected.insert(expected.end(), mid.begin(), mid.end());
expected.insert(expected.end(), post.begin(), post.end());
EXPECT_EQ(b.received, std::vector<Packet>{expected});
}
TEST_F(BasicFragmentationTest, MissingFragmentDropsOnlyThatPacket) {
Peer a(&this->link_a), b(&this->link_b);
a.send(make_packet(30));
this->link_a.sent.erase(this->link_a.sent.begin() + 1);
deliver(this->link_a, this->link_b);
EXPECT_TRUE(b.received.empty());
a.send(make_packet(25));
deliver(this->link_a, this->link_b);
EXPECT_EQ(b.received, std::vector<Packet>{make_packet(25)});
}
TEST_F(BasicFragmentationTest, NotStartedWithoutRoomForAllFragments) {
Peer a(&this->link_a);
this->link_a.capacity = 2;
EXPECT_FALSE(a.send(make_packet(30))); // needs 3
EXPECT_TRUE(this->link_a.sent.empty());
EXPECT_TRUE(a.send(make_packet(20)));
EXPECT_FALSE(a.send(make_packet(101)));
}
TEST_F(BasicFragmentationTest, InterleavedSourcesNotMixed) {
Peer a(&this->link_a), c(&this->link_a), b(&this->link_b);
a.send(make_packet(20));
auto from_a = std::move(this->link_a.sent);
this->link_a.sent.clear();
c.send(make_packet(20));
auto from_c = std::move(this->link_a.sent);
// Fragments from devices 1 and 2 arrive interleaved, perhaps with the same packet number
this->link_b.inject(from_a[0], 1);
this->link_b.inject(from_c[0], 2);
this->link_b.inject(from_a[1], 1);
this->link_b.inject(from_c[1], 2);
for (auto &packet : b.received)
EXPECT_EQ(packet, make_packet(20)); // nothing mixed from the two
}
TEST_F(BasicFragmentationTest, OverReliabilityWithLosses) {
// Fragmentation above HDLC on each side, with every fourth frame from a dropped on the way; not every third, as
// each resend is a burst of three, so the same fragment would be lost every time
TestHdlc hdlc_a(&this->link_a), hdlc_b(&this->link_b);
hdlc_a.setup();
hdlc_b.setup();
// HDLC adds 3 bytes to the 12 byte link, leaving 7 bytes of payload in each fragment
Peer a(&hdlc_a), b(&hdlc_b);
Packet packet = make_packet(20); // 3 fragments, within the queue of 4
ASSERT_TRUE(a.send(packet));
int count = 0;
for (int i = 0; i != 30; i++) {
hdlc_a.loop();
hdlc_b.loop();
if (i % 5 == 4)
hdlc_a.on_timeout_();
for (auto &frame : this->link_a.sent) {
if (++count % 4 != 0)
this->link_b.inject(frame);
}
this->link_a.sent.clear();
deliver(this->link_b, this->link_a);
}
EXPECT_EQ(b.received, std::vector<Packet>{packet});
}
TEST_F(BasicFragmentationTest, OversizedSingleFragmentDropped) {
// A link packet holds more than this layer's max_packet_size
BasicFragmentation fragmentation(&this->link_a, 4);
std::vector<Packet> received;
fragmentation.add_packet_interface_listener(
[&received](const PacketBuffer &data, const PacketMetaData &) { received.push_back(data.to_vector()); });
fragmentation.setup();
this->link_a.inject({0x01, 0x80, 1, 2, 3, 4, 5});
EXPECT_TRUE(received.empty());
this->link_a.inject({0x02, 0x80, 1, 2, 3, 4});
EXPECT_EQ(received, std::vector<Packet>{Packet({1, 2, 3, 4})});
}
} // namespace esphome::packet_interface::fragmentation_testing