Files
esphome/tests/components/packet_interface/hdlc_reliability.cpp
T
clydebarrowandClaude Code 0fe7d81a56 [packet_interface] Answer a repeated SABM without resetting the link
A peer that has not heard the UA repeats its SABM, and cannot have sent a
numbered frame meanwhile. Treating that as a restart threw away queued
frames and reported the session lost for a single dropped UA. A SABM is
now a restart only if the peer has sent a numbered frame since the link
came up.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-19 14:40:33 +11:00

465 lines
14 KiB
C++

#include <gtest/gtest.h>
#include <cstdint>
#include <functional>
#include <memory>
#include <vector>
#include "esphome/components/packet_interface/hdlc_reliability.h"
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
namespace esphome::packet_interface::testing {
// Copies a packet into a vector, for comparing in tests
static std::vector<uint8_t> to_vector(const esphome::packet_interface::PacketBuffer &data) {
std::vector<uint8_t> result(data.size());
data.copy_to(result.data(), result.size());
return result;
}
using Frame = std::vector<uint8_t>;
// A link layer that records what is sent, so a test can deliver it to the peer
class MockLink : public PacketInterface {
public:
explicit MockLink(size_t max_packet_size = 64) : PacketInterface(max_packet_size) {}
bool send_to_interface(const PacketBuffer &data, const PacketMetaData &) override {
if (this->budget == 0) {
this->refused++;
return false;
}
this->budget--;
this->sent.push_back(to_vector(data));
return true;
}
void inject(const Frame &frame) { this->on_receive_from_interface_(PacketBuffer(frame), {}); }
std::vector<Frame> sent;
size_t budget{SIZE_MAX}; // frames accepted before the link refuses, like a full network stack
size_t refused{0};
};
class TestHdlc : public HdlcReliability {
public:
using HdlcReliability::HdlcReliability;
using HdlcReliability::on_timeout_;
using HdlcReliability::outstanding_;
};
struct Peer {
explicit Peer(uint8_t queue_length = 4) {
this->hdlc = std::make_unique<TestHdlc>(&this->link);
this->hdlc->set_max_queue_length(queue_length);
this->hdlc->set_max_retries(3);
this->hdlc->setup();
this->hdlc->add_packet_interface_listener(
[this](const PacketBuffer &data, const PacketMetaData &) { this->received.push_back(to_vector(data)); });
this->hdlc->add_on_session_state_callback([this](bool connected) { this->states.push_back(connected); });
}
bool send(const Frame &data) { return this->hdlc->send_to_interface(PacketBuffer(data), {}); }
MockLink link;
std::unique_ptr<TestHdlc> hdlc;
std::vector<Frame> received;
std::vector<bool> states;
};
// Returns true to drop a frame on its way from one peer to the other
using DropFilter = std::function<bool(const Frame &)>;
static bool is_info(const Frame &frame) { return (frame[0] & 1) == 0; }
static bool is_ack(const Frame &frame) { return (frame[0] & 0x0F) == 0x01; }
// Runs a layer's loop only while it has asked for it, as the main loop does
static void run_loop(TestHdlc &hdlc) {
if (!hdlc.is_idle())
hdlc.loop();
}
// Move frames between the peers and run their loops until nothing more is sent
static void pump(Peer &a, Peer &b, const DropFilter &drop_a_to_b = nullptr, const DropFilter &drop_b_to_a = nullptr) {
for (int i = 0; i != 50; i++) {
run_loop(*a.hdlc);
run_loop(*b.hdlc);
if (a.link.sent.empty() && b.link.sent.empty())
return;
auto from_a = std::move(a.link.sent);
auto from_b = std::move(b.link.sent);
a.link.sent.clear();
b.link.sent.clear();
for (auto &frame : from_a) {
if (!drop_a_to_b || !drop_a_to_b(frame))
b.link.inject(frame);
}
for (auto &frame : from_b) {
if (!drop_b_to_a || !drop_b_to_a(frame))
a.link.inject(frame);
}
}
FAIL() << "Peers did not go quiet";
}
static Frame make_frame(uint8_t control, const Frame &payload) {
Frame frame{control};
frame.insert(frame.end(), payload.begin(), payload.end());
uint16_t crc = crc16(frame.data(), frame.size());
frame.push_back(crc & 0xFF);
frame.push_back(crc >> 8);
return frame;
}
// Each test replaces App with a fresh one; the retransmit timer needs its scheduler
class HdlcReliabilityTest : public ::testing::Test {
protected:
void SetUp() override {
App.~Application();
new (&App) Application();
static char name[] = "hdlc-tests";
static char friendly_name[] = "";
App.pre_setup(name, sizeof(name) - 1, friendly_name, 0);
}
void TearDown() override {
App.~Application();
new (&App) Application(); // later tests and the test listener use App
}
};
TEST_F(HdlcReliabilityTest, MaxPacketSizeAllowsForOverhead) {
Peer a;
EXPECT_EQ(a.hdlc->get_send_capacity(), 4);
EXPECT_EQ(a.hdlc->get_max_packet_size(), 64 - HdlcReliability::FRAME_OVERHEAD);
EXPECT_EQ(a.link.top(), a.hdlc.get());
EXPECT_EQ(a.hdlc->get_capabilities(), CAPABILITY_RELIABILITY);
}
TEST_F(HdlcReliabilityTest, ResetHandshake) {
Peer a, b;
a.hdlc->loop();
ASSERT_EQ(a.link.sent.size(), 1);
EXPECT_EQ(a.link.sent[0], make_frame(0x2F, {})); // SABM
pump(a, b);
EXPECT_TRUE(a.hdlc->is_connected());
EXPECT_TRUE(b.hdlc->is_connected());
}
TEST_F(HdlcReliabilityTest, RepeatedSabmBeforeNumberedFramesKeepsQueue) {
Peer a, b;
pump(a, b);
// A frame is queued and sent, but has not reached A, so A has sent no numbered frame
ASSERT_TRUE(b.send({0x42}));
auto held = std::move(b.link.sent);
b.link.sent.clear();
EXPECT_EQ(b.hdlc->get_send_capacity(), 3);
// A repeats its SABM because it did not hear the UA; that is not a restart
b.link.inject(make_frame(0x2F, {})); // SABM
EXPECT_TRUE(b.hdlc->is_connected());
EXPECT_TRUE(b.states.empty()); // no session lost reported
EXPECT_EQ(b.hdlc->get_send_capacity(), 3); // the queued frame was kept
ASSERT_FALSE(b.link.sent.empty());
EXPECT_EQ(b.link.sent[0], make_frame(0x63, {})); // UA, then the frame is sent again
// The frame is still delivered when it reaches A
for (auto &frame : held)
a.link.inject(frame);
EXPECT_EQ(a.received, std::vector<Frame>{{0x42}});
}
TEST_F(HdlcReliabilityTest, SabmAfterNumberedFramesIsARestart) {
Peer a, b;
pump(a, b);
// B has heard a numbered frame, so a SABM now means A has started again
ASSERT_TRUE(a.send({0x10}));
pump(a, b);
b.link.inject(make_frame(0x2F, {})); // SABM
EXPECT_TRUE(b.hdlc->is_connected());
EXPECT_EQ(b.states, std::vector<bool>{false});
}
TEST_F(HdlcReliabilityTest, QueuedBeforeConnectIsSentAfter) {
Peer a, b;
ASSERT_TRUE(a.send({0x01}));
ASSERT_TRUE(a.send({0x02}));
pump(a, b);
EXPECT_EQ(b.received, (std::vector<Frame>{{0x01}, {0x02}}));
}
TEST_F(HdlcReliabilityTest, DeliversInOrderBothWays) {
Peer a, b;
pump(a, b);
a.send({0x10});
b.send({0x20});
a.send({0x11});
pump(a, b);
EXPECT_EQ(b.received, (std::vector<Frame>{{0x10}, {0x11}}));
EXPECT_EQ(a.received, (std::vector<Frame>{{0x20}}));
}
TEST_F(HdlcReliabilityTest, SequenceNumbersWrap) {
Peer a(3), b(3);
pump(a, b);
std::vector<Frame> expected;
for (uint8_t i = 0; i != 20; i++) {
expected.push_back({i, static_cast<uint8_t>(i * 3)});
ASSERT_TRUE(a.send(expected.back()));
if (i % 2 == 1)
pump(a, b);
}
pump(a, b);
EXPECT_EQ(b.received, expected);
}
TEST_F(HdlcReliabilityTest, GapRecoveredByReject) {
Peer a, b;
pump(a, b);
bool dropped = false;
a.send({0x01});
a.send({0x02});
a.send({0x03});
pump(a, b, [&dropped](const Frame &frame) {
if (dropped || !is_info(frame))
return false;
dropped = true;
return true;
});
EXPECT_EQ(b.received, (std::vector<Frame>{{0x01}, {0x02}, {0x03}}));
}
TEST_F(HdlcReliabilityTest, LossRecoveredByTimeout) {
Peer a, b;
pump(a, b);
a.send({0x42});
pump(a, b, [](const Frame &frame) { return is_info(frame); });
EXPECT_TRUE(b.received.empty());
a.hdlc->on_timeout_();
pump(a, b);
EXPECT_EQ(b.received, (std::vector<Frame>{{0x42}}));
}
TEST_F(HdlcReliabilityTest, LostAckDoesNotDuplicate) {
Peer a, b;
pump(a, b);
a.send({0x42});
pump(a, b, nullptr, [](const Frame &frame) { return is_ack(frame); });
ASSERT_EQ(b.received.size(), 1);
a.hdlc->on_timeout_();
pump(a, b);
EXPECT_EQ(b.received.size(), 1);
// The resend was acknowledged, so the full queue is available again
for (int i = 0; i != 4; i++)
EXPECT_TRUE(a.send({0x01}));
}
TEST_F(HdlcReliabilityTest, QueueFull) {
Peer a(2), b(2);
pump(a, b);
EXPECT_TRUE(a.send({0x01}));
EXPECT_TRUE(a.send({0x02}));
EXPECT_FALSE(a.send({0x03}));
}
TEST_F(HdlcReliabilityTest, OversizedPacketRejected) {
Peer a;
EXPECT_FALSE(a.send(Frame(a.hdlc->get_max_packet_size() + 1)));
}
TEST_F(HdlcReliabilityTest, CrcErrorDropped) {
Peer a, b;
pump(a, b);
Frame frame = make_frame(0x00, {0x55}); // I-frame N(S)=0 N(R)=0
frame[1] ^= 0xFF;
b.link.inject(frame);
EXPECT_TRUE(b.received.empty());
b.link.inject(make_frame(0x00, {0x55}));
EXPECT_EQ(b.received, (std::vector<Frame>{{0x55}}));
}
TEST_F(HdlcReliabilityTest, RetriesExhaustedResetsLink) {
Peer a, b;
pump(a, b);
// One frame is delivered first, so b can tell a reset from a repeated SABM
ASSERT_TRUE(a.send({0x00}));
pump(a, b);
ASSERT_EQ(b.received, std::vector<Frame>{{0x00}});
a.send({0x01});
pump(a, b, [](const Frame &frame) { return is_info(frame); });
for (int i = 0; i != 4; i++) {
a.hdlc->on_timeout_();
pump(a, b, [](const Frame &frame) { return is_info(frame); });
}
// The unacknowledged frame was discarded and the link was reset, which both report as data lost
EXPECT_EQ(b.received, std::vector<Frame>{{0x00}});
EXPECT_TRUE(a.hdlc->is_connected());
EXPECT_EQ(a.states, std::vector<bool>{false});
EXPECT_EQ(b.states, std::vector<bool>{false});
a.send({0x02});
pump(a, b);
EXPECT_EQ(b.received, (std::vector<Frame>{{0x00}, {0x02}}));
}
TEST_F(HdlcReliabilityTest, PeerRestart) {
Peer a;
auto b = std::make_unique<Peer>();
pump(a, *b);
a.send({0x01});
pump(a, *b);
// The peer restarts with fresh sequence numbers
b = std::make_unique<Peer>();
pump(a, *b);
a.send({0x02});
b->send({0x03});
pump(a, *b);
EXPECT_EQ(b->received, (std::vector<Frame>{{0x02}}));
EXPECT_EQ(a.received, (std::vector<Frame>{{0x03}}));
// The restarted peer's reset discarded what a had not yet had acknowledged
EXPECT_EQ(a.states, std::vector<bool>{false});
EXPECT_TRUE(b->states.empty());
}
// Waits past the retry after a refused send, servicing timers as the main loop does
static void run_send_retry() {
const uint32_t start = millis();
while (millis() - start < 60) {
App.scheduler.call(millis());
delay(1);
}
App.scheduler.call(millis());
}
TEST_F(HdlcReliabilityTest, RefusedFramesSentAgainWithoutTimeout) {
Peer a, b;
// Only the retry of a refused send can fire while the test runs, not a retransmit
a.hdlc->set_ack_timeout(10000);
b.hdlc->set_ack_timeout(10000);
for (uint8_t i = 0; i != 3; i++)
ASSERT_TRUE(a.send({i}));
a.link.budget = 3; // SABM, UA to the peer's SABM, and the first queued frame
pump(a, b);
EXPECT_GE(a.link.refused, 1);
EXPECT_EQ(b.received, (std::vector<Frame>{{0}}));
a.link.budget = SIZE_MAX; // the lower layer accepts again, as when a radio stops receiving
// The layer waits for its retry rather than running every loop
pump(a, b);
EXPECT_EQ(b.received, (std::vector<Frame>{{0}}));
run_send_retry();
pump(a, b);
EXPECT_EQ(b.received, (std::vector<Frame>{{0}, {1}, {2}}));
EXPECT_EQ(a.hdlc->outstanding_(), 0);
}
TEST_F(HdlcReliabilityTest, RefusedAckSentAgain) {
Peer a, b;
a.hdlc->set_ack_timeout(10000);
b.hdlc->set_ack_timeout(10000);
pump(a, b);
a.send({1});
for (auto &frame : a.link.sent)
b.link.inject(frame);
a.link.sent.clear();
b.link.budget = 0;
b.hdlc->loop();
EXPECT_TRUE(b.link.sent.empty());
b.link.budget = SIZE_MAX;
pump(a, b);
EXPECT_EQ(a.hdlc->outstanding_(), 1); // the RR waits for the retry
run_send_retry();
pump(a, b);
EXPECT_EQ(a.hdlc->outstanding_(), 0);
}
TEST_F(HdlcReliabilityTest, AckDelayLetsReplyCarryAck) {
Peer a, b;
b.hdlc->set_ack_delay(100);
pump(a, b);
a.send({1});
for (auto &frame : a.link.sent)
b.link.inject(frame);
a.link.sent.clear();
b.hdlc->loop();
EXPECT_TRUE(b.link.sent.empty()); // no RR yet
b.send({2});
ASSERT_EQ(b.link.sent.size(), 1);
EXPECT_TRUE(is_info(b.link.sent[0]));
EXPECT_EQ(b.link.sent[0][0] >> 5, 1); // N(R) acknowledges the frame from a
b.link.sent.clear();
b.hdlc->loop();
EXPECT_TRUE(b.link.sent.empty()); // the RR is no longer owed
}
TEST_F(HdlcReliabilityTest, MaxRetriesOf255ResetsLink) {
Peer a, b;
a.hdlc->set_max_retries(255);
pump(a, b);
a.send({0x01});
a.link.sent.clear();
for (int i = 0; i != 255; i++) {
a.hdlc->on_timeout_();
a.link.sent.clear();
}
EXPECT_TRUE(a.states.empty());
a.hdlc->on_timeout_();
EXPECT_EQ(a.states, std::vector<bool>{false});
}
TEST_F(HdlcReliabilityTest, OversizedFrameDropped) {
Peer a, b;
pump(a, b);
// An I-frame, N(S) 0 and N(R) 0, one byte larger than this layer's packets
b.link.inject(make_frame(0x00, Frame(b.hdlc->get_max_packet_size() + 1, 0x55)));
EXPECT_TRUE(b.received.empty());
b.link.inject(make_frame(0x00, Frame(b.hdlc->get_max_packet_size(), 0x55)));
EXPECT_EQ(b.received.size(), 1);
}
TEST_F(HdlcReliabilityTest, LinkTooSmallFailsSetup) {
MockLink link(HdlcReliability::FRAME_OVERHEAD);
TestHdlc hdlc(&link);
EXPECT_EQ(hdlc.get_max_packet_size(), 0);
hdlc.setup();
EXPECT_TRUE(hdlc.is_failed());
Frame packet{1};
EXPECT_FALSE(hdlc.send_to_interface(PacketBuffer(packet), {}));
EXPECT_EQ(hdlc.get_send_capacity(), 0);
}
TEST_F(HdlcReliabilityTest, AckDelayTimerSendsAck) {
Peer a, b;
b.hdlc->set_ack_delay(5);
pump(a, b);
a.send({1});
for (auto &frame : a.link.sent)
b.link.inject(frame);
a.link.sent.clear();
b.hdlc->loop();
EXPECT_TRUE(b.link.sent.empty());
delay(10);
App.scheduler.call(millis());
b.hdlc->loop();
ASSERT_EQ(b.link.sent.size(), 1);
EXPECT_TRUE(is_ack(b.link.sent[0]));
EXPECT_EQ(b.link.sent[0][0] >> 5, 1); // N(R) acknowledges the frame from a
}
TEST_F(HdlcReliabilityTest, RefusedRejectFallsBackToAck) {
Peer a, b;
pump(a, b);
a.send({1});
a.send({2});
ASSERT_EQ(a.link.sent.size(), 2);
// The first frame is lost, and the REJ for the gap is refused
b.link.budget = 0;
b.link.inject(a.link.sent[1]);
a.link.sent.clear();
EXPECT_EQ(b.link.refused, 1);
b.link.budget = SIZE_MAX;
b.hdlc->loop();
ASSERT_EQ(b.link.sent.size(), 1);
EXPECT_TRUE(is_ack(b.link.sent[0]));
EXPECT_EQ(b.link.sent[0][0] >> 5, 0);
a.hdlc->on_timeout_();
pump(a, b);
EXPECT_EQ(b.received, (std::vector<Frame>{{1}, {2}}));
}
} // namespace esphome::packet_interface::testing