Files
MeshCore/test/fixtures/serial_ethernet_sessions/test.cpp
T
mikecarper a88ced23b6 Fix OTA resume, CLI identity, radio persistence, and network sessions
Distinguish OTA read failures from missing blocks and select unique raw-flash checkpoints by MID and target. Keep deferred CLI requests and replies bound to immutable command identity and full authenticated keys.

Gate radio mutations on their exact reply delivery, recover coding-rate restoration, and roll back failed Companion preference writes. Preserve accepted low duty cycles across reboot.

Retain partial network writes, cancel displaced client operations, and reject malformed or stalled framed input. Add regression and CI coverage, and size-optimize the RAK4631 Ethernet build without dropping features.
2026-09-15 14:32:40 -07:00

252 lines
8.9 KiB
C++

#include <helpers/ethernet/SerialEthernetInterface.h>
#include <helpers/MultiSerialInterface.h>
#include <WiFi.h>
#include <cassert>
#include <cstdio>
#include <cstdlib>
uint32_t mock_millis = 100;
using Bytes = std::vector<uint8_t>;
using Socket = std::shared_ptr<MockSocket>;
static constexpr uint32_t A = 0xc0a80114;
static constexpr uint32_t B = 0xc0a8011e;
static Bytes framed(std::initializer_list<Bytes> frames) {
Bytes result;
for (const auto& frame : frames) {
#if !ETHERNET_RAW_LINE
result.insert(result.end(), {'>', uint8_t(frame.size()), uint8_t(frame.size() >> 8)});
#endif
result.insert(result.end(), frame.begin(), frame.end());
#if ETHERNET_RAW_LINE
result.insert(result.end(), {'\r', '\n'});
#endif
}
return result;
}
static Bytes command(const Bytes& payload) {
#if ETHERNET_RAW_LINE
Bytes data = payload;
data.push_back('\n');
#else
Bytes data{'<', uint8_t(payload.size()), uint8_t(payload.size() >> 8)};
data.insert(data.end(), payload.begin(), payload.end());
#endif
return data;
}
struct Transport : SerialEthernetInterface {
bool connected = false;
Socket client;
Socket next_client;
bool isConnected() const override { return isEnabled() && connected; }
void disconnectClient() override {
connected = false;
if (client) client->connected = false;
}
int available() override { return client ? int(client->received.size()) : 0; }
int read() override {
if (!client || client->received.empty()) return -1;
const int value = client->received.front();
client->received.pop_front();
++client->read_count;
return value;
}
size_t write(const uint8_t* src, size_t size) override {
if (!isConnected() || !client) return 0;
size = std::min(size, client->write_limit);
client->sent.insert(client->sent.end(), src, src + size);
return size;
}
Socket connect(uint32_t ip, const Bytes& input = {}) {
next_client = std::make_shared<MockSocket>(IPAddress(ip));
next_client->received.insert(next_client->received.end(), input.begin(), input.end());
disconnectClient();
onClientConnected(ip); // Same ordering as both hardware driver loops.
client = next_client;
next_client.reset();
connected = true;
return client;
}
};
struct Fixture {
Transport transport;
MultiSerialInterface routes;
uint8_t input[MAX_FRAME_SIZE] = {};
int callbacks = 0;
bool pending_operation = false;
Fixture() {
assert(routes.addInterface(InterfaceType::Ethernet, &transport));
routes.enable();
transport.setSessionChangedCallback([](void* context) {
auto& f = *static_cast<Fixture*>(context);
assert(!f.transport.isConnected());
if (f.transport.next_client) {
assert(f.transport.next_client->sent.empty());
assert(f.transport.next_client->read_count == 0);
}
++f.callbacks;
f.pending_operation = false;
f.routes.forgetReplyRouteForDisconnected(&f.transport);
}, this);
}
size_t tick() { return routes.checkRecvFrame(input); }
void enqueue(const Bytes& data) {
assert(transport.writeFrame(data.data(), data.size()) == data.size());
}
void drain(const Socket& socket) {
socket->write_limit = std::numeric_limits<size_t>::max();
for (int i = 0; i < 20; ++i) tick();
}
};
int main(int argc, char** argv) {
assert(argc == 2);
const int scenario = std::atoi(argv[1]);
Fixture f;
const Bytes reply{0x01, 0x21, 0x22, 0x23};
const Bytes other{0x02, 0x31};
const Bytes push{0x80, 0x41, 0x42};
auto socket = f.transport.connect(A);
if (scenario <= 2) {
f.enqueue(reply);
socket->write_limit = scenario == 0 ? 2 : scenario == 1 ? 4 : 0;
f.tick();
assert(socket->sent.size() == socket->write_limit);
f.drain(socket);
assert(socket->sent == framed({reply}));
} else if (scenario == 3 || scenario == 4) {
f.enqueue(push);
socket->write_limit = 2;
f.tick();
if (scenario == 4) {
f.enqueue(Bytes{0x81, 0x51});
f.enqueue(Bytes{0x88, 0x61});
}
f.enqueue(reply);
if (scenario == 4) f.enqueue(other);
f.drain(socket);
assert(socket->sent == (scenario == 4 ? framed({push, reply, other, {0x81, 0x51}})
: framed({push, reply})));
} else if (scenario == 5) {
Bytes maximum(MAX_FRAME_SIZE, 0x45);
maximum[0] = 0x80;
f.enqueue(maximum);
socket->write_limit = framed({maximum}).size() - 1;
f.tick();
assert(socket->sent.size() == socket->write_limit);
f.enqueue(reply);
f.drain(socket);
assert(socket->sent == framed({maximum, reply}));
} else if (scenario == 6 || scenario == 7) {
f.pending_operation = true;
f.enqueue(reply);
socket->write_limit = 2;
f.tick();
auto replacement = f.transport.connect(scenario == 6 ? A : B);
assert(!socket->connected);
assert(f.callbacks == (scenario == 6 ? 0 : 1));
assert(f.pending_operation == (scenario == 6));
f.drain(replacement);
assert(replacement->sent == (scenario == 6 ? framed({reply}) : Bytes{}));
if (scenario == 7) {
auto returned = f.transport.connect(A);
f.drain(returned);
assert(returned->sent.empty()); // No extra held queue on Ethernet.
}
} else if (scenario == 8) {
f.pending_operation = true;
f.enqueue(reply);
f.transport.disconnectClient();
f.transport.onClientDisconnected();
assert(f.callbacks == 1 && !f.pending_operation);
assert(!f.transport.hasPendingIO());
mock_millis += 60000;
auto returned = f.transport.connect(A);
f.drain(returned);
assert(returned->sent == framed({reply}) && f.callbacks == 1);
} else if (scenario == 9) {
const Bytes request = command(Bytes{0x04});
socket->received.insert(socket->received.end(), request.begin(), request.end());
assert(f.tick() == 1);
assert(f.routes.captureReplyRoute() == &f.transport);
f.routes.lockReplyRoute();
f.pending_operation = true;
auto replacement = f.transport.connect(B, command(Bytes{0x05}));
assert(!f.pending_operation && f.callbacks == 1);
assert(f.routes.captureReplyRoute() == nullptr);
assert(replacement->sent.empty() && replacement->read_count == 0);
assert(f.tick() == 1 && f.input[0] == 0x05);
assert(f.routes.captureReplyRoute() == &f.transport);
} else if (scenario == 10) {
f.enqueue(reply);
f.pending_operation = true;
f.transport.disable();
f.transport.disable();
assert(!socket->connected && !f.pending_operation && f.callbacks == 1);
assert(!f.transport.hasPendingIO());
f.transport.enable();
auto replacement = f.transport.connect(A);
f.drain(replacement);
assert(replacement->sent.empty());
} else if (scenario == 11) {
#if ETHERNET_RAW_LINE
socket->received = std::deque<uint8_t>(MAX_FRAME_SIZE + 1, 'a');
socket->received.push_back('\n');
#else
socket->received = {'<', 0xff, 0xff};
#endif
const Bytes tail = command(Bytes{0x71});
socket->received.insert(socket->received.end(), tail.begin(), tail.end());
assert(f.tick() == 0);
assert(!socket->connected && f.callbacks == 1);
assert(!f.transport.isReadBusy());
for (int i = 0; i < 5; ++i) assert(f.tick() == 0);
f.transport.connect(A, command(Bytes{0x72}));
assert(f.tick() == 1 && f.input[0] == 0x72);
} else if (scenario == 12 || scenario == 14) {
if (scenario == 14) mock_millis = UINT32_MAX - 2000;
Bytes partial = command(Bytes{'a', 'b'});
partial.pop_back();
socket->received.insert(socket->received.end(), partial.begin(), partial.end());
assert(f.tick() == 0);
assert(f.transport.isReadBusy());
mock_millis += 4999;
assert(f.tick() == 0 && socket->connected);
mock_millis += 1;
#if ETHERNET_RAW_LINE
// Human terminal input is not subject to the binary frame deadline.
assert(f.tick() == 0 && socket->connected);
mock_millis += 60000;
socket->received.push_back('\n');
assert(f.tick() == 2 && f.input[0] == 'a' && f.input[1] == 'b');
assert(f.callbacks == 0 && !f.transport.isReadBusy());
#else
assert(f.tick() == 0 && !socket->connected);
assert(f.callbacks == 1 && !f.transport.isReadBusy());
#endif
} else if (scenario == 13) {
Bytes maximum(MAX_FRAME_SIZE, 'a');
const Bytes bytes = command(maximum);
for (size_t i = 0; i < bytes.size(); ++i) {
socket->received.push_back(bytes[i]);
assert(f.tick() == (i + 1 == bytes.size() ? MAX_FRAME_SIZE : 0));
}
for (int i = 0; i < MAX_FRAME_SIZE; ++i) assert(f.input[i] == 'a');
assert(!f.transport.isReadBusy() && socket->connected);
const Bytes next = command(Bytes{'b'});
socket->received.insert(socket->received.end(), next.begin(), next.end());
assert(f.tick() == 1 && f.input[0] == 'b');
} else if (scenario == 15) {
const Bytes data = command(reply);
socket->received.insert(socket->received.end(), data.begin(), data.end());
assert(f.transport.checkRecvFrame(nullptr) == 0);
assert(!socket->connected && f.callbacks == 1);
} else {
assert(false && "unknown scenario");
}
std::printf("PASS: Ethernet session scenario %d\n", scenario);
}