356 lines
16 KiB
Python
356 lines
16 KiB
Python
"""Execute saved repeater discovery controls with real CLI and prefs images."""
|
|
from pathlib import Path
|
|
import os
|
|
import subprocess
|
|
import tempfile
|
|
import unittest
|
|
|
|
from test_repeater_trace_repeat import production_harness
|
|
from test_replay_reset_integration import extract_braced
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
MAIN = r'''
|
|
struct MyMesh {
|
|
CommonCLI _cli;
|
|
NodePrefs& _prefs = _cli.prefs;
|
|
bool handleDiscoveryResponseCommand(const char*, char*, size_t);
|
|
};
|
|
@HANDLER@
|
|
|
|
static void run(MyMesh& mesh, const char* command, const char* expected) {
|
|
char reply[160] = {};
|
|
assert(mesh.handleDiscoveryResponseCommand(command, reply, sizeof(reply)));
|
|
assert(strcmp(reply, expected) == 0);
|
|
}
|
|
|
|
static std::vector<uint8_t> image(CommonCLI& cli) {
|
|
Capture capture;
|
|
assert(writeCommonPrefsImage(capture, &cli.prefs));
|
|
return capture.bytes;
|
|
}
|
|
|
|
static void assertLoaded(const std::vector<uint8_t>& bytes, uint8_t expected) {
|
|
CommonCLI reader;
|
|
reader.prefs.discovery_responses = uint8_t(expected ^ 3);
|
|
reader.fs.files["/com_prefs"] = bytes;
|
|
reader.loadPrefsInt(&reader.fs, "/com_prefs");
|
|
assert(reader.prefs.discovery_responses == expected);
|
|
assert(reader.prefs.freq == 910 && reader.prefs.primary_radio_preamble == 48);
|
|
}
|
|
|
|
static void cliChoices() {
|
|
for (uint8_t initial : {0, 1, 2, 3}) {
|
|
for (uint8_t bit : {1, 2}) {
|
|
for (bool enabled : {false, true}) {
|
|
MyMesh mesh;
|
|
mesh._prefs.discovery_responses = initial;
|
|
const char* name = bit == 1 ? "discovery.node.responses" : "discovery.region.responses";
|
|
char command[80], expected[100];
|
|
snprintf(command, sizeof(command), "get %s", name);
|
|
run(mesh, command, (initial & bit) ? "> on" : "> off");
|
|
snprintf(command, sizeof(command), "set %s %s", name, enabled ? "on" : "off");
|
|
snprintf(expected, sizeof(expected), "OK - %s %s", name, enabled ? "on" : "off");
|
|
run(mesh, command, expected);
|
|
const uint8_t mask = enabled ? uint8_t(initial | bit) : uint8_t(initial & ~bit);
|
|
assert(mesh._prefs.discovery_responses == mask);
|
|
assert((mesh._prefs.discovery_responses & (bit ^ 3)) == (initial & (bit ^ 3)));
|
|
assert(mesh._cli._common_save_succeeded);
|
|
assert(mesh._cli.fs.files["/com_prefs"] == image(mesh._cli));
|
|
assertLoaded(mesh._cli.fs.files["/com_prefs"], mask);
|
|
snprintf(command, sizeof(command), "get %s", name);
|
|
run(mesh, command, enabled ? "> on" : "> off");
|
|
}
|
|
}
|
|
}
|
|
MyMesh mesh;
|
|
assert(mesh._prefs.discovery_responses == 3);
|
|
run(mesh, "set discovery.node.responses\t \toff", "OK - discovery.node.responses off");
|
|
run(mesh, "set discovery.region.responses off", "OK - discovery.region.responses off");
|
|
assert(mesh._prefs.discovery_responses == 0);
|
|
}
|
|
|
|
static void invalidCommands() {
|
|
for (const char* name : {"discovery.node.responses", "discovery.region.responses"}) {
|
|
for (const char* value : {"", " ", "true", "false", "1", "0", "ON", "Off",
|
|
"on reboot", "off reboot", "on junk", "off ", "on\t", "on\n", "on;off"}) {
|
|
MyMesh mesh;
|
|
mesh._prefs.discovery_responses = 1;
|
|
mesh._cli.savePrefs(&mesh._cli.fs, PrefsSaveRouting::Scope::Common);
|
|
const auto before = mesh._cli.fs.files["/com_prefs"];
|
|
char command[100], reply[160] = {};
|
|
snprintf(command, sizeof(command), "set %s %s", name, value);
|
|
assert(mesh.handleDiscoveryResponseCommand(command, reply, sizeof(reply)));
|
|
assert(strncmp(reply, "Err - use ", 10) == 0);
|
|
assert(mesh._prefs.discovery_responses == 1 && mesh._cli.fs.files["/com_prefs"] == before);
|
|
snprintf(command, sizeof(command), "get %s on", name);
|
|
assert(mesh.handleDiscoveryResponseCommand(command, reply, sizeof(reply)));
|
|
assert(strncmp(reply, "Err - use ", 10) == 0);
|
|
assert(mesh._prefs.discovery_responses == 1 && mesh._cli.fs.files["/com_prefs"] == before);
|
|
}
|
|
}
|
|
for (const char* command : {"get discovery.node.responsesx", "set discovery.region.responses_extra off",
|
|
"get discovery.node.response", "set discovery.region.responses/off", "discovery.node.responses on"}) {
|
|
MyMesh mesh;
|
|
const auto before = mesh._cli.fs.files;
|
|
char reply[160] = "unchanged";
|
|
assert(!mesh.handleDiscoveryResponseCommand(command, reply, sizeof(reply)));
|
|
assert(strcmp(reply, "unchanged") == 0 && mesh._prefs.discovery_responses == 3);
|
|
assert(mesh._cli.fs.files == before);
|
|
}
|
|
}
|
|
|
|
static void failedSaves() {
|
|
for (uint8_t initial : {0, 1, 2, 3}) {
|
|
for (uint8_t bit : {1, 2}) {
|
|
for (unsigned fault : {0, 1, 2, 3}) {
|
|
MyMesh mesh;
|
|
mesh._prefs.discovery_responses = initial;
|
|
mesh._cli.savePrefs(&mesh._cli.fs, PrefsSaveRouting::Scope::Common);
|
|
assert(mesh._cli._common_save_succeeded);
|
|
const auto before = mesh._cli.fs.files["/com_prefs"];
|
|
if (fault == 0) mesh._cli.fs.fail_write = true;
|
|
if (fault == 1) {
|
|
const size_t offset = mesh::discovery::COMMON_LEGACY_SIZE
|
|
#ifdef WITH_RS232_BRIDGE
|
|
+ mesh::discovery::LEGACY_RS232_TAIL_SIZE
|
|
#endif
|
|
;
|
|
mesh._cli.fs.fail_write_after = (initial ^ bit) == 3 ? offset - 1 : offset + 4;
|
|
}
|
|
if (fault == 2) mesh._cli.fs.fail_read_open = true;
|
|
if (fault == 3) mesh._cli.fs.fail_rename = 1;
|
|
char command[100];
|
|
snprintf(command, sizeof(command), "set discovery.%s.responses %s",
|
|
bit == 1 ? "node" : "region", (initial & bit) ? "off" : "on");
|
|
run(mesh, command, "Err - save unconfirmed; reboot may differ");
|
|
assert(mesh._prefs.discovery_responses == initial);
|
|
assert(mesh._cli.fs.files["/com_prefs"] == before);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static void persistence() {
|
|
CommonCLI cli;
|
|
const auto baseline = image(cli);
|
|
const size_t offset = mesh::discovery::COMMON_LEGACY_SIZE
|
|
#ifdef WITH_RS232_BRIDGE
|
|
+ mesh::discovery::LEGACY_RS232_TAIL_SIZE
|
|
#endif
|
|
;
|
|
assert(cli.prefs.discovery_responses == 3);
|
|
for (uint8_t mask : {0, 1, 2, 3}) {
|
|
cli.prefs.discovery_responses = mask;
|
|
cli.savePrefs(&cli.fs, PrefsSaveRouting::Scope::Common);
|
|
assert(cli._common_save_succeeded);
|
|
const auto saved = image(cli);
|
|
assert(cli.fs.files["/com_prefs"] == saved);
|
|
assert(saved.size() == baseline.size() + (mask == 3 ? 0 : mesh::discovery::PREFS_SIZE));
|
|
assert(std::equal(saved.begin(), saved.begin() + offset, baseline.begin()));
|
|
assertLoaded(saved, mask);
|
|
if (mask == 3) continue;
|
|
uint8_t decoded = 3;
|
|
assert(mesh::discovery::decodePrefs(saved.data() + offset, mesh::discovery::PREFS_SIZE, decoded));
|
|
assert(decoded == mask);
|
|
|
|
// All historical prefixes and partial optional records keep replies ON,
|
|
// even when a previous load into the same object had disabled them.
|
|
for (size_t length = 0; length < offset + mesh::discovery::PREFS_SIZE; ++length) {
|
|
CommonCLI reader;
|
|
reader.prefs.discovery_responses = 0;
|
|
reader.fs.files["/com_prefs"] = {saved.begin(), saved.begin() + length};
|
|
reader.loadPrefsInt(&reader.fs, "/com_prefs");
|
|
assert(reader.prefs.discovery_responses == 3);
|
|
}
|
|
for (size_t byte = 0; byte < mesh::discovery::PREFS_SIZE; ++byte) {
|
|
auto damaged = saved;
|
|
damaged[offset + byte] ^= 0x80;
|
|
assertLoaded(damaged, 3);
|
|
}
|
|
}
|
|
CommonCLI missing;
|
|
missing.prefs.discovery_responses = 0;
|
|
missing.loadPrefsInt(&missing.fs, "/missing");
|
|
assert(missing.prefs.discovery_responses == 3);
|
|
|
|
// A complete, checksummed record with a forbidden mask must still reject.
|
|
auto invalid = baseline;
|
|
uint8_t record[mesh::discovery::PREFS_SIZE];
|
|
mesh::discovery::encodePrefs(record, 0);
|
|
record[4] = 4;
|
|
const uint16_t crc = mesh::discovery::checksum(record, 6);
|
|
record[6] = uint8_t(crc >> 8); record[7] = uint8_t(crc);
|
|
invalid.insert(invalid.begin() + offset, record, record + sizeof(record));
|
|
assertLoaded(invalid, 3);
|
|
|
|
#if defined(ESP32_PLATFORM)
|
|
// The optional discovery record must not displace the existing CPU footer.
|
|
cli.prefs.discovery_responses = 0;
|
|
cli.prefs.cpu_freq_mhz = 80;
|
|
auto combined = image(cli);
|
|
CommonCLI cpu;
|
|
cpu.fs.files["/com_prefs"] = combined;
|
|
cpu.loadPrefsInt(&cpu.fs, "/com_prefs");
|
|
assert(cpu.prefs.discovery_responses == 0 && cpu.prefs.cpu_freq_mhz == 80);
|
|
#endif
|
|
#if defined(RAK_4631)
|
|
cli.prefs.discovery_responses = 1;
|
|
cli.prefs.peripherals_shared = 1;
|
|
auto combined = image(cli);
|
|
CommonCLI peripheral;
|
|
peripheral.fs.files["/com_prefs"] = combined;
|
|
peripheral.loadPrefsInt(&peripheral.fs, "/com_prefs");
|
|
assert(peripheral.prefs.discovery_responses == 1 && peripheral.prefs.peripherals_shared == 1);
|
|
#endif
|
|
#ifndef WITH_RS232_BRIDGE
|
|
// Loading an old UART profile on a non-UART image must still find its saved
|
|
// discovery record after the source image's checked ten-byte power tail.
|
|
CommonCLI plain;
|
|
auto uart = image(plain);
|
|
uint8_t power[mesh::bridge::rs232power::PREFS_SIZE];
|
|
mesh::bridge::rs232power::encodePrefs(power, 1, 1, 300);
|
|
mesh::discovery::encodePrefs(record, 2);
|
|
uart.insert(uart.begin() + mesh::discovery::COMMON_LEGACY_SIZE, power, power + sizeof(power));
|
|
uart.insert(uart.begin() + mesh::discovery::COMMON_LEGACY_SIZE + sizeof(power),
|
|
record, record + sizeof(record));
|
|
assertLoaded(uart, 2);
|
|
#endif
|
|
}
|
|
|
|
int main() {
|
|
cliChoices();
|
|
invalidCommands();
|
|
failedSaves();
|
|
persistence();
|
|
}
|
|
'''
|
|
|
|
OUTBOUND = r'''
|
|
#include <cassert>
|
|
#include <cstdint>
|
|
#include <cstring>
|
|
#include <initializer_list>
|
|
#include <helpers/DiscoveryResponsePrefs.h>
|
|
#define PUB_KEY_SIZE 32
|
|
#define PAYLOAD_TYPE_ANON_REQ 7
|
|
namespace mesh {
|
|
struct Identity {
|
|
void calcSharedSecret(uint8_t* out, const Identity&) { memset(out, 0xa5, PUB_KEY_SIZE); }
|
|
};
|
|
struct Packet { uint8_t payload[16] = {}; size_t length = 0; };
|
|
}
|
|
struct MyMesh {
|
|
static constexpr unsigned NEIGHBOR_DISCOVER_MIN_FREE_PACKETS = @MIN_FREE@;
|
|
struct { uint8_t discovery_responses = 3; } _prefs;
|
|
struct Manager { unsigned free = NEIGHBOR_DISCOVER_MIN_FREE_PACKETS;
|
|
unsigned getFreeCount() { return free; }
|
|
} manager;
|
|
Manager* _mgr = &manager;
|
|
struct Clock { uint32_t unique = 1000; uint32_t getCurrentTimeUnique() { return ++unique; } } clock;
|
|
Clock* getRTCClock() { return &clock; }
|
|
mesh::Identity self_id;
|
|
mesh::Packet packet;
|
|
bool allocation_ok = true;
|
|
unsigned allocations = 0, sends = 0;
|
|
mesh::Packet* createAnonDatagram(unsigned type, const mesh::Identity&,
|
|
const mesh::Identity&, const uint8_t*, const uint8_t* bytes, size_t length) {
|
|
++allocations;
|
|
assert(type == PAYLOAD_TYPE_ANON_REQ && length == 6);
|
|
if (!allocation_ok) return nullptr;
|
|
memcpy(packet.payload, bytes, length); packet.length = length;
|
|
return &packet;
|
|
}
|
|
void sendDirect(mesh::Packet* sent, const uint8_t* path, uint8_t length, uint32_t delay) {
|
|
assert(sent == &packet && path == nullptr && length == 0 && delay == 0); ++sends;
|
|
}
|
|
mesh::Packet* sendAnonRegionsReq(const mesh::Identity&, uint32_t&);
|
|
};
|
|
@CONSTANTS@
|
|
@METHOD@
|
|
int main() {
|
|
const mesh::Identity target;
|
|
for (uint8_t mask : {0, 1, 2, 3}) {
|
|
MyMesh mesh;
|
|
mesh._prefs.discovery_responses = mask;
|
|
uint32_t tag = 99;
|
|
assert(mesh.sendAnonRegionsReq(target, tag) == &mesh.packet);
|
|
assert(mesh.allocations == 1 && mesh.sends == 1 && tag == 1001);
|
|
uint32_t sent_tag = 0; memcpy(&sent_tag, mesh.packet.payload, sizeof(sent_tag));
|
|
assert(sent_tag == tag && mesh.packet.payload[4] == ANON_REQ_TYPE_REGIONS
|
|
&& mesh.packet.payload[5] == 0);
|
|
mesh.manager.free = MyMesh::NEIGHBOR_DISCOVER_MIN_FREE_PACKETS - 1;
|
|
assert(mesh.sendAnonRegionsReq(target, tag) == nullptr);
|
|
assert(mesh.allocations == 1 && mesh.sends == 1 && tag == 1001);
|
|
mesh.manager.free = MyMesh::NEIGHBOR_DISCOVER_MIN_FREE_PACKETS;
|
|
mesh.allocation_ok = false;
|
|
assert(mesh.sendAnonRegionsReq(target, tag) == nullptr);
|
|
assert(mesh.allocations == 2 && mesh.sends == 1);
|
|
}
|
|
}
|
|
'''
|
|
|
|
|
|
class RepeaterDiscoverySettingsTests(unittest.TestCase):
|
|
def test_response_choices_do_not_disable_outbound_region_queries(self):
|
|
import re
|
|
source = (ROOT / 'examples/simple_repeater/MyMesh.cpp').read_text()
|
|
header = (ROOT / 'examples/simple_repeater/MyMesh.h').read_text()
|
|
minimum = re.search(r'NEIGHBOR_DISCOVER_MIN_FREE_PACKETS\s*=\s*(\d+)', header).group(1)
|
|
constant = re.search(r'^#define ANON_REQ_TYPE_REGIONS\s+[^\n]+', source, re.M).group()
|
|
code = (OUTBOUND.replace('@MIN_FREE@', minimum).replace('@CONSTANTS@', constant)
|
|
.replace('@METHOD@', extract_braced(source, 'mesh::Packet* MyMesh::sendAnonRegionsReq(')))
|
|
with tempfile.TemporaryDirectory(prefix='repeater-discovery-outbound-') as directory:
|
|
work = Path(directory)
|
|
(work / 'test.cpp').write_text(code)
|
|
built = subprocess.run([os.environ.get('CXX', 'g++'), '-std=c++17',
|
|
'-Wall', '-Wextra', '-Werror', '-I' + str(ROOT / 'src'),
|
|
str(work / 'test.cpp'), '-o', str(work / 'test')],
|
|
capture_output=True, text=True, timeout=60)
|
|
self.assertEqual(built.returncode, 0, built.stderr)
|
|
tested = subprocess.run([str(work / 'test')], capture_output=True, text=True, timeout=10)
|
|
self.assertEqual(tested.returncode, 0, tested.stdout + tested.stderr)
|
|
|
|
def test_real_commands_saved_images_failures_and_legacy_defaults(self):
|
|
source = (ROOT / 'src/helpers/CommonCLI.cpp').read_text()
|
|
header = (ROOT / 'src/helpers/CommonCLI.h').read_text()
|
|
repeater = (ROOT / 'examples/simple_repeater/MyMesh.cpp').read_text()
|
|
code = production_harness(source, header).split('int main() {', 1)[0]
|
|
code = code.replace(' bool trySavePrefs(', ' bool saveCommonPrefs();\n bool trySavePrefs(', 1)
|
|
code = ('#include <algorithm>\n#include <helpers/PeripheralModePrefs.h>\n'
|
|
'#include <helpers/bridges/RS232PowerSettings.h>\n' + code)
|
|
code += extract_braced(source, 'bool CommonCLI::saveCommonPrefs(') + '\n'
|
|
code += MAIN.replace('@HANDLER@', extract_braced(
|
|
repeater, 'bool MyMesh::handleDiscoveryResponseCommand('))
|
|
sanitizer_flags = [] if os.name == 'nt' else [
|
|
'-fsanitize=address,undefined', '-fno-sanitize-recover=all', '-fno-pie', '-no-pie']
|
|
variants = [('ENABLE_OTA', platform) for platform in (
|
|
'NRF52_PLATFORM', 'STM32_PLATFORM', 'ESP32_PLATFORM', 'RP2040_PLATFORM')]
|
|
variants += [('NRF52_PLATFORM', 'RAK_4631'),
|
|
('ENABLE_OTA', 'ESP32_PLATFORM', 'WITH_MQTT_BRIDGE'),
|
|
('NRF52_PLATFORM', 'WITH_RS232_BRIDGE', 'WITH_RS232_BRIDGE_UART=2')]
|
|
with tempfile.TemporaryDirectory(prefix='repeater-discovery-settings-') as directory:
|
|
work = Path(directory)
|
|
transaction = (ROOT / 'src/helpers/ContactFileTransaction.h').read_text()
|
|
(work / 'ContactFileTransaction.h').write_text(transaction.replace(
|
|
'#include "IdentityStore.h"', '#include <helpers/IdentityStore.h>'))
|
|
(work / 'test.cpp').write_text(code)
|
|
for macros in variants:
|
|
with self.subTest(macros=macros):
|
|
binary = work / ('test.exe' if os.name == 'nt' else 'test')
|
|
built = subprocess.run([
|
|
os.environ.get('CXX', 'g++'), '-std=c++17',
|
|
*['-D' + macro if '=' in macro else '-D' + macro + '=1' for macro in macros],
|
|
*sanitizer_flags, '-I' + str(work),
|
|
'-I' + str(ROOT / 'test/fixtures/radio_profiles/mocks'),
|
|
'-I' + str(ROOT / 'src'), '-I' + str(ROOT / 'src/helpers'),
|
|
str(work / 'test.cpp'), '-o', str(binary)],
|
|
capture_output=True, text=True, timeout=60)
|
|
self.assertEqual(built.returncode, 0, built.stderr)
|
|
tested = subprocess.run([str(binary)], capture_output=True, text=True, timeout=15)
|
|
self.assertEqual(tested.returncode, 0, tested.stdout + tested.stderr)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|