Files
MeshCore/test/test_companion_prefs_image.py
T
mikecarper 6ce784bbd6 Fit all STM32 Companions with checked fractional preferences
Stage STM32 preference prefixes and checked footers in one atomic image, reject declared tails that encounter premature EOF, and share the typed CLI rollback helper. Use the proven Wio Mini code-layout flags on Wio E5 and Tiny Relay without changing features or memory boundaries. All four STM32 targets and RAK4631 Full pass at the full 1.18.0.1 release version length.
2026-10-18 09:11:51 -07:00

558 lines
26 KiB
Python

"""Check explicit preference bytes, STM32 staging and nRF field streaming."""
from pathlib import Path
import os
import subprocess
import tempfile
import unittest
from test_companion_preferences_transaction import HARNESS, esp_recovery_helpers
from test_radio_receive_contract import method
ROOT = Path(__file__).resolve().parents[1]
# Published field order and sizes, independent of the implementation's emitter
# and the class's in-memory member order or padding. None represents zero pad.
WIRE_FIELDS = (
('airtime_factor', 4), ('node_name', 32), (None, 4),
('@lat', 8), ('@lon', 8), ('freq', 4), ('sf', 1), ('cr', 1),
('client_repeat', 1), ('manual_add_contacts', 1), ('bw', 4),
('tx_power_dbm', 1), ('telemetry_mode_base', 1), ('telemetry_mode_loc', 1),
('telemetry_mode_env', 1), ('rx_delay_base', 4), ('advert_loc_policy', 1),
('multi_acks', 1), ('path_hash_mode', 1), (None, 1), ('ble_pin', 4),
('buzzer_quiet', 1), ('gps_enabled', 1), ('gps_interval', 4),
('autoadd_config', 1), ('autoadd_max_hops', 1), ('rx_boosted_gain', 1),
('default_scope_name', 31), ('default_scope_key', 16),
('radio_fem_rxgain', 1), ('radio_fem_rxgain_override', 1),
('vibe_quiet', 1), ('radio_fem_txgain', 1), ('rx_powersaving_enabled', 1),
('rx_ps_rx_us', 4), ('rx_ps_sleep_us', 4), ('rx_ps_level', 1),
('rx_ps_preamble', 1), ('powersaving_enabled', 1), ('wifi_enabled', 1),
('powersaving_policy_version', 1), ('usb_logging_enabled', 1),
('bluetooth_name', 32), ('display_rotation_degrees', 2), ('cad_enabled', 1),
('cad_scan_timeout_ms', 2), ('cad_retry_delay_ms', 2),
('cad_max_duration_ms', 2), ('bluetooth_mac_mode', 1), ('bluetooth_mac', 6),
('bluetooth_stealth_peer_type', 1), ('bluetooth_stealth_peer', 6),
('bluetooth_stealth_mode', 1), ('tx_delay_factor', 4),
('direct_tx_delay_factor', 4), ('interference_threshold', 1),
('agc_reset_interval', 1), ('tz_offset', 1),
)
TAIL_FIELDS = (
('flood_retry_attempts', 1), ('flood_retry_max_path', 1),
('flood_retry_group_max_path', 1), ('flood_retry_advert_enabled', 1),
('one_key_dm_enabled', 1), ('bluetooth_enabled', 1),
('gps_sync_interval_hours', 2), ('usb_debug_enabled', 1),
('lost_reply', 1),
)
OPTIONAL_FIELDS = (
('defined(TBEAM_1W)', (('fan_mode', 5), ('fan_lo', 1), ('fan_hi', 1))),
('defined(RP2040_PLATFORM) && defined(ENABLE_WIFI_INTERFACE)',
(('wifi_ssid', 33), ('wifi_pwd', 64))),
)
TESTS = r'''
#include <algorithm>
#include <vector>
static void seed(CompanionNodePrefs& prefs, uint32_t& random) {
auto fill = [&](void* destination, size_t size) {
auto* bytes = static_cast<uint8_t*>(destination);
for (size_t i = 0; i < size; ++i) {
random = random * 1664525u + 1013904223u;
bytes[i] = static_cast<uint8_t>(random >> 24);
}
};
@SEED@
prefs.tx_media = random % 5;
prefs.tx_ssh = (random >> 8) % 5;
#if defined(RAK_4631)
prefs.peripherals_shared = uint8_t(random >> 16);
#endif
}
static std::vector<uint8_t> golden(const CompanionNodePrefs& prefs,
double lat, double lon) {
std::vector<uint8_t> bytes;
auto append = [&](const void* data, size_t size) {
const auto* first = static_cast<const uint8_t*>(data);
bytes.insert(bytes.end(), first, first + size);
};
@GOLDEN@
#if defined(TBEAM_1W)
assert(bytes.size() == 243);
#elif defined(RP2040_PLATFORM) && defined(ENABLE_WIFI_INTERFACE)
assert(bytes.size() == 333);
#else
assert(bytes.size() == 236);
#endif
#if defined(RAK_4631)
bytes.insert(bytes.end(), {'P', 'M', 'O', 1,
uint8_t(prefs.peripherals_shared == 0 ? 0 : 1), 0});
uint16_t peripheral_crc = 0xffff;
for (size_t i = bytes.size() - 6; i < bytes.size(); ++i) {
peripheral_crc ^= uint16_t(bytes[i]) << 8;
for (unsigned bit = 0; bit < 8; ++bit)
peripheral_crc = (peripheral_crc & 0x8000)
? uint16_t((peripheral_crc << 1) ^ 0x1021) : uint16_t(peripheral_crc << 1);
}
bytes.push_back(uint8_t(peripheral_crc >> 8)); bytes.push_back(uint8_t(peripheral_crc));
#endif
// Independent wire definition of the new checked traffic footer. Keep
// every historical byte above unchanged and encode the public policy tags.
bytes.insert(bytes.end(), {'T', 'X', 'R', 1,
uint8_t(prefs.tx_media ? 0xa0 | prefs.tx_media : 0),
uint8_t(prefs.tx_ssh ? 0xa0 | prefs.tx_ssh : 0)});
uint16_t crc = 0xffff;
for(size_t i=bytes.size()-6;i<bytes.size();++i){
crc ^= uint16_t(bytes[i]) << 8;
for(unsigned bit=0;bit<8;++bit)
crc = (crc&0x8000) ? uint16_t((crc<<1)^0x1021) : uint16_t(crc<<1);
}
bytes.push_back(uint8_t(crc>>8));bytes.push_back(uint8_t(crc));
return bytes;
}
static void unchanged(const DataStore& store,
const std::vector<uint8_t>& before) {
assert(store.fs.files.at("/new_prefs") == before);
assert(!store.fs.exists("/new_prefs.tmp"));
}
int main() {
uint32_t random = 0x31415926;
unsigned comparisons = 0;
for (unsigned trial = 0; trial < 512; ++trial) {
CompanionNodePrefs prefs;
// Exercise every persisted byte, not a memcpy of the nontrivial class.
// Raw invalid values are intentionally retained by the writer; validation
// and migration remain the loader/CLI's existing responsibility.
seed(prefs, random);
DataStore store;
WRITE_CALLS.clear();
assert(store.savePrefs(prefs, 47.125, -122.25));
assert(store.fs.files.at("/new_prefs") == golden(prefs, 47.125, -122.25));
assert(!store.fs.exists("/new_prefs.tmp"));
#if defined(STM32_PLATFORM)
assert(WRITE_CALLS == std::vector<size_t>{244});
assert(store.fs.files.at("/new_prefs").size() == 244);
#endif
#if defined(NRF52_PLATFORM)
const std::vector<size_t> streaming = {@WRITE_SIZES@
#if defined(RAK_4631)
, 8
#endif
, 8};
assert(WRITE_CALLS == streaming); // Direct fields, never a 236-byte staging write.
#if defined(RAK_4631)
assert(store.fs.files.at("/new_prefs").size() == 252);
#endif
#endif
DataStore reference;
assert(reference.savePrefsLegacy(prefs, 47.125, -122.25));
assert(reference.fs.files.at("/new_prefs") == store.fs.files.at("/new_prefs"));
++comparisons;
}
CompanionNodePrefs original;
assert(original.lost_reply == 0);
assert(original.tx_media == mesh::RADIO_TX_AUTO && original.tx_ssh == mesh::RADIO_TX_AUTO);
strcpy(original.node_name, "durable image");
original.freq = 915.25f; original.ble_pin = 123456;
original.bluetooth_enabled = 1; original.usb_debug_enabled = 1;
original.gps_sync_interval_hours = 336;
original.lost_reply = 2;
original.tx_media = mesh::RADIO_TX_SECONDARY;
original.tx_ssh = mesh::RADIO_TX_OFF;
#if defined(RAK_4631)
original.peripherals_shared = 1;
#endif
DataStore initial;
assert(initial.savePrefs(original, 47.125, -122.25));
const auto before = golden(original, 47.125, -122.25);
const size_t historical_size=before.size()-mesh::companion::traffic::FooterSize
#if defined(RAK_4631)
- mesh::peripherals::FooterSize
#endif
;
assert(initial.fs.files.at("/new_prefs") == before);
// Every complete historical wire boundary stays readable, and an old
// image always disables the new opt-in response even if live RAM was on.
const std::vector<size_t> legacy_sizes = {
84, 85, 90, 91, 92, 93, 140, 141, 142, 143, 144, 155,
156, 157, 158, 159, 191, 193, 200, 207, 214, 215, 226,
#if defined(TBEAM_1W)
// The generic 231-byte compatibility boundary also ends at TBeam's
// complete fan-mode string; preserve that existing accepted image.
231, 233, 237, 238, 239, 241, 242
#elif defined(RP2040_PLATFORM) && defined(ENABLE_WIFI_INTERFACE)
323, 327, 328, 329, 331, 332
#else
230, 231, 232, 234, 235
#endif
};
for (size_t length : legacy_sizes) {
DataStore reboot;
auto historical = before; historical.resize(length);
reboot.fs.files["/new_prefs"] = historical;
CompanionNodePrefs live; live.lost_reply = 2;
live.tx_media=mesh::RADIO_TX_BOTH;live.tx_ssh=mesh::RADIO_TX_PRIMARY;
live.getCustom()->setByKey("local_tag", "unchanged");
double lat = 1, lon = 2;
assert(reboot.loadPrefsInt("/new_prefs", live, lat, lon));
assert(live.lost_reply == 0 && live.ble_pin == original.ble_pin);
assert(live.tx_media==mesh::RADIO_TX_AUTO&&live.tx_ssh==mesh::RADIO_TX_AUTO);
assert(live.freq == original.freq && !strcmp(live.node_name, original.node_name));
assert(lat == 47.125 && lon == -122.25);
assert(reboot.fs.files.at("/new_prefs") == historical);
char tag[16] = {};
assert(live.getCustom()->getByKey("local_tag", tag, sizeof(tag)));
assert(!strcmp(tag, "unchanged"));
++comparisons;
}
// The accepted-size table is an exact allowlist. Check every length through
// a full extra byte range, including sizes that could alias legacy values
// if the underlying file size were accidentally narrowed with the table.
std::vector<size_t> accepted_sizes = legacy_sizes;
accepted_sizes.push_back(historical_size);
#if defined(RAK_4631)
accepted_sizes.push_back(historical_size + mesh::peripherals::FooterSize);
#endif
accepted_sizes.push_back(before.size());
for (size_t length = 0; length <= before.size() + 256; ++length) {
DataStore reboot;
auto candidate = before;
candidate.resize(length);
reboot.fs.files["/new_prefs"] = candidate;
CompanionNodePrefs live;
live.freq = 920.5f;
live.lost_reply = 1;
live.getCustom()->setByKey("length_tag", "unchanged");
double lat = 1, lon = 2;
const bool expected = std::find(accepted_sizes.begin(), accepted_sizes.end(),
length) != accepted_sizes.end();
assert(reboot.loadPrefsInt("/new_prefs", live, lat, lon) == expected);
if (expected) {
assert(live.freq == original.freq);
assert(live.lost_reply == (length >= historical_size ? 2 : 0));
assert(live.tx_media == (length == before.size() ? mesh::RADIO_TX_SECONDARY : mesh::RADIO_TX_AUTO));
assert(live.tx_ssh == (length == before.size() ? mesh::RADIO_TX_OFF : mesh::RADIO_TX_AUTO));
#if defined(RAK_4631)
assert(live.peripherals_shared == (length >= 244 ? 1 : 0));
#endif
assert(lat == 47.125 && lon == -122.25);
} else {
assert(live.freq == 920.5f && live.lost_reply == 1);
assert(lat == 1 && lon == 2);
}
char tag[16] = {};
assert(live.getCustom()->getByKey("length_tag", tag, sizeof(tag)));
assert(!strcmp(tag, "unchanged"));
assert(reboot.fs.files.at("/new_prefs") == candidate);
++comparisons;
}
// All 256 byte encodings are preserved by the append-only serializer.
// Only off/no/yes are accepted on reboot; corrupt values normalize off
// without erasing radio settings or changing the committed image.
for (unsigned value = 0; value < 256; ++value) {
CompanionNodePrefs saved;
assert(saved.copyPersistedValuesFrom(original));
saved.lost_reply = static_cast<uint8_t>(value);
DataStore writer;
assert(writer.savePrefs(saved, 47.125, -122.25));
const auto disk = writer.fs.files.at("/new_prefs");
assert(disk[historical_size-1] == value);
assert(std::equal(disk.begin(), disk.begin()+historical_size-1, before.begin()));
assert(std::equal(disk.begin()+historical_size, disk.end(), before.begin()+historical_size));
DataStore reboot; reboot.fs.files["/new_prefs"] = disk;
CompanionNodePrefs live; live.lost_reply = 2;
double lat = 1, lon = 2;
assert(reboot.loadPrefsInt("/new_prefs", live, lat, lon));
assert(live.lost_reply == (value <= 2 ? value : 0));
assert(live.ble_pin == original.ble_pin && live.freq == original.freq);
assert(live.gps_sync_interval_hours == 336 && live.usb_debug_enabled == 1);
assert(lat == 47.125 && lon == -122.25);
assert(reboot.fs.files.at("/new_prefs") == disk);
++comparisons;
}
{
DataStore torn; torn.fs.files["/new_prefs"] = before;
torn.fs.fail_read_after = static_cast<int>(before.size() - 1);
CompanionNodePrefs live; live.lost_reply = 1; live.freq = 920.5f;
strcpy(live.node_name, "live unchanged");
double lat = 1, lon = 2;
assert(!torn.loadPrefsInt("/new_prefs", live, lat, lon));
assert(live.lost_reply == 1 && live.freq == 920.5f);
assert(!strcmp(live.node_name, "live unchanged") && lat == 1 && lon == 2);
assert(torn.fs.files.at("/new_prefs") == before);
++comparisons;
}
CompanionNodePrefs changed;
assert(changed.copyPersistedValuesFrom(original));
strcpy(changed.node_name, "replacement image");
changed.freq = 910.5f; changed.usb_debug_enabled = 0;
changed.gps_sync_interval_hours = 24;
changed.lost_reply = 1;
const auto after = golden(changed, 42.25, -121.125);
// Every possible short write and readback, including every tail byte,
// must preserve the old durable image. STM32 issues only one image write;
// the other supported platforms preserve their scalar-write path.
for (size_t quota = 0; quota < before.size(); ++quota) {
DataStore write;
write.fs.files["/new_prefs"] = before;
write.fs.fail_write_after = static_cast<int>(quota);
assert(!write.savePrefs(changed, 42.25, -121.125));
unchanged(write, before);
DataStore reference;
reference.fs.files["/new_prefs"] = before;
reference.fs.fail_write_after = static_cast<int>(quota);
assert(!reference.savePrefsLegacy(changed, 42.25, -121.125));
unchanged(reference, before);
write.fs.fail_write_after = -1;
assert(write.savePrefs(changed, 42.25, -121.125));
assert(write.fs.files.at("/new_prefs") == after);
DataStore read;
read.fs.files["/new_prefs"] = before;
read.fs.fail_read_after = static_cast<int>(quota);
assert(!read.savePrefs(changed, 42.25, -121.125));
unchanged(read, before);
++comparisons;
}
#if defined(RAK_4631)
// All checked peripheral-footer bits are validation inputs, including the
// boundary immediately before the independent traffic footer.
for (unsigned bit = 0; bit < 64; ++bit) {
DataStore corrupt;
auto candidate = before;
candidate[236 + bit / 8] ^= uint8_t(1u << (bit % 8));
corrupt.fs.files["/new_prefs"] = candidate;
CompanionNodePrefs live; live.freq = 920.5f; live.peripherals_shared = 0;
double lat = 1, lon = 2;
assert(!corrupt.loadPrefsInt("/new_prefs", live, lat, lon));
assert(live.freq == 920.5f && live.peripherals_shared == 0 && lat == 1 && lon == 2);
assert(corrupt.fs.files.at("/new_prefs") == candidate);
++comparisons;
}
#endif
for (unsigned fault = 0; fault < 3; ++fault) {
DataStore store;
store.fs.files["/new_prefs"] = before;
if (fault == 0) store.fs.fail_write_open = true;
if (fault == 1) store.fs.fail_write = true;
if (fault == 2) store.fs.fail_read_open = true;
assert(!store.savePrefs(changed, 42.25, -121.125));
unchanged(store, before);
++comparisons;
}
#if defined(STM32_PLATFORM) || defined(NRF52_PLATFORM)
const unsigned rename_steps = 1;
#else
const unsigned rename_steps = 2;
#endif
for (unsigned failure = 1; failure <= rename_steps; ++failure) {
DataStore store;
store.fs.files["/new_prefs"] = before;
store.fs.fail_rename = static_cast<int>(failure);
assert(!store.savePrefs(changed, 42.25, -121.125));
unchanged(store, before);
++comparisons;
}
assert(initial.savePrefs(changed, 42.25, -121.125));
CompanionNodePrefs loaded;
double lat = 0, lon = 0;
assert(initial.loadPrefs(loaded, lat, lon));
assert(!strcmp(loaded.node_name, changed.node_name));
assert(loaded.freq == changed.freq && loaded.ble_pin == changed.ble_pin);
assert(loaded.gps_sync_interval_hours == 24 && loaded.usb_debug_enabled == 0);
assert(loaded.lost_reply == 1);
assert(loaded.tx_media == original.tx_media && loaded.tx_ssh == original.tx_ssh);
assert(lat == 42.25 && lon == -121.125);
#if defined(STM32_PLATFORM)
// The extended STM32 image is still one atomic write. Its old prefix and
// TXR bytes come from the independent wire definition; TZF is a literal
// binary32/CRC vector, not the production footer encoder.
{
CompanionNodePrefs fractional;
assert(fractional.copyPersistedValuesFrom(changed));
fractional.tz_offset = 5.75f;
auto expected = golden(fractional, 42.25, -121.125);
expected.insert(expected.end(), {'T', 'Z', 'F', 1, 0, 0, 0xb8, 0x40, 0x7e, 0x2e});
assert(expected.size() == 254 && expected[225] == 5);
DataStore writer;
writer.fs.files["/new_prefs"] = before;
WRITE_CALLS.clear();
assert(writer.savePrefs(fractional, 42.25, -121.125));
assert(WRITE_CALLS == std::vector<size_t>{254});
assert(writer.fs.files.at("/new_prefs") == expected);
for (size_t quota = 0; quota < expected.size(); ++quota) {
for (bool readback : {false, true}) {
DataStore failed;
failed.fs.files["/new_prefs"] = before;
if (readback) failed.fs.fail_read_after = static_cast<int>(quota);
else failed.fs.fail_write_after = static_cast<int>(quota);
assert(!failed.savePrefs(fractional, 42.25, -121.125));
unchanged(failed, before);
failed.fs.fail_read_after = failed.fs.fail_write_after = -1;
WRITE_CALLS.clear();
assert(failed.savePrefs(fractional, 42.25, -121.125));
assert(WRITE_CALLS == std::vector<size_t>{254});
assert(failed.fs.files.at("/new_prefs") == expected);
++comparisons;
}
}
CompanionNodePrefs reboot;
assert(writer.loadPrefsInt("/new_prefs", reboot, lat, lon));
assert(reboot.tz_offset == 5.75f && reboot.freq == fractional.freq);
assert(reboot.tx_media == fractional.tx_media && reboot.tx_ssh == fractional.tx_ssh);
}
// A future field or wrong staging capacity must fail closed, not overrun
// the stack or silently publish a truncated/extra-byte format.
for (bool undersized : {false, true}) {
DataStore store;
store.fs.files["/new_prefs"] = before;
assert(!(undersized ? store.savePrefsTooSmall(changed, lat, lon)
: store.savePrefsTooLarge(changed, lat, lon)));
unchanged(store, before);
++comparisons;
}
#endif
printf("PASS: %u independent wire/fault comparisons\n", comparisons);
}
'''
def field_code(rows, seed=False):
result = []
for name, size in rows:
if name == 'tz_offset':
if seed:
result.append('prefs.tz_offset = float(int(random % 27) - 12);')
else:
result.append('const int8_t legacy_tz = static_cast<int8_t>(prefs.tz_offset);')
result.append('append(&legacy_tz, 1);')
elif seed:
if name is not None and not name.startswith('@'):
result.append(f'fill(&prefs.{name}, sizeof(prefs.{name}));')
elif name is None:
result.append(f'bytes.insert(bytes.end(), {size}, uint8_t(0));')
else:
value = name[1:] if name.startswith('@') else 'prefs.' + name
result.append(f'static_assert(sizeof({value}) == {size}, "published field size");')
result.append(f'append(&{value}, {size});')
return '\n '.join(result)
class CompanionPrefsImageTests(unittest.TestCase):
def test_explicit_wire_layout_and_atomic_staging(self):
source = (ROOT / 'examples/companion_radio/DataStore.cpp').read_text()
save = method(source, 'bool DataStore::savePrefs(')
nrf_branch_start = save.index('#elif defined(NRF52_PLATFORM) && defined(__GNUC__)')
nrf_branch = save[nrf_branch_start:save.index('\n#else', nrf_branch_start)]
self.assertIn('file.write(', nrf_branch)
self.assertIn('prefValue(', nrf_branch)
self.assertNotIn('image[', nrf_branch)
self.assertNotIn('memcpy', nrf_branch)
legacy_writer = (ROOT / 'test/fixtures/companion_prefs_storage/legacy_writer.inc').read_text()
commit = ' success = file.commit(success);'
self.assertEqual(legacy_writer.count(commit), 1)
legacy_writer = legacy_writer.replace(commit, '''#if defined(RAK_4631)
uint8_t peripheral_footer[8];
mesh::peripherals::encodeFooter(peripheral_footer, _prefs.peripherals_shared);
success = success && file.write(peripheral_footer, sizeof(peripheral_footer)) == sizeof(peripheral_footer);
#endif
uint8_t traffic_footer[8];
mesh::companion::traffic::encodeFooter(traffic_footer, _prefs.tx_media, _prefs.tx_ssh);
success = success && file.write(traffic_footer, sizeof(traffic_footer)) == sizeof(traffic_footer);
''' + commit)
methods = esp_recovery_helpers(source) + '\n'.join(
method(source, signature) for signature in (
'bool DataStore::loadPrefs(', 'bool DataStore::loadPrefsInt(')
) + '\n' + save + '\n' + legacy_writer
for suffix, capacity in (('TooSmall', 235), ('TooLarge', 237)):
variant = save.replace('DataStore::savePrefs(', 'DataStore::savePrefs' + suffix + '(', 1)
prefix = 'static constexpr size_t PREFS_PREFIX_SIZE = 236;'
self.assertIn(prefix, variant)
variant = variant.replace(prefix, f'static constexpr size_t PREFS_PREFIX_SIZE = {capacity};', 1)
methods += '\n#if defined(STM32_PLATFORM)\n' + variant + '\n#endif\n'
code = HARNESS.split('int main(')[0].replace('@METHODS@', methods)
code = code.replace(' bool savePrefs(const CompanionNodePrefs&, double, double);',
' bool savePrefs(const CompanionNodePrefs&, double, double);\n'
' bool savePrefsLegacy(const CompanionNodePrefs&, double, double);\n'
' bool savePrefsTooSmall(const CompanionNodePrefs&, double, double);\n'
' bool savePrefsTooLarge(const CompanionNodePrefs&, double, double);')
self.assertEqual(sum(size for _, size in WIRE_FIELDS), 226)
self.assertEqual(sum(size for _, size in TAIL_FIELDS), 10)
generated = {}
for seed in (False, True):
rows = [field_code(WIRE_FIELDS, seed)]
for condition, fields in OPTIONAL_FIELDS:
rows.append('#if ' + condition + '\n' + field_code(fields, seed) + '\n#endif')
rows.append(field_code(TAIL_FIELDS, seed))
generated['@SEED@' if seed else '@GOLDEN@'] = '\n '.join(rows)
tests = TESTS
for placeholder, value in generated.items():
tests = tests.replace(placeholder, value)
tests = tests.replace('@WRITE_SIZES@', ', '.join(
str(size) for _, size in WIRE_FIELDS + TAIL_FIELDS))
with tempfile.TemporaryDirectory(prefix='meshcore-prefs-image-') as directory:
work = Path(directory)
(work / 'Utils.h').write_text('''#pragma once
#include <Arduino.h>
#include <cassert>
namespace mesh { struct Utils {
static void printHex(Stream&,const uint8_t*,size_t){assert(false);}
static void fromHex(uint8_t*,size_t,const char*){assert(false);}
}; }
''')
(work / 'platform_shim.h').write_text('''#include <cstdlib>
#include <cstdio>
inline char* utoa(unsigned int value,char* output,int base){
if(base!=10)abort();sprintf(output,"%u",value);return output;
}
''')
transaction = (ROOT / 'src/helpers/ContactFileTransaction.h').read_text()
(work / 'ContactFileTransaction.h').write_text(transaction.replace(
'#include "IdentityStore.h"', '#include <helpers/IdentityStore.h>'))
# Reuse the existing memory-filesystem boundary, adding only an
# observable open failure needed to exercise the actual writer.
fs = (ROOT / 'test/fixtures/radio_profiles/mocks/helpers/IdentityStore.h').read_text()
fs = fs.replace('class MemoryFS;',
'static std::vector<size_t> WRITE_CALLS;\nclass MemoryFS;')
fs = fs.replace('inline size_t File::write(const uint8_t* data, size_t size) {',
'inline size_t File::write(const uint8_t* data, size_t size) {\n'
' WRITE_CALLS.push_back(size);')
fs = fs.replace(' bool fail_write = false;',
' bool fail_write = false, fail_write_open = false;')
fs = fs.replace(' if (*mode == \'w\') files[path].clear();',
' if (*mode == \'w\' && fail_write_open) return {};\n'
' if (*mode == \'w\') files[path].clear();')
(work / 'helpers').mkdir()
(work / 'helpers/IdentityStore.h').write_text(fs)
cpp = work / 'test.cpp'
cpp.write_text(code + tests)
for platform in ('STM32_PLATFORM', 'NRF52_PLATFORM', 'NRF52_PLATFORM,RAK_4631', 'ESP32_PLATFORM',
'RP2040_PLATFORM', 'ESP32_PLATFORM,TBEAM_1W',
'RP2040_PLATFORM,ENABLE_WIFI_INTERFACE'):
with self.subTest(platform=platform):
binary = work / 'test'
sanitizers = [] if os.name == 'nt' else [
'-fsanitize=address,undefined', '-fno-sanitize-recover=all',
'-fno-pie', '-no-pie']
built = subprocess.run([os.environ.get('CXX', 'g++'), '-std=c++17',
*['-D' + flag + '=1' for flag in platform.split(',')],
*sanitizers, '-include', str(work / 'platform_shim.h'),
'-I', str(work), '-I', str(ROOT / 'test/fixtures/radio_profiles/mocks'),
'-I', str(ROOT / 'test/mocks'), '-I', str(ROOT / 'src'),
'-I', str(ROOT / 'src/helpers'), '-I', str(ROOT), str(cpp),
*[str(ROOT / 'src/helpers' / name) for name in (
'ConfigSerializer.cpp', 'DynamicConfigSerializer.cpp',
'CommonRadioPrefs.cpp', 'TxtDataHelpers.cpp')],
'-o', str(binary)], capture_output=True, text=True)
self.assertEqual(built.returncode, 0, built.stdout + built.stderr)
ran = subprocess.run([str(binary)], capture_output=True, text=True)
self.assertEqual(ran.returncode, 0, ran.stdout + ran.stderr)
if __name__ == '__main__':
unittest.main()