Files
MeshCore/test/fixtures/serial_wifi_sessions/test.cpp
T

414 lines
15 KiB
C++

#include <helpers/wifi/SerialWifiInterface.h>
#include <cassert>
#include <cstdio>
#include <cstdlib>
#include <initializer_list>
#include <limits>
#include <vector>
uint32_t mock_millis = 0;
std::deque<WiFiClient> WiFiServer::incoming;
using Bytes = std::vector<uint8_t>;
using Socket = std::shared_ptr<MockSocket>;
static const IPAddress A(192, 168, 1, 20);
static const IPAddress B(192, 168, 1, 30);
static const IPAddress C(192, 168, 1, 40);
static Bytes framed(std::initializer_list<Bytes> frames) {
Bytes result;
for (const auto& frame : frames) {
result.push_back('>');
result.push_back(uint8_t(frame.size()));
result.push_back(uint8_t(frame.size() >> 8));
result.insert(result.end(), frame.begin(), frame.end());
}
return result;
}
struct Fixture {
SerialWifiInterface transport;
uint8_t input[MAX_FRAME_SIZE] = {};
int callbacks = 0;
bool pending_operation = false;
Socket next_client;
Fixture() {
mock_millis = 100;
WiFiServer::incoming.clear();
transport.begin(5000);
transport.enable();
transport.setSessionChangedCallback([](void* context) {
Fixture& fixture = *static_cast<Fixture*>(context);
++fixture.callbacks;
assert(!fixture.transport.isConnected());
fixture.pending_operation = false;
// Cancellation must happen before a new socket sees an old response or
// has any of its own command bytes consumed.
if (fixture.next_client) {
assert(fixture.next_client->sent.empty());
assert(fixture.next_client->read_count == 0);
}
}, this);
}
~Fixture() { transport.end(); }
size_t tick() { return transport.checkRecvFrame(input); }
Socket connect(IPAddress address, const Bytes& command = {}, bool valid = true) {
Socket socket = std::make_shared<MockSocket>(address);
socket->received.insert(socket->received.end(), command.begin(), command.end());
next_client = socket;
WiFiServer::incoming.push_back(WiFiClient(socket));
tick();
next_client.reset();
assert(transport.isConnected() == valid);
return socket;
}
void enqueue(const Bytes& frame) {
assert(transport.writeFrame(frame.data(), frame.size()) == frame.size());
}
void drain(const Socket& socket) {
socket->write_limit = std::numeric_limits<size_t>::max();
for (int i = 0; i < 12; ++i) tick();
}
};
int main(int argc, char** argv) {
assert(argc == 2);
const int scenario = std::atoi(argv[1]);
Fixture f;
const Bytes reply_a{0x01, 0x21, 0x22, 0x23};
const Bytes reply_b{0x02, 0x31, 0x32};
const Bytes reply_c{0x03, 0x41};
if (scenario == 0) {
auto old = f.connect(A);
f.enqueue(reply_a);
auto replacement = f.connect(A);
assert(!old->connected && f.callbacks == 0);
f.drain(replacement);
assert(replacement->sent == framed({reply_a}));
} else if (scenario == 1) {
auto old = f.connect(A);
f.enqueue(reply_a);
f.pending_operation = true;
old->connected = false;
f.tick();
assert(!f.transport.isConnected());
assert(f.callbacks == 1 && !f.pending_operation);
mock_millis += 60000; // Same-IP retention is not the different-IP grace timer.
auto replacement = f.connect(A);
f.drain(replacement);
assert(replacement->sent == framed({reply_a}) && f.callbacks == 1);
} else if (scenario == 2) {
f.connect(A);
f.enqueue(reply_a);
auto other = f.connect(B);
f.enqueue(reply_b);
f.drain(other);
assert(other->sent == framed({reply_b}) && f.callbacks == 1);
} else if (scenario == 3) {
f.connect(A);
f.enqueue(reply_a);
auto other = f.connect(B);
f.enqueue(reply_b);
mock_millis += 29999;
auto returned = f.connect(A);
f.drain(returned);
assert(returned->sent == framed({reply_a}));
assert(other->sent.empty());
auto other_returned = f.connect(B);
f.drain(other_returned);
assert(other_returned->sent == framed({reply_b}));
assert(f.callbacks == 3);
} else if (scenario == 4) {
f.connect(A);
f.enqueue(reply_a);
f.connect(B);
mock_millis += 30000;
auto expired = f.connect(A);
f.drain(expired);
assert(expired->sent.empty());
} else if (scenario == 5) {
f.connect(A);
f.enqueue(reply_a);
f.connect(B);
mock_millis += 20000;
f.connect(B); // This must not extend A's held deadline.
assert(f.callbacks == 1);
mock_millis += 10000;
auto expired = f.connect(A);
f.drain(expired);
assert(expired->sent.empty());
} else if (scenario == 6 || scenario == 7) {
mock_millis = UINT32_MAX - 10000;
f.connect(A);
f.enqueue(reply_a);
f.connect(B);
mock_millis += scenario == 6 ? 29999 : 30000;
auto returned = f.connect(A);
f.drain(returned);
assert(returned->sent == (scenario == 6 ? framed({reply_a}) : Bytes{}));
} else if (scenario == 8) {
f.connect(A);
f.enqueue(reply_a);
f.connect(B);
f.enqueue(reply_b);
f.connect(C); // Park B and evict A: only one spare fixed-size queue exists.
f.enqueue(reply_c);
auto evicted = f.connect(A);
f.drain(evicted);
assert(evicted->sent.empty());
auto newest = f.connect(C);
f.drain(newest);
assert(newest->sent == framed({reply_c}));
} else if (scenario == 9) {
auto socket = f.connect(A);
f.enqueue(reply_a);
socket->write_limit = 2;
for (int i = 0; i < 8; ++i) f.tick();
assert(socket->sent == framed({reply_a}));
assert(!f.transport.hasPendingIO());
} else if (scenario == 10) {
auto socket = f.connect(A);
const Bytes push{0x80, 0xA1, 0xA2, 0xA3};
f.enqueue(push);
socket->write_limit = 2;
f.tick();
assert(socket->sent.size() == 2);
f.enqueue(reply_a); // Responses have higher queue priority than this push.
f.drain(socket);
assert(socket->sent == framed({push, reply_a}));
} else if (scenario == 11) {
auto old = f.connect(A);
f.enqueue(reply_a);
old->write_limit = 5; // Header and part of payload already sent.
f.tick();
assert(old->sent.size() == 5);
auto replacement = f.connect(A);
f.drain(replacement);
assert(replacement->sent == framed({reply_a}));
} else if (scenario == 12) {
auto old = f.connect(A);
f.enqueue(reply_a);
old->write_limit = 2; // Partial header must not be a suffix on reconnection.
f.tick();
auto other = f.connect(B);
f.drain(other);
assert(other->sent.empty());
auto returned = f.connect(A);
f.drain(returned);
assert(returned->sent == framed({reply_a}));
} else if (scenario == 13 || scenario == 14) {
f.connect(A);
f.enqueue(reply_a);
f.connect(B);
f.enqueue(reply_b);
if (scenario == 13) f.transport.disable();
else f.transport.end();
assert(!f.transport.isConnected() && !f.transport.hasPendingIO());
f.transport.begin(5000);
f.transport.enable();
auto previous = f.connect(A);
f.drain(previous);
assert(previous->sent.empty());
auto other = f.connect(B);
f.drain(other);
assert(other->sent.empty());
} else if (scenario == 15) {
auto old = f.connect(A, Bytes{'<', 4, 0, 0x11}); // Incomplete old command.
assert(old->read_count == 3);
f.pending_operation = true;
f.enqueue(reply_a);
auto other = f.connect(B, Bytes{'<', 2, 0, 0x71, 0x72});
assert(f.callbacks == 1 && !f.pending_operation);
// The new header is parsed from its start, not attached to the old header.
if (other->read_count == 0) assert(f.tick() == 2);
assert(other->read_count == 5);
assert(f.input[0] == 0x71 && f.input[1] == 0x72);
assert(other->sent.empty());
} else if (scenario == 16) {
auto old = f.connect(A, Bytes{'<', 4, 0, 0x11});
assert(old->read_count == 3);
f.pending_operation = true;
auto replacement = f.connect(A, Bytes{'<', 2, 0, 0x61, 0x62});
assert(f.callbacks == 0 && f.pending_operation);
assert(replacement->read_count == 5);
assert(f.input[0] == 0x61 && f.input[1] == 0x62);
} else if (scenario == 17) {
auto socket = f.connect(A);
f.enqueue(reply_a);
f.tick(); // A zero-byte socket write is backpressure, not frame completion.
assert(socket->sent.empty() && f.transport.hasPendingIO());
f.drain(socket);
assert(socket->sent == framed({reply_a}));
} else if (scenario == 18) {
f.connect(A);
for (int i = 0; i < 4; ++i) f.enqueue(Bytes{0x01, uint8_t(i)});
assert(f.transport.writeFrame(reply_a.data(), reply_a.size()) == 0);
f.connect(B);
for (int i = 0; i < 4; ++i) f.enqueue(Bytes{0x02, uint8_t(i)});
auto returned = f.connect(A);
f.drain(returned);
assert(returned->sent == framed({{0x01, 0}, {0x01, 1}, {0x01, 2}, {0x01, 3}}));
auto other = f.connect(B);
f.drain(other);
assert(other->sent == framed({{0x02, 0}, {0x02, 1}, {0x02, 2}, {0x02, 3}}));
} else if (scenario == 19) {
f.connect(A);
f.enqueue(reply_a);
f.connect(B);
mock_millis += 20000;
f.connect(C); // B has no backlog: preserve A without extending its deadline.
mock_millis += 9999;
auto returned = f.connect(A);
f.drain(returned);
assert(returned->sent == framed({reply_a}));
} else if (scenario == 20) {
f.connect(A);
f.enqueue(reply_a);
f.connect(B);
mock_millis += 20000;
f.connect(C);
assert(!f.transport.hasPendingIO()); // Parked-only data must not busy-loop.
mock_millis += 10000;
f.transport.loop();
auto expired = f.connect(A);
f.drain(expired);
assert(expired->sent.empty());
} else if (scenario == 21) {
auto socket = f.connect(A);
const Bytes push{0x80, 0xA1, 0xA2, 0xA3};
const Bytes keep{0x81, 0xB1};
const Bytes evict{0x88, 0xC1};
f.enqueue(push);
socket->write_limit = 2;
f.tick();
f.enqueue(keep);
f.enqueue(evict);
f.enqueue(reply_a);
f.enqueue(reply_b); // Full queue can evict a waiting push, never its sent head.
f.drain(socket);
assert(socket->sent == framed({push, reply_a, reply_b, keep}));
} else if (scenario == 22) {
f.connect(A, Bytes{'<', 4, 0, 0x11});
f.pending_operation = true;
f.transport.disable();
assert(f.callbacks == 1 && !f.pending_operation);
f.transport.end(); // Repeated teardown must not cancel the same owner twice.
assert(f.callbacks == 1);
f.transport.begin(5000);
f.transport.enable();
auto replacement = f.connect(A, Bytes{'<', 2, 0, 0x61, 0x62});
assert(replacement->read_count == 5);
assert(f.input[0] == 0x61 && f.input[1] == 0x62);
} else if (scenario == 23) {
auto socket = f.connect(A);
Bytes maximum(MAX_FRAME_SIZE);
for (size_t i = 0; i < maximum.size(); ++i) maximum[i] = uint8_t(i);
maximum[0] = 0x80; // Low-priority push, pinned once any bytes are sent.
f.enqueue(maximum);
socket->write_limit = MAX_FRAME_SIZE + 2; // All but the final payload byte.
f.tick();
assert(socket->sent.size() == MAX_FRAME_SIZE + 2);
assert(f.transport.hasPendingIO());
f.enqueue(reply_a);
f.drain(socket);
assert(socket->sent == framed({maximum, reply_a}));
assert(!f.transport.hasPendingIO());
} else if (scenario == 24) {
auto old = f.connect(A);
Bytes maximum(MAX_FRAME_SIZE);
for (size_t i = 0; i < maximum.size(); ++i) maximum[i] = uint8_t(i ^ 0x55);
maximum[0] = 0x01;
f.enqueue(maximum);
old->write_limit = MAX_FRAME_SIZE + 2;
f.tick();
assert(old->sent.size() == MAX_FRAME_SIZE + 2);
f.connect(B);
Bytes other_maximum(MAX_FRAME_SIZE, 0xFE);
other_maximum[0] = 0x02;
f.enqueue(other_maximum);
auto returned = f.connect(A);
f.drain(returned);
assert(returned->sent == framed({maximum}));
auto other_returned = f.connect(B);
f.drain(other_returned);
assert(other_returned->sent == framed({other_maximum}));
} else if (scenario >= 25 && scenario <= 27) {
auto socket = f.connect(A);
f.pending_operation = true;
const Bytes invalid = scenario == 25 ? Bytes{'<', 0xff, 0xff}
: scenario == 26 ? Bytes{'?', 1, 0} : Bytes{'<', 0, 0};
socket->received.insert(socket->received.end(), invalid.begin(), invalid.end());
socket->received.insert(socket->received.end(), {'<', 1, 0, 0x71});
assert(f.tick() == 0);
assert(!socket->connected && !f.transport.hasReceivedFrameHeader());
assert(f.callbacks == 1 && !f.pending_operation);
assert(socket->read_count == 3); // Do not wait for or scan the illegal body.
for (int i = 0; i < 10; ++i) assert(f.tick() == 0);
f.connect(A, Bytes{'<', 1, 0, 0x72});
assert(f.input[0] == 0x72);
} else if (scenario == 28 || scenario == 29 || scenario == 34) {
if (scenario == 34) mock_millis = UINT32_MAX - 2000;
auto socket = f.connect(A, scenario == 29 ? Bytes{'<', 2} : Bytes{'<', 2, 0, 0x71});
f.pending_operation = true;
mock_millis += 4999;
assert(f.tick() == 0 && f.transport.isConnected());
mock_millis += 1;
f.transport.loop();
assert(!socket->connected && !f.transport.hasReceivedFrameHeader());
assert(f.callbacks == 1 && !f.pending_operation);
assert(f.tick() == 0);
f.connect(A, Bytes{'<', 1, 0, 0x72});
assert(f.input[0] == 0x72);
} else if (scenario == 30 || scenario == 31) {
auto socket = f.connect(A, scenario == 30 ? Bytes{'<', 2, 0} : Bytes{});
if (scenario == 30) socket->received = {0x71, 0x72};
else socket->received = {'<', 2, 0, 0x71, 0x72};
socket->read_limit = 1;
assert(f.tick() == 0);
assert(!socket->connected && !f.transport.hasReceivedFrameHeader());
assert(f.callbacks == 1);
f.connect(A, Bytes{'<', 1, 0, 0x73});
assert(f.input[0] == 0x73);
} else if (scenario == 32) {
auto socket = f.connect(A, Bytes{'<'});
socket->received.push_back(2);
assert(f.tick() == 0);
socket->received.insert(socket->received.end(), {0, 0x71});
assert(f.tick() == 0);
mock_millis += 4999;
socket->received.push_back(0x72);
assert(f.tick() == 2);
assert(f.input[0] == 0x71 && f.input[1] == 0x72);
assert(f.callbacks == 0 && socket->connected);
} else if (scenario == 33) {
auto socket = f.connect(A);
socket->received = {'<', MAX_FRAME_SIZE, 0};
for (int i = 0; i < MAX_FRAME_SIZE; ++i) socket->received.push_back(uint8_t(i));
socket->received.insert(socket->received.end(), {'<', 1, 0, 0x72});
assert(f.tick() == MAX_FRAME_SIZE);
for (int i = 0; i < MAX_FRAME_SIZE; ++i) assert(f.input[i] == uint8_t(i));
assert(f.tick() == 1 && f.input[0] == 0x72);
assert(f.callbacks == 0 && socket->connected);
} else if (scenario == 35) {
auto socket = f.connect(A, Bytes{'<', 2, 0, 0x71});
f.pending_operation = true;
f.enqueue(reply_a);
socket->connected = false;
f.transport.loop();
assert(f.callbacks == 1 && !f.pending_operation);
f.transport.disable();
assert(f.callbacks == 1); // No second cancellation for an ended session.
} else {
assert(false && "unknown scenario");
}
std::printf("PASS: Wi-Fi session scenario %d\n", scenario);
}