150 lines
8.2 KiB
Python
150 lines
8.2 KiB
Python
"""Keep canonical retry preference bytes while batching their atomic write."""
|
|
from pathlib import Path
|
|
import shutil
|
|
import subprocess
|
|
import tempfile
|
|
import unittest
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
PREFIX = r'''
|
|
#include <cstdint>
|
|
#include <cstddef>
|
|
#include <cassert>
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <vector>
|
|
#include <algorithm>
|
|
#include <limits>
|
|
#define DEFAULT_POWERSAVING_ENABLED 0
|
|
#define DEFAULT_ADVERT_LOC_POLICY 0
|
|
#ifndef FLOOD_RETRY_PREFIX_SLOTS
|
|
#define FLOOD_RETRY_PREFIX_SLOTS 8
|
|
#define FLOOD_RETRY_PREFIX_LEN 3
|
|
#define FLOOD_RETRY_BRIDGE_BUCKETS 6
|
|
#define FLOOD_RETRY_BUCKET_PREFIXES 17
|
|
#define FLOOD_RETRY_IGNORE_PREFIXES 8
|
|
#endif
|
|
namespace mesh { namespace bridge {constexpr uint8_t ESPNOW_FORMAT_WRAPPED=0;} struct RepeaterRadioTiming {static constexpr uint8_t DEFAULT_RX_WATCHDOG_ENABLED=1;}; }
|
|
class ConfigSerializer {public:virtual void structure() {} uint8_t base_padding[3] = {};};
|
|
'''
|
|
|
|
WRITER = r'''
|
|
struct Writer {
|
|
std::vector<uint8_t> bytes;size_t budget=std::numeric_limits<size_t>::max();bool okay=true;
|
|
__attribute__((noinline)) size_t write(const uint8_t* p,size_t n) {
|
|
if(!okay)return 0;const size_t used=std::min(n,budget);bytes.insert(bytes.end(),p,p+used);budget-=used;
|
|
if(used!=n)okay=false;return used;
|
|
}
|
|
bool commit()const{return okay;}
|
|
};
|
|
'''
|
|
|
|
REFERENCE_WRITES = r'''
|
|
writer.write((uint8_t *)&prefs->retry_preset, sizeof(prefs->retry_preset)); // 295
|
|
writer.write((uint8_t *)&prefs->direct_retry_attempts, sizeof(prefs->direct_retry_attempts)); // 296
|
|
writer.write((uint8_t *)&prefs->direct_retry_base_ms, sizeof(prefs->direct_retry_base_ms)); // 297
|
|
writer.write((uint8_t *)&prefs->direct_retry_step_ms, sizeof(prefs->direct_retry_step_ms)); // 299
|
|
writer.write((uint8_t *)&prefs->direct_retry_snr_margin_x4, sizeof(prefs->direct_retry_snr_margin_x4)); // 301
|
|
writer.write((uint8_t *)&prefs->direct_retry_cr4_snr_x4, sizeof(prefs->direct_retry_cr4_snr_x4)); // 303
|
|
writer.write((uint8_t *)&prefs->direct_retry_cr5_snr_x4, sizeof(prefs->direct_retry_cr5_snr_x4)); // 304
|
|
writer.write((uint8_t *)&prefs->direct_retry_cr7_snr_x4, sizeof(prefs->direct_retry_cr7_snr_x4)); // 305
|
|
writer.write((uint8_t *)&prefs->direct_retry_cr8_snr_x4, sizeof(prefs->direct_retry_cr8_snr_x4)); // 306
|
|
writer.write((uint8_t *)&prefs->direct_retry_enabled, sizeof(prefs->direct_retry_enabled)); // 307
|
|
writer.write((uint8_t *)&prefs->direct_retry_cr_enabled, sizeof(prefs->direct_retry_cr_enabled)); // 308
|
|
writer.write((uint8_t *)&prefs->direct_retry_prefs_magic, sizeof(prefs->direct_retry_prefs_magic)); // 309
|
|
writer.write((uint8_t *)&prefs->flood_retry_attempts, sizeof(prefs->flood_retry_attempts)); // 311
|
|
writer.write((uint8_t *)&prefs->flood_retry_max_path, sizeof(prefs->flood_retry_max_path)); // 312
|
|
writer.write((uint8_t *)&prefs->flood_retry_prefixes[0][0], sizeof(prefs->flood_retry_prefixes)); // 313
|
|
writer.write((uint8_t *)&prefs->flood_retry_bridge_enabled, sizeof(prefs->flood_retry_bridge_enabled));
|
|
writer.write((uint8_t *)&prefs->flood_retry_bridge_buckets[0][0][0], sizeof(prefs->flood_retry_bridge_buckets));
|
|
writer.write((uint8_t *)&prefs->flood_retry_ignore_prefixes[0][0], sizeof(prefs->flood_retry_ignore_prefixes));
|
|
writer.write((uint8_t *)&prefs->flood_retry_advert_enabled, sizeof(prefs->flood_retry_advert_enabled));
|
|
writer.write((uint8_t *)&prefs->battery_alert_enabled, sizeof(prefs->battery_alert_enabled));
|
|
writer.write((uint8_t *)&prefs->battery_alert_low_percent, sizeof(prefs->battery_alert_low_percent));
|
|
writer.write((uint8_t *)&prefs->battery_alert_critical_percent, sizeof(prefs->battery_alert_critical_percent));
|
|
writer.write((uint8_t *)&prefs->direct_retry_recent_enabled, sizeof(prefs->direct_retry_recent_enabled));
|
|
writer.write((uint8_t *)&prefs->flood_channel_data_enabled, sizeof(prefs->flood_channel_data_enabled));
|
|
writer.write((uint8_t *)&prefs->legacy_flood_channel_block_max_hops,
|
|
sizeof(prefs->legacy_flood_channel_block_max_hops));
|
|
writer.write((uint8_t *)&prefs->flood_channel_data_max_hops, sizeof(prefs->flood_channel_data_max_hops));
|
|
writer.write((uint8_t *)&prefs->telemetry_access, sizeof(prefs->telemetry_access)); // 674
|
|
'''
|
|
|
|
CHECKS = r'''
|
|
#pragma GCC diagnostic push
|
|
#pragma GCC diagnostic ignored "-Winvalid-offsetof"
|
|
int main(){
|
|
NodePrefs prefs;uint32_t state=1;size_t length=0;
|
|
for(unsigned seed=0;seed<32;seed++) {
|
|
Writer expected;
|
|
auto* first=reinterpret_cast<uint8_t*>(&prefs)+__builtin_offsetof(NodePrefs,retry_preset);
|
|
const size_t size=__builtin_offsetof(NodePrefs,telemetry_access)+sizeof(prefs.telemetry_access)-__builtin_offsetof(NodePrefs,retry_preset);
|
|
for(size_t i=0;i<size;i++){state=state*1664525u+1013904223u;first[i]=uint8_t(state>>24);}
|
|
reference(expected,&prefs);length=expected.bytes.size();assert(expected.commit());
|
|
Writer actual;writeCommonRetryPrefs(actual,&prefs);assert(actual.commit()&&actual.bytes==expected.bytes);
|
|
for(size_t limit=0;limit<=length;limit++){
|
|
Writer a,b;a.budget=b.budget=limit;reference(a,&prefs);writeCommonRetryPrefs(b,&prefs);
|
|
assert(a.bytes==b.bytes&&a.commit()==b.commit());assert(a.commit()==(limit==length));
|
|
}
|
|
}
|
|
printf("PASS: exact canonical %zu-byte tail, 32 arbitrary states, every cumulative short-write boundary\n",length);
|
|
}
|
|
#pragma GCC diagnostic pop
|
|
'''
|
|
|
|
def fixture(padded=False):
|
|
header = (ROOT / "src/helpers/CommonCLI.h").read_text()
|
|
begin = header.index("class NodePrefs : public ConfigSerializer")
|
|
begin = header.index("public:", begin)
|
|
end = header.index("\nprivate:", begin)
|
|
declarations = "class NodePrefs : public ConfigSerializer {\n" + header[begin:end] + "\n};\n"
|
|
if padded:
|
|
original = " uint8_t direct_retry_attempts = 0;"
|
|
assert original in declarations
|
|
declarations = declarations.replace(original, original + "\n uint8_t layout_break = 0;", 1)
|
|
source = (ROOT / "src/helpers/CommonCLI.cpp").read_text()
|
|
begin = source.index("// This canonical retry/flood span contains no padding.")
|
|
end = source.index("void CommonCLI::savePrefs(FILESYSTEM* fs", begin)
|
|
helper = source[begin:end]
|
|
return (PREFIX + declarations + helper + WRITER
|
|
+ "void reference(Writer& writer, NodePrefs* prefs) {\n"
|
|
+ REFERENCE_WRITES + "}\n" + CHECKS)
|
|
|
|
|
|
class CommonRetryTailTests(unittest.TestCase):
|
|
def compile(self, work, definitions=(), padded=False):
|
|
compiler = shutil.which("g++") or shutil.which("clang++")
|
|
self.assertIsNotNone(compiler, "native C++ compiler required")
|
|
cpp, executable = work / "tail.cpp", work / "tail"
|
|
cpp.write_text(fixture(padded))
|
|
result = subprocess.run([
|
|
compiler, "-std=c++17", "-O1", "-Wall", "-Wextra", "-Werror",
|
|
"-Wno-misleading-indentation", "-fsanitize=address,undefined",
|
|
"-fno-sanitize-recover=all", "-fno-pie", "-no-pie", *definitions,
|
|
str(cpp), "-o", str(executable),
|
|
], capture_output=True, text=True, timeout=60)
|
|
return result, executable
|
|
|
|
def test_actual_members_match_old_bytes_and_every_short_write_boundary(self):
|
|
for definitions in ((), ("-DENABLE_OTA=1", "-DRAK_4631=1"), (
|
|
"-DFLOOD_RETRY_PREFIX_SLOTS=3", "-DFLOOD_RETRY_PREFIX_LEN=5",
|
|
"-DFLOOD_RETRY_BRIDGE_BUCKETS=2", "-DFLOOD_RETRY_BUCKET_PREFIXES=7",
|
|
"-DFLOOD_RETRY_IGNORE_PREFIXES=4")):
|
|
with self.subTest(definitions=definitions), tempfile.TemporaryDirectory() as directory:
|
|
result, executable = self.compile(Path(directory), definitions)
|
|
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
|
checked = subprocess.run([str(executable)], capture_output=True, text=True, timeout=30)
|
|
self.assertEqual(checked.returncode, 0, checked.stdout + checked.stderr)
|
|
self.assertIn("exact canonical", checked.stdout)
|
|
|
|
def test_changed_member_layout_refuses_to_compile(self):
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
result, _ = self.compile(Path(directory), padded=True)
|
|
self.assertNotEqual(result.returncode, 0)
|
|
self.assertIn("retry tail has padding or changed order", result.stderr)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|