160 lines
7.5 KiB
Python
160 lines
7.5 KiB
Python
#!/usr/bin/env python3
|
|
"""Execute preset application, role retry budgets, and the flood-count setter."""
|
|
from pathlib import Path
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import tempfile
|
|
import unittest
|
|
|
|
from test_replay_reset_integration import extract_braced
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
|
|
class RetryCrPresetsTest(unittest.TestCase):
|
|
def test_presets_and_disabling_flood_leave_direct_settings_unchanged(self):
|
|
compiler = shutil.which("g++") or shutil.which("clang++")
|
|
if compiler is None:
|
|
self.skipTest("a host C++17 compiler is required")
|
|
common = (ROOT / "src/helpers/CommonCLI.cpp").read_text()
|
|
header = (ROOT / "src/helpers/CommonCLI.h").read_text()
|
|
core = (ROOT / "src/Mesh.cpp").read_text()
|
|
repeater = (ROOT / "examples/simple_repeater/MyMesh.cpp").read_text()
|
|
definitions = "\n".join(re.findall(
|
|
r"^#define (?:RETRY_PRESET_|DIRECT_RETRY_|FLOOD_RETRY_)[A-Z0-9_]+[^\n]*",
|
|
header, re.M))
|
|
definitions += "\n" + re.search(
|
|
r"#define FLOOD_RETRY_PATH_GATE_DISABLED[^\n]*",
|
|
(ROOT / "src/Mesh.h").read_text()).group()
|
|
generated = definitions + r'''
|
|
#include <cassert>
|
|
#include <cstdint>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <vector>
|
|
#define constrain(x, low, high) ((x) < (low) ? (low) : (x) > (high) ? (high) : (x))
|
|
struct NodePrefs {
|
|
uint8_t retry_preset, direct_retry_attempts, direct_retry_enabled;
|
|
uint8_t direct_retry_cr_enabled, direct_retry_recent_enabled;
|
|
uint8_t direct_retry_prefs_magic[2], flood_retry_attempts;
|
|
uint8_t flood_retry_max_path, flood_retry_group_max_path, disable_fwd;
|
|
uint16_t direct_retry_base_ms, direct_retry_step_ms, direct_retry_snr_margin_x4;
|
|
int8_t direct_retry_cr4_snr_x4, direct_retry_cr5_snr_x4;
|
|
int8_t direct_retry_cr7_snr_x4, direct_retry_cr8_snr_x4;
|
|
};
|
|
namespace mesh {
|
|
struct Packet { uint8_t hops = 0; uint8_t getPathHashCount() const { return hops; } };
|
|
class Mesh { public:
|
|
static uint8_t getDirectRetryCodingRateForAttempt(uint8_t, uint8_t);
|
|
};
|
|
}
|
|
class MyMesh : public mesh::Mesh {
|
|
public:
|
|
NodePrefs _prefs{};
|
|
unsigned direct_cancellations = 0, flood_cancellations = 0;
|
|
uint8_t getFloodRetryMaxAttempts(const mesh::Packet*) const;
|
|
uint8_t roomFloodAttempts(const mesh::Packet*) const;
|
|
uint8_t sensorFloodAttempts(const mesh::Packet*) const;
|
|
uint8_t getDirectRetryCodingRateForSNR(int8_t) const;
|
|
void onRetryConfigChanged();
|
|
void cancelAllDirectRetries() { ++direct_cancellations; }
|
|
void cancelAllFloodRetries() { ++flood_cancellations; }
|
|
};
|
|
struct CLI {
|
|
NodePrefs* _prefs;
|
|
MyMesh* _callbacks;
|
|
unsigned saves = 0;
|
|
void savePrefs() { ++saves; }
|
|
int _atoi(const char* value) { return atoi(value); }
|
|
void set(const char* config, char* reply);
|
|
};
|
|
'''
|
|
for signature in ("static void markDirectRetryPrefsValid(",
|
|
"static void applyFloodRetryPreset(",
|
|
"static void applyDirectRetryPreset(",
|
|
"static void setDefaultDirectRetryPrefs(",
|
|
"static bool looksUnsignedInteger("):
|
|
generated += extract_braced(common, signature) + "\n"
|
|
generated += "namespace mesh {\n" + extract_braced(
|
|
core, "uint8_t Mesh::getDirectRetryCodingRateForAttempt(") + "\n}\n"
|
|
for signature in ("uint8_t MyMesh::getFloodRetryMaxAttempts(",
|
|
"uint8_t MyMesh::getDirectRetryCodingRateForSNR(",
|
|
"void MyMesh::onRetryConfigChanged("):
|
|
generated += extract_braced(repeater, signature) + "\n"
|
|
for role, owner, method in (("simple_room_server", "MyMesh", "roomFloodAttempts"),
|
|
("simple_sensor", "SensorMesh", "sensorFloodAttempts")):
|
|
source = (ROOT / f"examples/{role}/{owner}.cpp").read_text()
|
|
budget = extract_braced(source, f"uint8_t {owner}::getFloodRetryMaxAttempts(")
|
|
generated += budget.replace(f"{owner}::getFloodRetryMaxAttempts", f"MyMesh::{method}") + "\n"
|
|
setter = extract_braced(common, 'if (memcmp(config, "flood.retry.count ", 18) == 0)')
|
|
generated += "void CLI::set(const char* config, char* reply) { " + setter + " }\n"
|
|
generated += r'''
|
|
static std::vector<int> directSettings(const NodePrefs& p) {
|
|
return {p.direct_retry_attempts, p.direct_retry_enabled, p.direct_retry_cr_enabled,
|
|
p.direct_retry_recent_enabled, p.direct_retry_base_ms, p.direct_retry_step_ms,
|
|
p.direct_retry_snr_margin_x4, p.direct_retry_cr4_snr_x4, p.direct_retry_cr5_snr_x4,
|
|
p.direct_retry_cr7_snr_x4, p.direct_retry_cr8_snr_x4};
|
|
}
|
|
static std::vector<uint8_t> directSchedule(const MyMesh& node, int8_t snr) {
|
|
std::vector<uint8_t> rates;
|
|
for (uint8_t attempt = 1; attempt <= node._prefs.direct_retry_attempts; ++attempt) {
|
|
rates.push_back(node.getDirectRetryCodingRateForAttempt(
|
|
node.getDirectRetryCodingRateForSNR(snr), attempt));
|
|
}
|
|
return rates;
|
|
}
|
|
int main() {
|
|
const uint8_t presets[] = {RETRY_PRESET_INFRA, RETRY_PRESET_ROOFTOP, RETRY_PRESET_MOBILE};
|
|
const uint8_t direct_counts[] = {4, 15, 15};
|
|
const uint8_t origin_counts[] = {2, 6, 15};
|
|
const uint8_t expected_cr5[] = {5, 7, 7, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8};
|
|
for (unsigned preset = 0; preset < 3; ++preset) {
|
|
for (int8_t snr : {-40, 10, 12, 30, 40}) {
|
|
MyMesh node;
|
|
setDefaultDirectRetryPrefs(&node._prefs);
|
|
applyDirectRetryPreset(&node._prefs, presets[preset]);
|
|
assert(node._prefs.direct_retry_attempts == direct_counts[preset]);
|
|
mesh::Packet packet;
|
|
assert(node.getFloodRetryMaxAttempts(&packet) == origin_counts[preset]);
|
|
assert(node.roomFloodAttempts(&packet) == origin_counts[preset]);
|
|
assert(node.sensorFloodAttempts(&packet) == origin_counts[preset]);
|
|
for (uint8_t i = 0; i < origin_counts[preset]; ++i) {
|
|
assert(node.getDirectRetryCodingRateForAttempt(5, i + 1) == expected_cr5[i]);
|
|
}
|
|
const auto settings = directSettings(node._prefs);
|
|
const auto schedule = directSchedule(node, snr);
|
|
CLI cli{&node._prefs, &node};
|
|
char reply[160] = {};
|
|
cli.set("flood.retry.count 0", reply);
|
|
assert(strcmp(reply, "OK") == 0 && cli.saves == 1);
|
|
assert(node._prefs.retry_preset == RETRY_PRESET_CUSTOM);
|
|
assert(node.getFloodRetryMaxAttempts(&packet) == 0);
|
|
assert(node.roomFloodAttempts(&packet) == 0);
|
|
assert(node.sensorFloodAttempts(&packet) == 0);
|
|
assert(node.flood_cancellations == 1 && node.direct_cancellations == 0);
|
|
assert(directSettings(node._prefs) == settings);
|
|
assert(directSchedule(node, snr) == schedule);
|
|
}
|
|
}
|
|
puts("15 preset/disabled-flood schedule checks passed");
|
|
}
|
|
'''
|
|
with tempfile.TemporaryDirectory(prefix=".tmp-retry-cr-", dir=ROOT) as directory:
|
|
work = Path(directory)
|
|
source = work / "retry-cr.cpp"
|
|
source.write_text(generated, encoding="ascii")
|
|
binary = work / "retry-cr"
|
|
result = subprocess.run([compiler, "-std=c++17", "-Wall", "-Wextra",
|
|
str(source), "-o", str(binary)],
|
|
capture_output=True, text=True, timeout=60)
|
|
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
|
checked = subprocess.run([str(binary)], capture_output=True, text=True, timeout=10)
|
|
self.assertEqual(checked.returncode, 0, checked.stdout + checked.stderr)
|
|
self.assertIn("15 preset/disabled-flood schedule checks passed", checked.stdout)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|