"""Exercise value-only snapshots using the real adapter-bearing preferences.""" from pathlib import Path import os import re 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] TESTS = r''' #include #include @FIELD_CHECKS@ static void seedValues(CompanionNodePrefs& prefs, uint32_t& random) { auto fill = [&](void* destination, size_t size) { auto* bytes = static_cast(destination); for (size_t i = 0; i < size; ++i) { random = random * 1664525u + 1013904223u; bytes[i] = static_cast(random >> 24); } }; @SEED_FIELDS@ } static void equalValues(const CompanionNodePrefs& a, const CompanionNodePrefs& b) { @COMPARE_FIELDS@ } static size_t adapterOffset(const CompanionNodePrefs& prefs) { // The value helper does not copy the adapters or their inter-field padding. // Use the complete object's byte representation for bounded observation. const uintptr_t first = reinterpret_cast(&prefs); const uintptr_t last = reinterpret_cast(&prefs.tx_ssh); assert(last >= first && last - first < sizeof(prefs)); return static_cast(last - first) + sizeof(prefs.tx_ssh); } static std::vector adapterBytes(const CompanionNodePrefs& prefs) { const auto* bytes = reinterpret_cast(&prefs); return {bytes + adapterOffset(prefs), bytes + sizeof(prefs)}; } static void expectTag(CompanionNodePrefs& prefs, const char* expected) { char value[32] = {}; assert(prefs.getCustom()->getByKey("snapshot_tag", value, sizeof(value))); assert(!strcmp(value, expected)); } int main() { uint32_t random = 0x19283746; for (unsigned trial = 0; trial < 64; ++trial) { CompanionNodePrefs source, snapshot; seedValues(source, random); source.getRadioPrefs()->setFreq(915.25f); assert(source.getCustom()->setByKey("snapshot_tag", "source")); assert(snapshot.getCustom()->setByKey("snapshot_tag", "snapshot")); snapshot.clearDirty(); assert(source.isDirty() && !snapshot.isDirty()); auto source_adapters = adapterBytes(source); auto snapshot_adapters = adapterBytes(snapshot); assert(snapshot.copyPersistedValuesFrom(source)); equalValues(source, snapshot); assert(adapterBytes(source) == source_adapters); assert(adapterBytes(snapshot) == snapshot_adapters); assert(source.isDirty() && !snapshot.isDirty()); expectTag(source, "source"); expectTag(snapshot, "snapshot"); const auto* bytes = reinterpret_cast(&snapshot); std::vector before_self(bytes, bytes + sizeof(snapshot)); assert(snapshot.copyPersistedValuesFrom(snapshot)); assert(!memcmp(&snapshot, before_self.data(), before_self.size())); // A normally constructed scratch adapter must still own the scratch // object, including its dynamic-config fallback after copying values. snapshot.getRadioPrefs()->setFreq(910.5f); assert(snapshot.freq == 910.5f && source.freq == 915.25f); source.radio_fem_txgain = 0; assert(snapshot.getCustom()->setByKey("fem_txgain", "1")); assert(snapshot.radio_fem_txgain == 1 && source.radio_fem_txgain == 0); source.getRadioPrefs()->setFreq(920.5f); assert(source.freq == 920.5f && snapshot.freq == 910.5f); } unsigned comparisons = 0; for (unsigned trial = 0; trial < 3; ++trial) { CompanionNodePrefs saved; seedValues(saved, random); saved.bluetooth_enabled = static_cast(trial); DataStore image_store; assert(image_store.savePrefs(saved, 47.125, -122.25)); auto image = image_store.fs.files["/new_prefs"]; // The frozen reference remains authoritative only for the historical // prefix. Checked new-footer behavior has dedicated persistence tests. image.resize(image.size()-mesh::companion::traffic::FooterSize); // Test every possible old/torn image length, including every officially // supported append boundary and unknown lengths beyond the current tail. for (size_t length = 0; length <= image.size() + 5; ++length) { auto disk = image; disk.resize(length); for (size_t quota = 0; quota <= length; ++quota) { DataStore actual_store, legacy_store; actual_store.fs.files["/new_prefs"] = disk; legacy_store.fs.files["/new_prefs"] = disk; actual_store.fs.fail_read_after = static_cast(quota); legacy_store.fs.fail_read_after = static_cast(quota); CompanionNodePrefs actual, legacy; seedValues(actual, random); assert(legacy.copyPersistedValuesFrom(actual)); actual.getRadioPrefs()->setFreq(915.25f); legacy.getRadioPrefs()->setFreq(915.25f); assert(actual.getCustom()->setByKey("snapshot_tag", "actual")); assert(legacy.getCustom()->setByKey("snapshot_tag", "legacy")); auto actual_adapters = adapterBytes(actual); auto legacy_adapters = adapterBytes(legacy); double lat = 1, lon = 2, legacy_lat = 1, legacy_lon = 2; READ_CALLS.clear(); AVAILABLE_CALLS = 0; const bool expected = legacy_store.loadPrefsLegacy( "/new_prefs", legacy, legacy_lat, legacy_lon); const auto expected_reads = READ_CALLS; const unsigned expected_available = AVAILABLE_CALLS; READ_CALLS.clear(); AVAILABLE_CALLS = 0; const bool got = actual_store.loadPrefsInt( "/new_prefs", actual, lat, lon); assert(got == expected); assert(READ_CALLS == expected_reads); // The common footer reader adds one EOF probe after any recognized // historical image; no original field read or ordering changes. assert(AVAILABLE_CALLS == expected_available+(expected_reads.empty()?0:1)); if (length == 84 && quota == 84) { // The original mandatory prefix has 21 reads, ending at ble_pin. // This order is an independent wire contract, not the new table. const std::vector mandatory = { 4, 32, 4, 8, 8, 4, 1, 1, 1, 1, 4, 1, 1, 1, 1, 4, 1, 1, 1, 1, 4}; assert(expected && READ_CALLS == mandatory); unsigned optional_fields = 46; #ifdef TBEAM_1W optional_fields += 3; #endif #if defined(RP2040_PLATFORM) && defined(ENABLE_WIFI_INTERFACE) optional_fields += 2; #endif // Every absent optional field observes EOF, then the completed // image receives the existing final trailing-byte check. assert(AVAILABLE_CALLS == optional_fields + 2); } if(got)legacy.tx_media=legacy.tx_ssh=mesh::RADIO_TX_AUTO; equalValues(actual, legacy); assert(!memcmp(&lat, &legacy_lat, sizeof(lat))); assert(!memcmp(&lon, &legacy_lon, sizeof(lon))); assert(actual_store.fs.files["/new_prefs"] == disk); assert(legacy_store.fs.files["/new_prefs"] == disk); assert(adapterBytes(actual) == actual_adapters); assert(adapterBytes(legacy) == legacy_adapters); assert(actual.isDirty() && legacy.isDirty()); expectTag(actual, "actual"); expectTag(legacy, "legacy"); auto legacy_txgain = legacy.radio_fem_txgain; assert(actual.getCustom()->setByKey("fem_txgain", "1")); assert(actual.radio_fem_txgain == 1 && legacy.radio_fem_txgain == legacy_txgain); ++comparisons; } } } printf("PASS: %u actual-loader value/adapter/failure comparisons\n", comparisons); } ''' class CompanionPrefsValuesTests(unittest.TestCase): def test_value_only_copy_and_full_loader_match_legacy(self): source = (ROOT / 'examples/companion_radio/DataStore.cpp').read_text() loader = method(source, 'bool DataStore::loadPrefsInt(') # A frozen sequential reader, not a clone of the current field table, # is the authority for all old/torn images and read failure traces. legacy = (ROOT / 'test/fixtures/companion_prefs_storage/legacy_loader.inc').read_text() self.assertNotIn('copyPersistedValuesFrom', legacy) methods = esp_recovery_helpers(source) + '\n'.join((loader, legacy, method(source, 'bool DataStore::savePrefs('))) code = HARNESS.split('int main(')[0].replace('@METHODS@', methods) code = code.replace(' bool loadPrefsInt(const char*, CompanionNodePrefs&, double&, double&);', ' bool loadPrefsInt(const char*, CompanionNodePrefs&, double&, double&);\n' ' bool loadPrefsLegacy(const char*, CompanionNodePrefs&, double&, double&);') prefs = (ROOT / 'examples/companion_radio/NodePrefs.h').read_text() prefix = prefs[prefs.index('class CompanionNodePrefs {'):prefs.index('\nprivate:')] declarations = re.findall( r'^\s*(?:float|char|u?int(?:8|16|32)_t)\s+([A-Za-z_][A-Za-z_0-9]*)', prefix, re.M) self.assertEqual(declarations[0], 'airtime_factor') self.assertEqual(declarations[-2:], ['tx_media', 'tx_ssh']) optional = { 'fan_mode': 'TBEAM_1W', 'fan_lo': 'TBEAM_1W', 'fan_hi': 'TBEAM_1W', 'wifi_ssid': 'defined(RP2040_PLATFORM) && defined(ENABLE_WIFI_INTERFACE)', 'wifi_pwd': 'defined(RP2040_PLATFORM) && defined(ENABLE_WIFI_INTERFACE)', 'peripherals_shared': 'RAK_4631', } def fields(template): rows = [] for name in declarations: row = template.format(name=name) if name in optional: condition = optional[name] if not condition.startswith('defined('): condition = 'defined(' + condition + ')' row = '#if ' + condition + '\n' + row + '\n#endif' rows.append(row) return '\n'.join(rows) tests = TESTS.replace('@FIELD_CHECKS@', fields( 'static_assert(std::is_trivially_copyable::value,\n' ' "A persisted snapshot member must remain a value");')) tests = tests.replace('@SEED_FIELDS@', fields('fill(&prefs.{name}, sizeof(prefs.{name}));')) tests = tests.replace('@COMPARE_FIELDS@', fields( 'assert(!memcmp(&a.{name}, &b.{name}, sizeof(a.{name})));')) with tempfile.TemporaryDirectory(prefix='meshcore-prefs-values-') as directory: work = Path(directory) (work / 'Utils.h').write_text('''#pragma once #include #include 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 #include 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 ')) # Instrument only the existing filesystem boundary. Both actual # readers and the real transactional writer remain unchanged. fs = (ROOT / 'test/fixtures/radio_profiles/mocks/helpers/IdentityStore.h').read_text() fs = fs.replace('class MemoryFS;', 'static std::vector READ_CALLS;\n' 'static unsigned AVAILABLE_CALLS = 0;\nclass MemoryFS;') fs = fs.replace('size_t available() const { return size() - cursor_; }', 'size_t available() const { ++AVAILABLE_CALLS;return size() - cursor_; }') fs = fs.replace('inline int File::read(uint8_t* data, size_t size) {', 'inline int File::read(uint8_t* data, size_t size) {\n' ' READ_CALLS.push_back(size);') (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', '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) self.assertNotIn('offsetof', built.stderr) ran = subprocess.run([str(binary)], capture_output=True, text=True, timeout=60) self.assertEqual(ran.returncode, 0, ran.stdout + ran.stderr) if __name__ == '__main__': unittest.main()