mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-10-22 11:52:29 +00:00
223 lines
9.5 KiB
Python
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()
|