Files
HaloKeymind/test/test_repeater_trace_forwarding.py
T

223 lines
9.5 KiB
Python

"""Execute actual repeater trace gates with the production Packet definition."""
from pathlib import Path
import os
import shutil
import subprocess
import sys
import tempfile
import unittest
from test_replay_reset_integration import extract_braced
ROOT = Path(__file__).resolve().parents[1]
HARNESS = r'''
#include <cassert>
#include <cstring>
#include <initializer_list>
#include <Packet.h>
#include <helpers/RoutingPolicy.h>
#define LOOP_DETECT_OFF 0
#define LOOP_DETECT_MINIMAL 1
#define LOOP_DETECT_MODERATE 2
#define LOOP_DETECT_STRICT 3
#define FLOOD_CHANNEL_HOPS_ALL 255
const uint8_t max_loop_minimal[] = {1}, max_loop_moderate[] = {2}, max_loop_strict[] = {3};
namespace mesh {
@CONSTRUCTOR@
// The boundary records delegation to the existing OTA/profile transmit veto.
class Mesh {
public:
mutable unsigned base_calls = 0;
bool base_allowed = true;
virtual ~Mesh() = default;
virtual bool allowPacketTransmit(const Packet*) const {
++base_calls;
return base_allowed;
}
};
}
struct SimpleMeshTables {
struct RecentRepeaterInfo { int8_t snr_x4 = 12; } recent;
bool known = false;
const RecentRepeaterInfo* findRecentRepeaterByHash(const uint8_t*, uint8_t) const {
return known ? &recent : nullptr;
}
};
struct MyMesh : mesh::Mesh {
struct Prefs {
uint8_t disable_fwd = 1, trace_when_repeat_off = 0;
uint8_t flood_max = 64, flood_max_unscoped = 64, flood_max_advert = 64;
uint8_t flood_channel_data_enabled = 1, loop_detect = LOOP_DETECT_OFF;
uint8_t direct_retry_enabled = 1, direct_retry_recent_enabled = 0;
int8_t direct_retry_snr_margin_x4 = 0;
} _prefs;
bool recv_pkt_channel_scope_rejected = false, recv_pkt_regionless_scope_set = false;
bool recv_pkt_channel_scope_bypass = false, looped = false;
const void* recv_pkt_region = this;
unsigned recv_pkt_filter_match_mask = 0;
bool filter_block = false, moderation_block = false, clock_sync_mesh_edge_enabled = false;
unsigned commits = 0, clock_samples = 0;
SimpleMeshTables tables;
bool floodChannelDataHopApplies(const mesh::Packet*) const { return true; }
bool isLooped(const mesh::Packet*, const uint8_t*) { return looped; }
bool shouldBlockFloodPacketForward(const mesh::Packet*, unsigned) { return filter_block; }
bool shouldBlockFloodGroupTextForward(const mesh::Packet*) { return moderation_block; }
void commitFloodPacketFilterRates(const mesh::Packet*, unsigned) { ++commits; }
void recordAcceptedFloodClockSample(const mesh::Packet*) { ++clock_samples; }
const SimpleMeshTables* getTables() const { return &tables; }
int8_t getDirectRetryMinSNRX4() const { return 12; }
bool allowPacketForward(const mesh::Packet*);
bool allowPacketTransmit(const mesh::Packet*) const override;
bool allowDirectRetry(const mesh::Packet*, const uint8_t*, uint8_t) const;
};
@METHODS@
int main() {
MyMesh node;
mesh::Packet p;
// Admission is an exact opt-in, not a general forwarding bypass. Test every
// route/type and invalid stored values with the actual header accessors.
for (uint8_t pref : {uint8_t(0), uint8_t(1), uint8_t(2), uint8_t(255)}) {
node._prefs.trace_when_repeat_off = pref;
for (uint8_t route = 0; route < 4; ++route) {
for (uint8_t type = 0; type < 16; ++type) {
p.header = route | (type << PH_TYPE_SHIFT);
p.path_len = 0;
assert(node.allowPacketForward(&p) ==
(pref == 1 && p.isRouteDirect() && type == PAYLOAD_TYPE_TRACE));
for (uint16_t path : {uint16_t(0), uint16_t(1), uint16_t(MAX_PATH_SIZE)}) {
p.path_len = path;
const bool blocked = pref != 1 && p.isRouteDirect()
&& type == PAYLOAD_TYPE_TRACE && path > 0;
const unsigned before = node.base_calls;
assert(node.allowPacketTransmit(&p) == !blocked);
assert(node.base_calls == before + (blocked ? 0 : 1));
assert(node.allowDirectRetry(&p, nullptr, 0) == !blocked);
}
}
}
}
assert(!node.allowPacketForward(nullptr));
assert(node.allowPacketTransmit(nullptr));
assert(node.allowDirectRetry(nullptr, nullptr, 0));
// repeat on restores the original gate; switching it off retires a relay,
// but never a locally generated trace whose SNR path starts empty.
p.header = ROUTE_TYPE_DIRECT | (PAYLOAD_TYPE_TRACE << PH_TYPE_SHIFT);
p.path_len = 1;
node._prefs.trace_when_repeat_off = 0;
node._prefs.disable_fwd = 0;
assert(node.allowPacketForward(&p) && node.allowPacketTransmit(&p));
node._prefs.disable_fwd = 1;
assert(!node.allowPacketForward(&p) && !node.allowPacketTransmit(&p));
node._prefs.trace_when_repeat_off = 1;
assert(node.allowPacketForward(&p) && node.allowPacketTransmit(&p));
node._prefs.trace_when_repeat_off = 0;
assert(!node.allowPacketTransmit(&p) && !node.allowDirectRetry(&p, nullptr, 0));
p.path_len = 0;
assert(node.allowPacketTransmit(&p) && node.allowDirectRetry(&p, nullptr, 0));
// An enabled opt-in still delegates the base hard-drain/profile safety veto.
p.path_len = 1;
node._prefs.trace_when_repeat_off = 1;
node.base_allowed = false;
assert(!node.allowPacketTransmit(&p));
node.base_allowed = true;
node._prefs.direct_retry_enabled = 0;
assert(!node.allowDirectRetry(&p, nullptr, 0));
node._prefs.direct_retry_enabled = 1;
node._prefs.direct_retry_recent_enabled = 1;
const uint8_t hop[] = {0x42};
assert(node.allowDirectRetry(&p, hop, 1)); // unknown remains eligible
node.tables.known = true;
node.tables.recent.snr_x4 = 11;
assert(!node.allowDirectRetry(&p, hop, 1));
node.tables.recent.snr_x4 = 12;
assert(node.allowDirectRetry(&p, hop, 1));
node._prefs.direct_retry_snr_margin_x4 = 1;
assert(!node.allowDirectRetry(&p, hop, 1));
// The opt-in never overrides existing flood-hop/region/scope/loop rules,
// or spends moderation quota for a packet rejected earlier in the chain.
node._prefs.disable_fwd = 0;
p.header = ROUTE_TYPE_FLOOD | (PAYLOAD_TYPE_GRP_TXT << PH_TYPE_SHIFT);
p.setPathHashSizeAndCount(1, 1);
assert(node.allowPacketForward(&p));
const unsigned committed = node.commits;
node._prefs.flood_max = 0;
assert(!node.allowPacketForward(&p));
node._prefs.flood_max = 64;
node.recv_pkt_channel_scope_rejected = true;
assert(!node.allowPacketForward(&p));
node.recv_pkt_channel_scope_rejected = false;
node.recv_pkt_region = nullptr;
assert(!node.allowPacketForward(&p));
node.recv_pkt_region = &node;
node._prefs.loop_detect = LOOP_DETECT_MINIMAL;
node.looped = true;
assert(!node.allowPacketForward(&p));
node.looped = false;
#if !defined(PORTABLE_MQTT_OBSERVER)
node.filter_block = true;
assert(!node.allowPacketForward(&p));
node.filter_block = false;
node.moderation_block = true;
assert(!node.allowPacketForward(&p));
node.moderation_block = false;
assert(node.commits == committed);
#else
(void)committed;
#endif
assert(node.allowPacketForward(&p));
node._prefs.disable_fwd = 1;
assert(!node.allowPacketForward(&p));
}
'''
class RepeaterTraceForwardingTest(unittest.TestCase):
def test_actual_repeater_methods(self):
compiler = os.environ.get('CXX') or shutil.which('g++') or shutil.which('clang++')
if not compiler:
self.skipTest('a host C++17 compiler is required')
source = (ROOT / 'examples/simple_repeater/MyMesh.cpp').read_text(encoding='utf-8')
methods = '\n'.join(extract_braced(source, signature) for signature in (
'bool MyMesh::allowPacketForward(',
'bool MyMesh::allowPacketTransmit(',
'bool MyMesh::allowDirectRetry('))
packet_source = (ROOT / 'src/Packet.cpp').read_text(encoding='utf-8')
code = HARNESS.replace('@METHODS@', methods).replace(
'@CONSTRUCTOR@', extract_braced(packet_source, 'Packet::Packet()'))
flags = ['-fsanitize=address,undefined', '-fno-sanitize-recover=all',
'-fno-pie', '-no-pie'] if sys.platform.startswith('linux') else []
with tempfile.TemporaryDirectory(prefix='repeater-trace-forwarding-') as directory:
work = Path(directory)
cpp = work / 'test.cpp'
cpp.write_text(code, encoding='utf-8')
for portable in (False, True):
with self.subTest(portable=portable):
binary = work / 'test'
built = subprocess.run([
compiler, '-std=c++17', '-Wall', '-Wextra', '-Werror',
'-Wno-unused-parameter',
'-DMESH_ENABLE_FLOOD_GROUP_MODERATION=1', '-DMESH_ENABLE_CLOCK_SYNC=1',
*(['-DPORTABLE_MQTT_OBSERVER=1'] if portable else []), *flags,
'-I' + str(ROOT / 'src'), str(cpp), '-o', str(binary)],
capture_output=True, text=True, timeout=60)
self.assertEqual(built.returncode, 0, built.stdout + built.stderr)
tested = subprocess.run([str(binary)], capture_output=True,
text=True, timeout=10)
self.assertEqual(tested.returncode, 0, tested.stdout + tested.stderr)
def test_repeater_only_override(self):
header = (ROOT / 'examples/simple_repeater/MyMesh.h').read_text(encoding='utf-8')
self.assertIn('bool allowPacketTransmit(const mesh::Packet* packet) const override;', header)
for role in ('simple_room_server', 'simple_sensor', 'companion_radio'):
for path in (ROOT / 'examples' / role).glob('MyMesh.*'):
self.assertNotIn('trace_when_repeat_off', path.read_text(encoding='utf-8'))
if __name__ == '__main__':
unittest.main()