Files
MeshCore/test/test_radio_reply_mutation.py
T
mikecarper 0be4c7488c Add bounded Morse receive and negotiated RS232 autosleep
Add native narrow-band CW capture, ACK-first remote sessions, retained LoRa-readable pages, checked restoration and transport-safe progress. Add stock-compatible negotiated heartbeat/autosleep and recovery. Include host regressions and identity-bound HIL runners; keep unverified faster tuning HIL-only and document outstanding hardware validation.
2026-10-13 01:24:39 -07:00

207 lines
8.5 KiB
Python

"""Compile production secondary CLI and Dispatcher with injected transport/storage faults."""
from pathlib import Path
import os
import shutil
import subprocess
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[1]
def method(text, signature):
start = text.index(signature)
end = text.index('{', start) + 1
depth = 1
while depth:
depth += (text[end] == '{') - (text[end] == '}')
end += 1
return text[start:end]
LIFECYCLE = r'''
#include <cassert>
#include <cstddef>
#include <cstdint>
#include <vector>
#include <helpers/TempRadioReplyBarrier.h>
static uint32_t now_ms = 1000;
uint32_t millis() { return now_ms; }
namespace mesh {
struct Packet {};
struct Mesh {
virtual void onSendComplete(Packet*) {}
virtual void onSendFail(Packet*) {}
virtual void onRadioProfileCopyQueued(Packet*, const Packet*, uint8_t) {}
};
}
struct Profiles {
bool pending = false, accepted = false;
unsigned completions = 0;
bool finishReplyMutation(bool delivered) {
++completions; accepted = delivered; pending = delivered; return true;
}
};
struct CLI {
Profiles profiles;
struct NoMorseReply {
void complete(const void*) {}
void fail(const void*) {}
void copy(const void*, const void*) {}
} morse;
NoMorseReply& morseReply() { return morse; }
Profiles& radioProfiles() { return profiles; }
};
struct Manager {
std::vector<mesh::Packet*> queue;
int getOutboundTotal() const { return queue.size(); }
mesh::Packet* getOutboundByIdx(int i) { return queue[i]; }
mesh::Packet* removeOutboundByIdx(int i) {
auto* packet = queue[i]; queue.erase(queue.begin()+i); return packet;
}
};
struct MyMesh : mesh::Mesh {
CLI _cli;
Manager manager; Manager* _mgr = &manager;
mesh::TempRadioReplyBarrier temp_radio_reply_barrier;
bool radio_reply_secondary = false, primary_radio_mutation_starts_temp = false;
uint32_t radio_reply_deadline = 0;
mesh::Packet* pending_battery_alert_packet = nullptr;
bool battery_alert_sent = false;
uint64_t last_battery_alert_sent = 0, uptime_millis = 0;
uint32_t last_millis = 0;
mesh::Packet* outbound = nullptr;
unsigned released = 0, normal_schedules = 0, cancellations = 0;
const mesh::Packet* getOutboundInFlight() const { return outbound; }
void cancelOutboundRadioRetry(const mesh::Packet* p) { assert(p == outbound); ++cancellations; }
void releasePacket(mesh::Packet*) { ++released; }
bool scheduleNormalRadio() { ++normal_schedules; return true; }
void onSendComplete(mesh::Packet*) override;
void onSendFail(mesh::Packet*) override;
void onRadioProfileCopyQueued(mesh::Packet*, const mesh::Packet*, uint8_t) override;
void finishRadioReply(bool);
void serviceRadioReplyDeadline();
void arm(mesh::Packet* a, mesh::Packet* b = nullptr) {
temp_radio_reply_barrier.prepare(a);
if (b) onRadioProfileCopyQueued(b, a, 0);
temp_radio_reply_barrier.arm(a);
radio_reply_deadline = now_ms + 300000;
}
};
@METHODS@
int main() {
unsigned scenarios = 0;
for (bool secondary : {false, true}) for (unsigned success = 0; success < 4; ++success) {
MyMesh m; mesh::Packet a, b, unrelated;
m.radio_reply_secondary = secondary;
m.primary_radio_mutation_starts_temp = true;
m._cli.profiles.pending = secondary;
m.arm(&a, &b);
m.onSendComplete(&unrelated);
assert(m.temp_radio_reply_barrier.waiting());
if (success & 1) m.onSendComplete(&a); else m.onSendFail(&a);
assert(m.temp_radio_reply_barrier.waiting());
assert(m._cli.profiles.completions == 0 && m.normal_schedules == 0);
if (success & 2) m.onSendComplete(&b); else m.onSendFail(&b);
assert(!m.temp_radio_reply_barrier.waiting());
assert(m.radio_reply_deadline == 0);
if (secondary) {
assert(m._cli.profiles.completions == 1 && m._cli.profiles.accepted == (success != 0));
assert(m.normal_schedules == 0);
} else assert(m.normal_schedules == (success == 0 ? 1U : 0U));
++scenarios;
}
{ // Deleting an active schedule may restore saved radio, but must not erase other slots on ACK failure.
MyMesh m; mesh::Packet a;
m.primary_radio_mutation_starts_temp = false;
m.arm(&a); m.onSendFail(&a);
assert(!m.temp_radio_reply_barrier.waiting() && m.normal_schedules == 0);
++scenarios;
}
for (bool rollover : {false, true}) {
now_ms = rollover ? UINT32_MAX - 10 : 1000;
MyMesh m; mesh::Packet a, b, ordinary;
m.radio_reply_secondary = true;
m.arm(&a, &b); m.manager.queue = {&a, &ordinary, &b};
now_ms += 299999; m.serviceRadioReplyDeadline();
assert(m.temp_radio_reply_barrier.waiting() && m.released == 0);
++now_ms; m.serviceRadioReplyDeadline();
assert(!m.temp_radio_reply_barrier.waiting() && m.released == 2);
assert(m.manager.queue.size() == 1 && m.manager.queue[0] == &ordinary);
assert(!m._cli.profiles.accepted && m._cli.profiles.completions == 1);
++scenarios;
}
{ // A physically transmitting copy retains ownership until its actual completion/watchdog.
MyMesh m; mesh::Packet a, b;
m.radio_reply_secondary = true;
m.arm(&a, &b); m.outbound = &a; m.manager.queue = {&b};
now_ms += 300000; m.serviceRadioReplyDeadline();
assert(m.cancellations == 1 && m.released == 1);
assert(m.temp_radio_reply_barrier.waiting() && m._cli.profiles.completions == 0);
m.onSendComplete(&a);
assert(!m.temp_radio_reply_barrier.waiting() && m._cli.profiles.accepted);
++scenarios;
}
{ // Once one copy succeeded, a timeout can retire the other without canceling the mutation.
MyMesh m; mesh::Packet a, b;
m.radio_reply_secondary = true;
m.arm(&a, &b); m.onSendComplete(&a); m.manager.queue = {&b};
now_ms += 300000; m.serviceRadioReplyDeadline();
assert(m.released == 1 && m._cli.profiles.accepted);
assert(!m.temp_radio_reply_barrier.waiting());
++scenarios;
}
assert(scenarios == 13);
}
'''
class RadioReplyMutationTest(unittest.TestCase):
@staticmethod
def compiler_flags():
return ['-std=c++17', '-Wall', '-Wextra'] + ([] if os.name == 'nt' else
['-fsanitize=address,undefined', '-fno-omit-frame-pointer', '-fno-pie', '-no-pie'])
def test_radio_transactions_and_coding_rate_restore(self):
compiler = shutil.which('g++') or shutil.which('clang++')
self.assertIsNotNone(compiler)
with tempfile.TemporaryDirectory() as work:
exe = Path(work) / 'radio_replies.exe'
result = subprocess.run([compiler, *self.compiler_flags(),
'-I', str(ROOT/'test/fixtures/radio_profiles/mocks'),
'-I', str(ROOT/'test/mocks'), '-I', str(ROOT/'src'),
str(ROOT/'src/helpers/RadioProfileCLI.cpp'),
str(ROOT/'src/Dispatcher.cpp'), str(ROOT/'src/Packet.cpp'),
str(ROOT/'src/helpers/StaticPoolPacketManager.cpp'),
str(ROOT/'test/fixtures/radio_profiles/reply_mutation_test.cpp'),
'-o', str(exe)], capture_output=True, text=True)
self.assertEqual(result.returncode, 0, result.stderr[-14000:])
env = os.environ.copy()
# The production pool is a boot-lifetime singleton without a
# destructor; this harness constructs several independent pools.
# Keep bounds/UAF/UB sanitizers, but don't count that intentional
# lifetime allocation as a host-process teardown leak.
if os.name != 'nt':
env['ASAN_OPTIONS'] = env.get('ASAN_OPTIONS', '') + ':detect_leaks=0'
subprocess.run([str(exe)], check=True, env=env)
def test_production_repeater_ack_callbacks_and_deadline(self):
source = (ROOT/'examples/simple_repeater/MyMesh.cpp').read_text(encoding='utf-8')
methods = '\n'.join(method(source, signature) for signature in (
'void MyMesh::onSendComplete(', 'void MyMesh::onSendFail(',
'void MyMesh::onRadioProfileCopyQueued(', 'void MyMesh::finishRadioReply(',
'void MyMesh::serviceRadioReplyDeadline('))
compiler = shutil.which('g++') or shutil.which('clang++')
self.assertIsNotNone(compiler)
with tempfile.TemporaryDirectory() as work:
cpp = Path(work)/'lifecycle.cpp'; exe = Path(work)/'lifecycle.exe'
cpp.write_text(LIFECYCLE.replace('@METHODS@', methods), encoding='utf-8')
result = subprocess.run([compiler, *self.compiler_flags(), '-I', str(ROOT/'src'),
str(cpp), '-o', str(exe)], capture_output=True, text=True)
self.assertEqual(result.returncode, 0, result.stderr[-14000:])
subprocess.run([str(exe)], check=True)
if __name__ == '__main__':
unittest.main()