447 lines
21 KiB
Python
447 lines
21 KiB
Python
"""Execute the production TX-only beacon owner and Dispatcher lifecycle.
|
|
|
|
The test uses the actual Dispatcher, packet serializer, and static pool. Radio
|
|
and clock boundaries are counted adapters. No PlatformIO or hardware is used.
|
|
"""
|
|
|
|
import os
|
|
from pathlib import Path
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
|
|
HARNESS = r'''
|
|
#include <Dispatcher.h>
|
|
#include <helpers/StaticPoolPacketManager.h>
|
|
#include <cassert>
|
|
#include <cstring>
|
|
#include <cstdio>
|
|
#include <vector>
|
|
using namespace mesh;
|
|
class Clock : public MillisecondClock {
|
|
public:
|
|
uint32_t now;
|
|
explicit Clock(uint32_t initial = 1000) : now(initial) {}
|
|
unsigned long getMillis() override { return now; }
|
|
};
|
|
class TestRadio : public Radio {
|
|
public:
|
|
RadioProfiles p;
|
|
RadioProfileParams physical, saved;
|
|
int8_t power = 22, saved_power = 22;
|
|
uint8_t active = 0;
|
|
bool owned = false, receiving = false, complete = false, sending = false;
|
|
bool start_ok = true, carrier = false, morse_tx = false, morse_rx = false;
|
|
bool calibrating = false, incoming = false, channel_busy = false;
|
|
bool valid = true, busy_owns = false, fail_owns = false;
|
|
unsigned sends = 0, beacon_sends = 0, normal_sends = 0, finishes = 0, recovery = 0;
|
|
unsigned processed = 0, prepare_calls = 0, restore_calls = 0, loops = 0;
|
|
unsigned agc = 0, noise = 0, begins = 0, begin_while_owned = 0;
|
|
uint32_t airtime = 100;
|
|
Clock* clock = nullptr;
|
|
uint32_t cad_delay = 0;
|
|
RadioParamApplyResult prepare = RadioParamApplyResult::APPLIED;
|
|
RadioParamApplyResult restore = RadioParamApplyResult::APPLIED;
|
|
std::vector<uint8_t> sent;
|
|
std::vector<uint32_t> beacon_starts;
|
|
TestRadio() {
|
|
p.primary.freq = 909.5f; p.primary.bw = 125; p.primary.sf = 7; p.primary.cr = 5;
|
|
p.primary.preamble = 32;
|
|
p.secondary.params = p.primary; p.secondary.params.freq = 910;
|
|
physical = saved = p.primary;
|
|
}
|
|
RadioProfiles* profiles() override { return &p; }
|
|
const RadioProfiles* profiles() const override { return &p; }
|
|
bool supportsMeshtext() const override { return true; }
|
|
bool validateProfile(const RadioProfileParams& params) const override {
|
|
return valid && RadioProfiles::valid(params);
|
|
}
|
|
uint8_t receiveProfile() const override { return active; }
|
|
int recvRaw(uint8_t* bytes, int) override {
|
|
assert(!owned);
|
|
if (!incoming) return 0;
|
|
incoming = false; bytes[0] = 0x15; bytes[1] = 0; bytes[2] = 0x23; return 3;
|
|
}
|
|
void onReceiveProcessed() override { ++processed; }
|
|
uint32_t getEstAirtimeFor(int) override { return 100; }
|
|
uint32_t getProfileAirtime(uint8_t, int, uint8_t = 0) override { return 100; }
|
|
uint32_t getBeaconAirtime(const RadioProfileParams&, int) override { return airtime; }
|
|
float packetScore(float, int) override { return 1; }
|
|
RadioParamApplyResult prepareTransmitProfile(uint8_t profile) override {
|
|
assert(!owned); active = profile; physical = p.params(profile); return RadioParamApplyResult::APPLIED;
|
|
}
|
|
RadioParamApplyResult prepareBeaconTransmit(const RadioProfileParams& params, int8_t requested_power) override {
|
|
++prepare_calls;
|
|
if (prepare == RadioParamApplyResult::APPLIED || busy_owns || fail_owns) {
|
|
if (!owned) { saved = physical; saved_power = power; }
|
|
owned = true; physical = params; power = requested_power;
|
|
}
|
|
return prepare;
|
|
}
|
|
RadioParamApplyResult restoreBeaconReceive() override {
|
|
++restore_calls;
|
|
if (restore == RadioParamApplyResult::APPLIED) {
|
|
if (owned) { physical = saved; power = saved_power; }
|
|
owned = false;
|
|
}
|
|
return restore;
|
|
}
|
|
bool isBeaconRadioOwned() const override { return owned; }
|
|
bool isBeaconChannelBusy() override {
|
|
assert(owned);
|
|
if (clock) clock->now += cad_delay;
|
|
return channel_busy;
|
|
}
|
|
bool startSendRaw(const uint8_t* bytes, int length) override {
|
|
++sends;
|
|
if (owned) {
|
|
++beacon_sends;
|
|
if (clock) beacon_starts.push_back(clock->now);
|
|
} else ++normal_sends;
|
|
sent.assign(bytes, bytes + length); sending = start_ok; complete = false; return start_ok;
|
|
}
|
|
bool isSendComplete() override { return complete; }
|
|
void onSendFinished() override { ++finishes; sending = false; }
|
|
bool isInRecvMode() const override { return !sending; }
|
|
bool isReceiving() override { return receiving; }
|
|
bool isCarrierWaveActive() const override { return carrier; }
|
|
bool isMorseTxActive() const override { return morse_tx; }
|
|
bool isMorseRxActive() const override { return morse_rx; }
|
|
bool isCalibratingNoiseFloor() const override { return calibrating; }
|
|
bool recoverRadio(bool) override { ++recovery; return true; }
|
|
uint32_t getRecoveryCount() const override { return recovery; }
|
|
void loop() override { assert(!owned); ++loops; }
|
|
void begin() override { ++begins; if (owned) ++begin_while_owned; }
|
|
void resetAGC() override { assert(!owned); ++agc; }
|
|
bool tryResetAGC() override { resetAGC(); return true; }
|
|
void triggerNoiseFloorCalibrate(int) override { assert(!owned); ++noise; }
|
|
};
|
|
class TestDispatcher : public Dispatcher {
|
|
public:
|
|
unsigned received = 0, mc_sent = 0, failed = 0;
|
|
unsigned long duty_window = 3600000;
|
|
int interval = 0;
|
|
bool adaptive = false;
|
|
TestDispatcher(TestRadio& radio, Clock& clock, StaticPoolPacketManager& manager)
|
|
: Dispatcher(radio, clock, manager) {}
|
|
using Dispatcher::setRadioAvailable;
|
|
using Dispatcher::getNextQueueWakeDelay;
|
|
using Dispatcher::queueForeignRadioFrame;
|
|
DispatcherAction onRecvPacket(Packet*) override { ++received; return ACTION_RELEASE; }
|
|
void onSendComplete(Packet*) override { ++mc_sent; }
|
|
void onSendFail(Packet*) override { ++failed; }
|
|
unsigned long getDutyCycleWindowMs() const override { return duty_window; }
|
|
int getAGCResetInterval() const override { return interval; }
|
|
bool useAdaptiveAGC() const override { return adaptive; }
|
|
};
|
|
struct Fixture {
|
|
Clock clock;
|
|
TestRadio radio;
|
|
StaticPoolPacketManager manager{8};
|
|
TestDispatcher d{radio, clock, manager};
|
|
explicit Fixture(uint32_t initial = 1000) : clock(initial) { radio.clock = &clock; d.begin(); }
|
|
void tick(uint32_t delta = 1) { clock.now += delta; d.loop(); }
|
|
};
|
|
static const uint8_t bytes[] = {0x15, 0, 0x23};
|
|
static RadioProfileParams beacon_params() {
|
|
RadioProfileParams p;
|
|
p.freq = 906.875f; p.bw = 250; p.sf = 11; p.cr = 5; p.preamble = 16;
|
|
p.protocol = RadioProfileProtocol::Meshtext;
|
|
return p;
|
|
}
|
|
static bool queue(Fixture& f) {
|
|
return f.d.queueBeaconRadioFrame(bytes, sizeof(bytes), beacon_params(), 17);
|
|
}
|
|
static Packet* normal(Fixture& f, uint32_t delay = 0) {
|
|
Packet* packet = f.d.obtainNewPacket(); assert(packet);
|
|
packet->header = ROUTE_TYPE_FLOOD | (PAYLOAD_TYPE_GRP_TXT << PH_TYPE_SHIFT);
|
|
packet->tx_radio = RADIO_TX_PRIMARY; packet->payload[0] = 0x12; packet->payload_len = 1;
|
|
assert(f.d.sendPacket(packet, 0, delay));
|
|
return packet;
|
|
}
|
|
static void basic() {
|
|
Fixture f;
|
|
f.radio.p.secondary.mode = RadioProfileMode::RxTx;
|
|
f.radio.p.secondary_temporary = true;
|
|
const auto saved = f.radio.p;
|
|
assert(queue(f)); assert(f.d.hasOutbound() && !queue(f));
|
|
uint32_t delay = 99;
|
|
assert(f.d.getNextQueueWakeDelay(delay) && delay == 0);
|
|
f.tick();
|
|
assert(f.radio.beacon_sends == 1 && f.radio.owned && f.radio.physical == beacon_params());
|
|
assert(f.radio.sent == std::vector<uint8_t>(bytes, bytes + sizeof(bytes)));
|
|
assert(f.radio.power == 17 && !f.d.queueForeignRadioFrame(bytes, sizeof(bytes), 1, 1));
|
|
f.tick(20); assert(f.d.hasOutbound() && f.radio.finishes == 0);
|
|
f.radio.complete = true; f.tick();
|
|
assert(!f.d.hasOutbound() && !f.radio.owned && f.radio.finishes == 1);
|
|
assert(f.radio.physical == saved.primary && f.radio.power == 22);
|
|
assert(f.radio.p.primary == saved.primary && f.radio.p.secondary.params == saved.secondary.params);
|
|
assert(f.radio.p.secondary.mode == saved.secondary.mode && f.radio.p.secondary_temporary);
|
|
assert(f.radio.p.generation[0] == saved.generation[0] && f.radio.p.generation[1] == saved.generation[1]);
|
|
assert(!f.d.getNumSentFlood() && !f.d.getNumSentDirect() && !f.d.mc_sent);
|
|
assert(f.d.getTotalAirTime() == 21);
|
|
}
|
|
static void preparation() {
|
|
{ Fixture f; f.radio.prepare = RadioParamApplyResult::BUSY;
|
|
assert(queue(f)); f.tick(); assert(f.radio.prepare_calls == 1 && !f.radio.owned && !f.radio.sends);
|
|
f.tick(10); assert(f.radio.prepare_calls == 1);
|
|
f.tick(1); assert(f.radio.prepare_calls == 2);
|
|
f.tick(29987); assert(f.d.hasBeaconRadioFrame());
|
|
f.tick(1); assert(!f.d.hasBeaconRadioFrame() && !f.radio.sends && !f.radio.owned);
|
|
assert(queue(f)); // No RF attempt means the one-hour guard was not consumed.
|
|
f.d.cancelBeaconRadioFrame(); }
|
|
{ Fixture f; f.radio.prepare = RadioParamApplyResult::BUSY; f.radio.busy_owns = true;
|
|
f.radio.restore = RadioParamApplyResult::BUSY; assert(queue(f)); f.tick();
|
|
assert(f.d.hasBeaconRadioFrame() && f.radio.owned && f.radio.restore_calls == 1);
|
|
const unsigned loops = f.radio.loops; f.tick(500);
|
|
assert(f.radio.loops == loops && !f.radio.sends);
|
|
f.radio.restore = RadioParamApplyResult::APPLIED; f.tick();
|
|
assert(!f.d.hasBeaconRadioFrame() && !f.radio.owned); }
|
|
{ Fixture f; f.radio.prepare = RadioParamApplyResult::FAILED; f.radio.fail_owns = true;
|
|
assert(queue(f)); f.tick(); assert(!f.radio.owned && !f.d.hasBeaconRadioFrame() && !f.radio.sends); }
|
|
}
|
|
static void failures() {
|
|
{ Fixture f; f.radio.channel_busy = true; assert(queue(f)); f.tick();
|
|
assert(!f.radio.sends && !f.d.hasBeaconRadioFrame() && !f.radio.owned);
|
|
f.radio.channel_busy = false; assert(queue(f)); f.d.cancelBeaconRadioFrame(); }
|
|
{ Fixture f; f.radio.start_ok = false; assert(queue(f)); f.tick();
|
|
assert(f.radio.sends == 1 && !f.d.hasBeaconRadioFrame() && !f.radio.owned);
|
|
assert(queue(f)); f.tick(); assert(f.radio.sends == 1);
|
|
f.tick(30000); assert(!f.d.hasBeaconRadioFrame() && f.radio.sends == 1);
|
|
f.tick(3600000); assert(queue(f)); f.d.cancelBeaconRadioFrame(); }
|
|
{ Fixture f; f.radio.airtime = 0; assert(queue(f)); f.tick();
|
|
assert(!f.d.hasBeaconRadioFrame() && !f.radio.sends); }
|
|
{ Fixture f; f.radio.airtime = UINT32_MAX; assert(queue(f)); f.tick();
|
|
assert(!f.d.hasBeaconRadioFrame() && !f.radio.sends); }
|
|
{ Fixture f; assert(queue(f)); f.tick(); f.tick(1150);
|
|
assert(!f.d.hasBeaconRadioFrame() && !f.radio.owned && f.radio.finishes == 1 && f.radio.recovery == 1);
|
|
assert(f.d.getErrFlags() & ERR_EVENT_RADIO_WATCHDOG);
|
|
f.tick(10000); assert(f.radio.sends == 1); }
|
|
}
|
|
static void restoration() {
|
|
Fixture f; f.d.interval = 8; f.d.adaptive = true;
|
|
assert(queue(f)); f.tick();
|
|
const unsigned loops = f.radio.loops, agc = f.radio.agc, noise = f.radio.noise;
|
|
f.radio.restore = RadioParamApplyResult::BUSY;
|
|
f.radio.complete = true; f.radio.incoming = true;
|
|
normal(f); f.tick(20);
|
|
assert(f.radio.owned && f.d.hasBeaconRadioFrame() && f.radio.finishes == 1);
|
|
f.tick(30000);
|
|
assert(f.radio.loops == loops && f.radio.agc == agc && f.radio.noise == noise);
|
|
assert(f.radio.normal_sends == 0 && f.radio.incoming && f.manager.getOutboundTotal() == 1);
|
|
f.radio.restore = RadioParamApplyResult::FAILED; f.tick();
|
|
assert(f.radio.owned && f.radio.loops == loops && !f.radio.normal_sends);
|
|
f.radio.restore = RadioParamApplyResult::APPLIED; f.tick();
|
|
assert(!f.radio.owned && !f.d.hasBeaconRadioFrame() && !f.radio.incoming);
|
|
assert(f.radio.normal_sends == 1 && f.radio.agc > agc);
|
|
f.radio.complete = true; f.tick();
|
|
}
|
|
static void availability() {
|
|
{ Fixture f; assert(queue(f)); f.d.setRadioAvailable(false);
|
|
assert(!f.d.hasBeaconRadioFrame() && !queue(f));
|
|
const unsigned restores = f.radio.restore_calls, loops = f.radio.loops;
|
|
f.tick(10000); assert(f.radio.restore_calls == restores && f.radio.loops == loops);
|
|
f.d.setRadioAvailable(true); assert(queue(f)); f.d.cancelBeaconRadioFrame(); }
|
|
{ Fixture f; assert(queue(f)); f.tick(); f.radio.restore = RadioParamApplyResult::BUSY;
|
|
f.d.setRadioAvailable(false); assert(f.radio.owned && f.d.hasBeaconRadioFrame());
|
|
const unsigned restores = f.radio.restore_calls, finishes = f.radio.finishes, loops = f.radio.loops;
|
|
f.tick(10000);
|
|
assert(f.radio.restore_calls == restores && f.radio.finishes == finishes && f.radio.loops == loops);
|
|
f.d.setRadioAvailable(true);
|
|
assert(!f.radio.begin_while_owned); // Never initialize through an outstanding beacon lease.
|
|
f.radio.restore = RadioParamApplyResult::APPLIED; f.tick();
|
|
assert(!f.radio.owned && !f.d.hasBeaconRadioFrame()); }
|
|
}
|
|
static void priority() {
|
|
{ Fixture f; assert(queue(f)); normal(f); f.tick();
|
|
assert(f.radio.normal_sends == 1 && !f.radio.beacon_sends && !f.radio.owned);
|
|
assert(f.d.hasBeaconRadioFrame()); f.radio.complete = true; f.tick(); f.tick(101);
|
|
assert(f.radio.beacon_sends == 1 && f.radio.owned);
|
|
f.radio.complete = true; f.tick(); }
|
|
{ Fixture f; normal(f, 5000); assert(queue(f)); f.tick();
|
|
assert(f.radio.beacon_sends == 1 && !f.radio.normal_sends);
|
|
f.radio.complete = true; f.tick(); f.tick(5000);
|
|
assert(f.radio.normal_sends == 1); f.radio.complete = true; f.tick(); }
|
|
{ Fixture f; normal(f); f.tick(); assert(!queue(f)); f.radio.complete = true; f.tick(); }
|
|
{ Fixture f; f.radio.p.secondary.params.protocol = RadioProfileProtocol::Meshtext;
|
|
f.radio.p.secondary.mode = RadioProfileMode::RxTx;
|
|
assert(f.d.queueForeignRadioFrame(bytes, sizeof(bytes), 1, f.radio.p.generation[1]));
|
|
assert(!queue(f)); f.tick(); f.radio.complete = true; f.tick(); }
|
|
}
|
|
static void budget_and_hour() {
|
|
{ Fixture f; f.d.duty_window = 200; f.d.begin();
|
|
assert(queue(f)); f.tick(); f.radio.complete = true; f.tick(100);
|
|
assert(f.d.getRemainingTxBudget() == 0 && f.d.getTotalAirTime() == 100);
|
|
assert(queue(f)); f.tick(); assert(f.radio.beacon_sends == 1);
|
|
f.d.cancelBeaconRadioFrame();
|
|
f.clock.now = 3601000; assert(queue(f)); // Admission just before the actual RF deadline is valid.
|
|
f.d.loop(); assert(f.radio.beacon_sends == 1);
|
|
f.clock.now = 3601001; f.d.loop(); assert(f.radio.beacon_sends == 1);
|
|
f.clock.now = 3601101; f.d.loop(); assert(f.radio.beacon_sends == 2);
|
|
f.radio.complete = true; f.tick(100); }
|
|
{ Fixture f; f.d.duty_window = 20; f.d.begin(); assert(queue(f)); f.tick();
|
|
assert(!f.radio.sends && f.d.hasBeaconRadioFrame());
|
|
f.tick(30000); assert(!f.d.hasBeaconRadioFrame() && !f.radio.sends); }
|
|
{ Fixture f(UINT32_MAX - 500); assert(queue(f)); f.tick();
|
|
f.radio.complete = true; f.tick(50); assert(queue(f));
|
|
f.tick(); assert(f.radio.beacon_sends == 1); f.d.cancelBeaconRadioFrame();
|
|
f.clock.now = uint32_t(UINT32_MAX - 499 + uint64_t(3600000));
|
|
assert(queue(f)); f.tick(); assert(f.radio.beacon_sends == 2);
|
|
f.radio.complete = true; f.tick(); }
|
|
}
|
|
static void hourly_cad_delay() {
|
|
Fixture f;
|
|
f.radio.cad_delay = 30;
|
|
const uint32_t first_admission = f.clock.now;
|
|
assert(queue(f)); f.tick();
|
|
assert(f.radio.beacon_sends == 1 && f.radio.beacon_starts[0] == first_admission + 31);
|
|
f.radio.complete = true; f.tick();
|
|
// The manager staggers admission, not the end of synchronous CAD. A new
|
|
// hourly slot must wait for the RF guard instead of being dropped here.
|
|
f.clock.now = first_admission + 3600000;
|
|
assert(queue(f));
|
|
f.tick(); assert(f.radio.beacon_sends == 1 && !f.radio.owned);
|
|
f.tick(30); assert(f.radio.beacon_sends == 1 && !f.radio.owned);
|
|
f.tick(71); assert(f.radio.beacon_sends == 2 && f.radio.owned);
|
|
assert(uint32_t(f.radio.beacon_starts[1] - f.radio.beacon_starts[0]) >= 3600000);
|
|
f.radio.complete = true; f.tick();
|
|
}
|
|
static void sleep_deadlines() {
|
|
Fixture f; f.d.interval = 8000; f.d.adaptive = true; f.tick();
|
|
uint32_t delay = 0;
|
|
f.d.setBeaconWakeDelay(f.clock.now, 3600000, true);
|
|
assert(f.d.getNextQueueWakeDelay(delay) && delay == 8000);
|
|
f.d.setBeaconWakeDelay(f.clock.now, 1000, true);
|
|
assert(f.d.getNextQueueWakeDelay(delay) && delay == 1000);
|
|
f.d.setBeaconWakeDelay(f.clock.now, 1000, false);
|
|
assert(f.d.getNextQueueWakeDelay(delay) && delay == 8000);
|
|
assert(queue(f)); assert(f.d.getNextQueueWakeDelay(delay) && delay == 0);
|
|
f.d.cancelBeaconRadioFrame();
|
|
f.d.setBeaconWakeDelay(f.clock.now, 0, true);
|
|
assert(f.d.getNextQueueWakeDelay(delay) && delay == 0);
|
|
f.d.setRadioAvailable(false);
|
|
assert(!f.d.getNextQueueWakeDelay(delay));
|
|
}
|
|
static void begin_lifecycle() {
|
|
Fixture f; assert(queue(f)); f.tick();
|
|
assert(f.radio.owned);
|
|
f.d.begin();
|
|
assert(!f.radio.begin_while_owned && !f.radio.owned && !f.d.hasBeaconRadioFrame());
|
|
assert(f.radio.finishes == 1 && f.radio.restore_calls == 1);
|
|
}
|
|
int main(int argc, char** argv) {
|
|
assert(argc == 2);
|
|
if (!strcmp(argv[1], "basic")) basic();
|
|
else if (!strcmp(argv[1], "preparation")) preparation();
|
|
else if (!strcmp(argv[1], "failures")) failures();
|
|
else if (!strcmp(argv[1], "restoration")) restoration();
|
|
else if (!strcmp(argv[1], "availability")) availability();
|
|
else if (!strcmp(argv[1], "priority")) priority();
|
|
else if (!strcmp(argv[1], "budget")) budget_and_hour();
|
|
else if (!strcmp(argv[1], "hourly")) hourly_cad_delay();
|
|
else if (!strcmp(argv[1], "sleep")) sleep_deadlines();
|
|
else if (!strcmp(argv[1], "begin")) begin_lifecycle();
|
|
else assert(false);
|
|
puts("PASS");
|
|
}
|
|
'''
|
|
|
|
|
|
class BeaconDispatcherTest(unittest.TestCase):
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
compiler = os.environ.get("CXX") or shutil.which("g++") or shutil.which("clang++")
|
|
if not compiler:
|
|
raise unittest.SkipTest("a host C++ compiler is required")
|
|
cls.temp = tempfile.TemporaryDirectory(prefix="meshcore-beacon-dispatcher-")
|
|
cls.addClassCleanup(cls.temp.cleanup)
|
|
work = Path(cls.temp.name)
|
|
cpp = work / "dispatcher.cpp"
|
|
cpp.write_text(HARNESS, encoding="ascii")
|
|
cls.binary = work / ("dispatcher.exe" if sys.platform == "win32" else "dispatcher")
|
|
flags = ["-std=c++17", "-Wall", "-Wextra", "-Wno-unused-parameter",
|
|
"-DMESH_ENABLE_TX_BEACONS=1", "-DMESH_ENABLE_MESHTEXT_CHAT=1"]
|
|
if sys.platform.startswith("linux"):
|
|
flags += ["-fsanitize=address,undefined", "-fno-sanitize-recover=all",
|
|
"-fno-omit-frame-pointer", "-fno-pie", "-no-pie"]
|
|
cls.compiler, cls.flags, cls.cpp, cls.work = compiler, flags, cpp, work
|
|
result = cls.compile_variant(ROOT / "src/DispatcherBeacon.cpp", cls.binary)
|
|
if result.returncode:
|
|
raise AssertionError(result.stdout + result.stderr)
|
|
|
|
@classmethod
|
|
def compile_variant(cls, beacon_source, binary):
|
|
return subprocess.run(
|
|
[cls.compiler, *cls.flags, "-I" + str(ROOT / "test/mocks"), "-I" + str(ROOT / "src"),
|
|
str(ROOT / "src/Dispatcher.cpp"), str(beacon_source),
|
|
str(ROOT / "src/Packet.cpp"), str(ROOT / "src/helpers/StaticPoolPacketManager.cpp"),
|
|
str(cls.cpp), "-o", str(binary)], capture_output=True, text=True, timeout=90)
|
|
|
|
def run_case(self, name):
|
|
env = os.environ.copy()
|
|
# The legacy static pool intentionally lives for the entire firmware
|
|
# lifetime and has no teardown. Keep ASan/UBSan fatal, exclude only LSAN.
|
|
env["ASAN_OPTIONS"] = env.get("ASAN_OPTIONS", "") + ":detect_leaks=0"
|
|
result = subprocess.run(
|
|
[str(self.binary), name], capture_output=True, text=True,
|
|
env=env, cwd=self.work, timeout=30)
|
|
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
|
self.assertIn("PASS", result.stdout)
|
|
|
|
def test_one_tx_uses_private_tuple_preserves_radio2_and_accounts_airtime(self):
|
|
self.run_case("basic")
|
|
|
|
def test_prepare_busy_is_bounded_and_owned_failures_restore_exclusively(self):
|
|
self.run_case("preparation")
|
|
|
|
def test_cad_start_failure_invalid_airtime_and_tx_timeout_do_not_retry(self):
|
|
self.run_case("failures")
|
|
|
|
def test_restore_busy_blocks_normal_tx_rx_agc_and_noise_until_repaired(self):
|
|
self.run_case("restoration")
|
|
|
|
def test_suspension_and_reactivation_do_not_initialize_through_a_beacon_lease(self):
|
|
self.run_case("availability")
|
|
|
|
def test_due_normal_traffic_wins_and_meshtext_ownership_is_exclusive(self):
|
|
self.run_case("priority")
|
|
|
|
def test_budget_hour_guard_and_millisecond_rollover(self):
|
|
self.run_case("budget")
|
|
|
|
def test_hourly_admission_waits_for_actual_rf_guard_after_cad_delay(self):
|
|
self.run_case("hourly")
|
|
|
|
def test_old_hour_guard_at_admission_is_rejected_by_regression(self):
|
|
source = (ROOT / "src/DispatcherBeacon.cpp").read_text(encoding="utf-8")
|
|
marker = " const uint32_t now = _ms->getMillis();\n"
|
|
old_guard = " if (beacon_has_started && uint32_t(now - beacon_last_start) < 3600000UL) return false;\n"
|
|
self.assertIn(marker, source)
|
|
broken = self.work / "old-admission.cpp"
|
|
broken.write_text(source.replace(marker, marker + old_guard, 1), encoding="ascii")
|
|
binary = self.work / ("old-admission.exe" if sys.platform == "win32" else "old-admission")
|
|
compile_result = self.compile_variant(broken, binary)
|
|
self.assertEqual(compile_result.returncode, 0, compile_result.stdout + compile_result.stderr)
|
|
env = os.environ.copy()
|
|
env["ASAN_OPTIONS"] = env.get("ASAN_OPTIONS", "") + ":detect_leaks=0"
|
|
result = subprocess.run([str(binary), "hourly"], capture_output=True, text=True,
|
|
env=env, cwd=self.work, timeout=30)
|
|
self.assertNotEqual(result.returncode, 0, "old admission guard unexpectedly passed")
|
|
self.assertIn("queue(f)", result.stderr)
|
|
|
|
def test_beacon_sleep_deadline_is_merged_with_adaptive_agc(self):
|
|
self.run_case("sleep")
|
|
|
|
def test_begin_retires_an_in_flight_beacon_before_radio_initialization(self):
|
|
self.run_case("begin")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|