mirror of
https://github.com/torlando-tech/pyxis.git
synced 2026-05-14 03:15:04 +00:00
14f937cf8a
Standalone C++ tests of pyxis-unique code (BLE fragmenter/reassembler, peer manager, GATT op queue, LXST ring buffers, audio filters, HDLC framing) plus Python tests of the patch_nimble.py build script. Each C++ test is compiled directly by clang++/g++ with shims in tests/native/ (Bytes.h, Log.h, Utilities/OS.h) so pyxis sources can build without microReticulum's full Arduino/MsgPack dep tree. A pytest wrapper per test compiles, runs, and parses the summary line — the whole suite is one command: `pytest tests/build_scripts tests/native -v`. Total: 13 pytest tests, ~72 underlying C++ assertions, 3.4s. Surfaced an HPF-formula bug in lxst_audio (mirrored upstream in LXST-kt/native_audio_filters.cpp) — filed as LXST-kt#13 and tracked in the corresponding test with a TODO link. CI workflow runs the pyxis pytest suite plus the clean-passing microReticulum native17 unit tests (94/114 of the existing fork test/* suites) on push and PR.
313 lines
11 KiB
C++
313 lines
11 KiB
C++
// Native unit tests for BLEFragmenter <-> BLEReassembler round-trip.
|
|
//
|
|
// This is the highest-impact pyxis-unique surface for latent bugs: every
|
|
// Reticulum packet over BLE goes through fragment+reassemble. Verifies the
|
|
// v2.2 fragment header format and reassembly state machine handle:
|
|
// - single-fragment (END-only) packets
|
|
// - multi-fragment START / CONTINUE / END sequences
|
|
// - out-of-order fragment delivery
|
|
// - duplicate fragments
|
|
// - dropped fragments + timeout cleanup
|
|
// - fragment-without-START (orphan) rejection
|
|
// - per-peer isolation (one peer's reassembly doesn't bleed into another's)
|
|
// - MTU change between packets
|
|
|
|
#include "../../lib/ble_interface/BLEFragmenter.h"
|
|
#include "../../lib/ble_interface/BLEReassembler.h"
|
|
#include "Utilities/OS.h"
|
|
|
|
#include <cstdint>
|
|
#include <cstdio>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
using RNS::Bytes;
|
|
using RNS::BLE::BLEFragmenter;
|
|
using RNS::BLE::BLEReassembler;
|
|
namespace Fragment = RNS::BLE::Fragment;
|
|
|
|
// ── minimal test framework (same as test_hdlc.cpp) ──
|
|
|
|
static int g_pass = 0;
|
|
static int g_fail = 0;
|
|
|
|
#define EXPECT_EQ(actual, expected) \
|
|
do { \
|
|
auto _a = (actual); \
|
|
auto _e = (expected); \
|
|
if (!(_a == _e)) { \
|
|
char buf[256]; \
|
|
std::snprintf(buf, sizeof(buf), "%s:%d: %s != %s", \
|
|
__FILE__, __LINE__, #actual, #expected); \
|
|
throw std::runtime_error(buf); \
|
|
} \
|
|
} while (0)
|
|
|
|
#define EXPECT_TRUE(cond) \
|
|
do { \
|
|
if (!(cond)) { \
|
|
char buf[256]; \
|
|
std::snprintf(buf, sizeof(buf), "%s:%d: expected %s", \
|
|
__FILE__, __LINE__, #cond); \
|
|
throw std::runtime_error(buf); \
|
|
} \
|
|
} while (0)
|
|
|
|
#define RUN(name) \
|
|
do { \
|
|
try { \
|
|
name(); \
|
|
++g_pass; \
|
|
std::printf("PASS %s\n", #name); \
|
|
} catch (const std::exception& e) { \
|
|
++g_fail; \
|
|
std::printf("FAIL %s: %s\n", #name, e.what()); \
|
|
} \
|
|
} while (0)
|
|
|
|
static Bytes make_payload(size_t n, uint8_t seed) {
|
|
Bytes b;
|
|
for (size_t i = 0; i < n; ++i) b.append((uint8_t)((i * 31) ^ seed));
|
|
return b;
|
|
}
|
|
|
|
static Bytes make_peer(uint8_t tag) {
|
|
Bytes id;
|
|
for (int i = 0; i < 16; ++i) id.append(tag);
|
|
return id;
|
|
}
|
|
|
|
// Helper: collect everything the reassembler emits via callback.
|
|
struct Capture {
|
|
std::vector<std::pair<Bytes, Bytes>> packets; // (peer, packet)
|
|
std::vector<std::pair<Bytes, std::string>> timeouts;
|
|
|
|
void wire(BLEReassembler& r) {
|
|
r.setReassemblyCallback([this](const Bytes& peer, const Bytes& pkt) {
|
|
packets.push_back({peer, pkt});
|
|
});
|
|
r.setTimeoutCallback([this](const Bytes& peer, const std::string& reason) {
|
|
timeouts.push_back({peer, reason});
|
|
});
|
|
}
|
|
};
|
|
|
|
// ── tests ──
|
|
|
|
static void single_fragment_round_trip() {
|
|
BLEFragmenter f(64); // tiny MTU
|
|
BLEReassembler r;
|
|
Capture cap; cap.wire(r);
|
|
|
|
Bytes payload = make_payload(40, 0xAA);
|
|
auto frags = f.fragment(payload);
|
|
EXPECT_EQ(frags.size(), (size_t)1);
|
|
|
|
Bytes peer = make_peer(0x01);
|
|
EXPECT_TRUE(r.processFragment(peer, frags[0]));
|
|
EXPECT_EQ(cap.packets.size(), (size_t)1);
|
|
EXPECT_EQ(cap.packets[0].first, peer);
|
|
EXPECT_EQ(cap.packets[0].second, payload);
|
|
}
|
|
|
|
static void multi_fragment_round_trip_in_order() {
|
|
BLEFragmenter f(32); // forces multi-fragment for any nontrivial payload
|
|
BLEReassembler r;
|
|
Capture cap; cap.wire(r);
|
|
|
|
Bytes payload = make_payload(200, 0xBB);
|
|
auto frags = f.fragment(payload);
|
|
EXPECT_TRUE(frags.size() >= 2);
|
|
|
|
Bytes peer = make_peer(0x02);
|
|
for (size_t i = 0; i < frags.size(); ++i) {
|
|
bool ok = r.processFragment(peer, frags[i]);
|
|
EXPECT_TRUE(ok);
|
|
// Callback should fire exactly once on the final fragment.
|
|
EXPECT_EQ(cap.packets.size(), i + 1 == frags.size() ? (size_t)1 : (size_t)0);
|
|
}
|
|
EXPECT_EQ(cap.packets[0].second, payload);
|
|
}
|
|
|
|
static void out_of_order_fragments_reassemble() {
|
|
BLEFragmenter f(32);
|
|
BLEReassembler r;
|
|
Capture cap; cap.wire(r);
|
|
|
|
Bytes payload = make_payload(150, 0xCC);
|
|
auto frags = f.fragment(payload);
|
|
EXPECT_TRUE(frags.size() >= 3);
|
|
|
|
Bytes peer = make_peer(0x03);
|
|
// Send START first (required by reassembler — no fragment-without-START),
|
|
// then deliver remaining fragments in reverse order.
|
|
r.processFragment(peer, frags[0]);
|
|
for (size_t i = frags.size() - 1; i >= 1; --i) {
|
|
r.processFragment(peer, frags[i]);
|
|
if (i == 1) break;
|
|
}
|
|
|
|
EXPECT_EQ(cap.packets.size(), (size_t)1);
|
|
EXPECT_EQ(cap.packets[0].second, payload);
|
|
}
|
|
|
|
static void duplicate_fragments_dont_double_emit() {
|
|
BLEFragmenter f(32);
|
|
BLEReassembler r;
|
|
Capture cap; cap.wire(r);
|
|
|
|
Bytes payload = make_payload(100, 0xDD);
|
|
auto frags = f.fragment(payload);
|
|
|
|
Bytes peer = make_peer(0x04);
|
|
// Replay every fragment twice.
|
|
for (auto& fr : frags) r.processFragment(peer, fr);
|
|
for (auto& fr : frags) r.processFragment(peer, fr);
|
|
|
|
// Reassembly must complete exactly once. Replay after completion may
|
|
// be silently ignored or trigger a fresh failed reassembly — we only
|
|
// require that the original packet was emitted exactly once and not
|
|
// corrupted.
|
|
EXPECT_TRUE(cap.packets.size() >= 1);
|
|
EXPECT_EQ(cap.packets[0].second, payload);
|
|
if (cap.packets.size() > 1) {
|
|
for (auto& kv : cap.packets) EXPECT_EQ(kv.second, payload);
|
|
}
|
|
}
|
|
|
|
static void fragment_without_start_is_rejected() {
|
|
BLEFragmenter f(32);
|
|
BLEReassembler r;
|
|
Capture cap; cap.wire(r);
|
|
|
|
Bytes payload = make_payload(100, 0xEE);
|
|
auto frags = f.fragment(payload);
|
|
EXPECT_TRUE(frags.size() >= 3);
|
|
|
|
Bytes peer = make_peer(0x05);
|
|
// Skip START, deliver only CONTINUE + END. Reassembler should drop
|
|
// these as orphans.
|
|
for (size_t i = 1; i < frags.size(); ++i) r.processFragment(peer, frags[i]);
|
|
|
|
EXPECT_EQ(cap.packets.size(), (size_t)0);
|
|
EXPECT_EQ(r.pendingCount(), (size_t)0);
|
|
}
|
|
|
|
static void dropped_fragment_times_out() {
|
|
BLEFragmenter f(32);
|
|
BLEReassembler r;
|
|
Capture cap; cap.wire(r);
|
|
r.setTimeout(10.0);
|
|
|
|
Bytes payload = make_payload(150, 0xFF);
|
|
auto frags = f.fragment(payload);
|
|
EXPECT_TRUE(frags.size() >= 3);
|
|
|
|
Bytes peer = make_peer(0x06);
|
|
// Install a fake clock at t=0, deliver START + first CONTINUE, then drop
|
|
// the rest. Advance the clock past the timeout and call checkTimeouts.
|
|
RNS::Utilities::OS::set_fake_time(0.0);
|
|
r.processFragment(peer, frags[0]);
|
|
r.processFragment(peer, frags[1]);
|
|
EXPECT_EQ(r.pendingCount(), (size_t)1);
|
|
|
|
RNS::Utilities::OS::set_fake_time(20.0); // > timeout
|
|
r.checkTimeouts();
|
|
|
|
EXPECT_EQ(r.pendingCount(), (size_t)0);
|
|
EXPECT_EQ(cap.timeouts.size(), (size_t)1);
|
|
EXPECT_EQ(cap.timeouts[0].first, peer);
|
|
EXPECT_EQ(cap.packets.size(), (size_t)0);
|
|
|
|
RNS::Utilities::OS::clear_fake_time();
|
|
}
|
|
|
|
static void per_peer_isolation() {
|
|
BLEFragmenter f(32);
|
|
BLEReassembler r;
|
|
Capture cap; cap.wire(r);
|
|
|
|
Bytes payload_a = make_payload(150, 0xA0);
|
|
Bytes payload_b = make_payload(150, 0xB0);
|
|
auto frags_a = f.fragment(payload_a);
|
|
auto frags_b = f.fragment(payload_b);
|
|
|
|
Bytes peer_a = make_peer(0x0A);
|
|
Bytes peer_b = make_peer(0x0B);
|
|
|
|
// Interleave fragments from two peers.
|
|
for (size_t i = 0; i < frags_a.size(); ++i) {
|
|
r.processFragment(peer_a, frags_a[i]);
|
|
if (i < frags_b.size()) r.processFragment(peer_b, frags_b[i]);
|
|
}
|
|
for (size_t i = frags_a.size(); i < frags_b.size(); ++i) {
|
|
r.processFragment(peer_b, frags_b[i]);
|
|
}
|
|
|
|
EXPECT_EQ(cap.packets.size(), (size_t)2);
|
|
// Map by peer.
|
|
Bytes got_a, got_b;
|
|
for (auto& kv : cap.packets) {
|
|
if (kv.first == peer_a) got_a = kv.second;
|
|
if (kv.first == peer_b) got_b = kv.second;
|
|
}
|
|
EXPECT_EQ(got_a, payload_a);
|
|
EXPECT_EQ(got_b, payload_b);
|
|
}
|
|
|
|
static void fragment_count_matches_calculator() {
|
|
BLEFragmenter f(64);
|
|
Bytes payload = make_payload(300, 0x11);
|
|
auto frags = f.fragment(payload);
|
|
EXPECT_EQ(frags.size(), (size_t)f.calculateFragmentCount(payload.size()));
|
|
}
|
|
|
|
static void mtu_change_affects_subsequent_fragments() {
|
|
BLEFragmenter f(32);
|
|
Bytes payload = make_payload(100, 0x22);
|
|
auto small_mtu_frags = f.fragment(payload);
|
|
|
|
f.setMTU(128);
|
|
auto large_mtu_frags = f.fragment(payload);
|
|
// Larger MTU should produce strictly fewer (or equal) fragments.
|
|
EXPECT_TRUE(large_mtu_frags.size() <= small_mtu_frags.size());
|
|
}
|
|
|
|
static void clear_for_peer_drops_only_that_peer() {
|
|
BLEFragmenter f(32);
|
|
BLEReassembler r;
|
|
Capture cap; cap.wire(r);
|
|
|
|
auto frags_a = f.fragment(make_payload(100, 0x1));
|
|
auto frags_b = f.fragment(make_payload(100, 0x2));
|
|
|
|
Bytes peer_a = make_peer(0x1A);
|
|
Bytes peer_b = make_peer(0x1B);
|
|
|
|
r.processFragment(peer_a, frags_a[0]);
|
|
r.processFragment(peer_b, frags_b[0]);
|
|
EXPECT_EQ(r.pendingCount(), (size_t)2);
|
|
|
|
r.clearForPeer(peer_a);
|
|
EXPECT_EQ(r.pendingCount(), (size_t)1);
|
|
EXPECT_TRUE(!r.hasPending(peer_a));
|
|
EXPECT_TRUE(r.hasPending(peer_b));
|
|
}
|
|
|
|
int main() {
|
|
RUN(single_fragment_round_trip);
|
|
RUN(multi_fragment_round_trip_in_order);
|
|
RUN(out_of_order_fragments_reassemble);
|
|
RUN(duplicate_fragments_dont_double_emit);
|
|
RUN(fragment_without_start_is_rejected);
|
|
RUN(dropped_fragment_times_out);
|
|
RUN(per_peer_isolation);
|
|
RUN(fragment_count_matches_calculator);
|
|
RUN(mtu_change_affects_subsequent_fragments);
|
|
RUN(clear_for_peer_drops_only_that_peer);
|
|
|
|
std::printf("\n%d passed, %d failed\n", g_pass, g_fail);
|
|
return g_fail == 0 ? 0 : 1;
|
|
}
|