"""Execute persistent Bluetooth preferences, storage faults, and boot gates.""" from pathlib import Path import os import subprocess import tempfile import unittest from test_companion_preferences_transaction import HARNESS as STORE_HARNESS from test_companion_preferences_transaction import esp_recovery_helpers from test_radio_receive_contract import method ROOT = Path(__file__).resolve().parents[1] PREFERENCE_HARNESS = r''' struct MyMesh { CompanionNodePrefs _prefs; DataStore store; unsigned saves=0; bool savePrefs(){++saves;return store.savePrefs(_prefs,47.1,-122.2);} @SAVE_PREFERENCE@ bool setBluetoothEnabledPreference(bool); bool isBluetoothEnabledPreference() const; }; @PREFERENCE_METHODS@ int main(){ MyMesh mesh; assert(mesh.isBluetoothEnabledPreference()); // New devices retain legacy-on behavior. mesh._prefs.wifi_enabled=0;mesh._prefs.usb_logging_enabled=1; strcpy(mesh._prefs.node_name,"durable node");mesh._prefs.freq=910.525f; mesh._prefs.ble_pin=876543;mesh._prefs.bluetooth_stealth_mode=2; mesh._prefs.one_key_dm_enabled=1; assert(mesh.savePrefs()); const auto on_disk=mesh.store.fs.files["/new_prefs"]; #if defined(TBEAM_1W) assert(on_disk.size()==251); #elif defined(RP2040_PLATFORM) && defined(ENABLE_WIFI_INTERFACE) assert(on_disk.size()==341); #else assert(on_disk.size()==244); #endif const size_t bluetooth_offset=on_disk.size()-mesh::companion::traffic::FooterSize-5; assert(on_disk[bluetooth_offset]==1&&on_disk[bluetooth_offset+4]==0); // Both choices survive a fresh instance reading the actual on-device image. for(bool enabled : {false,true,false}){ assert(mesh.setBluetoothEnabledPreference(enabled)); DataStore reboot;reboot.fs=mesh.store.fs; CompanionNodePrefs prefs;prefs.bluetooth_enabled=!enabled; double lat=0,lon=0;assert(reboot.loadPrefs(prefs,lat,lon)); assert(prefs.bluetooth_enabled==enabled&&lat==47.1&&lon==-122.2); assert(prefs.ble_pin==876543&&prefs.freq==910.525f); assert(prefs.bluetooth_stealth_mode==2&&prefs.one_key_dm_enabled==1); assert(prefs.wifi_enabled==0&&prefs.usb_logging_enabled==1); const auto& saved=reboot.fs.files["/new_prefs"]; assert(saved[bluetooth_offset]==enabled&&saved[bluetooth_offset+4]==0); } const auto off_disk=mesh.store.fs.files["/new_prefs"]; // The public setter must roll live state back when the atomic save fails. mesh.store.fs.fail_write_after=17; assert(!mesh.setBluetoothEnabledPreference(true)); assert(!mesh.isBluetoothEnabledPreference()); assert(mesh.store.fs.files["/new_prefs"]==off_disk); mesh.store.fs.fail_write_after=-1; mesh.store.fs.fail_rename=1; assert(!mesh.setBluetoothEnabledPreference(true)); assert(!mesh.isBluetoothEnabledPreference()); assert(mesh.store.fs.files["/new_prefs"]==off_disk); mesh.store.fs.fail_rename=0; assert(mesh.setBluetoothEnabledPreference(true)); mesh.store.fs.fail_write=true; assert(!mesh.setBluetoothEnabledPreference(false)); assert(mesh.isBluetoothEnabledPreference()); mesh.store.fs.fail_write=false; // Legacy images cannot accidentally inherit an off value from the caller. for(unsigned size : {84u,156u,215u,226u,unsigned(bluetooth_offset)}){ DataStore legacy;legacy.fs.files["/new_prefs"]=on_disk; legacy.fs.files["/new_prefs"].resize(size); CompanionNodePrefs prefs;prefs.bluetooth_enabled=0; double lat=0,lon=0;assert(legacy.loadPrefs(prefs,lat,lon)); assert(prefs.bluetooth_enabled==1&&prefs.ble_pin==876543); assert(legacy.fs.files["/new_prefs"].size()==size); // Read-only migration. } // Invalid new encoding rejects the transaction rather than partially loading. for(uint8_t corrupt : {2,255}){ DataStore damaged;damaged.fs.files["/new_prefs"]=off_disk; damaged.fs.files["/new_prefs"][bluetooth_offset]=corrupt; CompanionNodePrefs prefs;prefs.bluetooth_enabled=1; strcpy(prefs.node_name,"unchanged");double lat=1,lon=2; assert(!damaged.loadPrefsInt("/new_prefs",prefs,lat,lon)); assert(prefs.bluetooth_enabled==1&&!strcmp(prefs.node_name,"unchanged")); assert(lat==1&&lon==2); } } ''' STARTUP_HARNESS = r''' #include #include #include #include #include #include #include #include "examples/companion_radio/BluetoothName.h" #define BLE_NAME_PREFIX "MC-" uint32_t now_ms=100; uint32_t millis(){return now_ms;} bool companion_bluetooth_initialized=false; char companion_bluetooth_start_failure[96]={}; uint32_t companion_bluetooth_start_at=0; constexpr uint32_t COMPANION_BLUETOOTH_RETRY_MS=5000; uint8_t companion_bluetooth_session_stealth_mode=mesh::companion::BLUETOOTH_STEALTH_OFF; struct CompanionNodePrefs { char bluetooth_name[64]={},node_name[32]="durable node"; uint8_t bluetooth_stealth_peer_type=0,bluetooth_stealth_peer[6]={}; }; struct { CompanionNodePrefs prefs; bool preference=true; CompanionNodePrefs* getNodePrefs(){return &prefs;} bool isBluetoothEnabledPreference() const {return preference;} uint32_t getBLEPin(){return 876543;} } the_mesh; namespace mesh { struct Logger {void println(const char*){}}; Logger& usbLoggingPort(){static Logger logger;return logger;} } struct Transport : BaseSerialInterface { bool enabled=false,begin_succeeds=true; unsigned enables=0,begins=0; template bool begin(T...){++begins;return begin_succeeds;} const char* beginFailure(){return "fixture startup failure";} void enable() override{enabled=true;++enables;} void disable() override{enabled=false;} bool isEnabled() const override{return enabled;} bool isConnected() const override{return false;} bool isReadBusy() const override{return false;} bool isWriteBusy() const override{return false;} size_t writeFrame(const uint8_t*,size_t n) override{return n;} size_t checkRecvFrame(uint8_t*) override{return 0;} } bluetooth_interface,usb_serial_interface; MultiSerialInterface interface_manager; bool identity_accepts=true; unsigned identity_preparations=0; bool prepareCompanionBluetoothIdentity(CompanionNodePrefs*){ ++identity_preparations;return identity_accepts; } const uint8_t* companionBluetoothAddress(const CompanionNodePrefs*,bool&){return nullptr;} @METHODS@ void fresh(bool preference){ interface_manager=MultiSerialInterface();bluetooth_interface=Transport(); usb_serial_interface=Transport();companion_bluetooth_initialized=false; companion_bluetooth_start_at=0;identity_accepts=true;identity_preparations=0; the_mesh.preference=preference; interface_manager.setBluetoothAutoEnable(preference); assert(interface_manager.addInterface(InterfaceType::USB,&usb_serial_interface)); } int main(){ // Both immediate and deferred stack registration must honor saved-off // advertising while preserving an initialized stack for USB recovery. for(bool manager_started : {false,true}){ fresh(false); if(manager_started)interface_manager.enable(); startCompanionBluetooth(); assert(companion_bluetooth_initialized&&bluetooth_interface.begins==1); assert(!bluetooth_interface.enabled&&bluetooth_interface.enables==0); if(!manager_started)interface_manager.enable(); assert(usb_serial_interface.enabled&&!bluetooth_interface.enabled); startCompanionBluetooth();serviceDeferredCompanionBluetooth(); assert(bluetooth_interface.begins==1&&bluetooth_interface.enables==0); interface_manager.enableBluetooth(); assert(bluetooth_interface.enabled&&bluetooth_interface.begins==1); } // A scheduled identity/start retry may initialize, never enable a saved-off // transport when the preference was changed before the retry became due. fresh(true);interface_manager.enable();identity_accepts=false; startCompanionBluetooth();assert(companion_bluetooth_start_at!=0); assert(!companion_bluetooth_initialized&&bluetooth_interface.begins==0); the_mesh.preference=false;interface_manager.setBluetoothAutoEnable(false); identity_accepts=true;now_ms+=COMPANION_BLUETOOTH_RETRY_MS; serviceDeferredCompanionBluetooth(); assert(companion_bluetooth_initialized&&bluetooth_interface.begins==1); assert(bluetooth_interface.enables==0&&!bluetooth_interface.enabled&&usb_serial_interface.enabled); // Existing on/default preference still starts normally in either order. fresh(true);startCompanionBluetooth();interface_manager.enable(); assert(bluetooth_interface.enabled&&bluetooth_interface.enables==1); fresh(true);interface_manager.enable();startCompanionBluetooth(); assert(bluetooth_interface.enabled&&bluetooth_interface.enables==1); } ''' class BluetoothPersistenceTests(unittest.TestCase): def test_actual_startup_and_deferred_retry_do_not_advertise_saved_off(self): main = (ROOT/'examples/companion_radio/main.cpp').read_text() methods = '\n'.join(method(main, signature) for signature in ( 'static void scheduleCompanionBluetoothRetry(', 'static void startCompanionBluetooth(', 'static void serviceDeferredCompanionBluetooth(')) with tempfile.TemporaryDirectory() as directory: work = Path(directory) (work/'Arduino.h').write_text('#pragma once\n#include \n#include \n') source = work/'startup.cpp' source.write_text(STARTUP_HARNESS.replace('@METHODS@', methods)) for platform in ('NRF52_PLATFORM', 'ESP32'): with self.subTest(platform=platform): binary = work/'startup' built = subprocess.run([os.environ.get('CXX','g++'), '-std=c++17', '-DBLE_PIN_CODE=123456','-D'+platform+'=1', '-I',str(work),'-I',str(ROOT/'src'),'-I',str(ROOT), str(source),'-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) def test_boot_policy_is_applied_before_any_interface_can_start(self): main = (ROOT/'examples/companion_radio/main.cpp').read_text() setup = method(main, '\nvoid setup()') policy = setup.index('interface_manager.setBluetoothAutoEnable(') self.assertLess(policy, setup.index('startCompanionBluetooth();')) self.assertLess(policy, setup.index('the_mesh.startInterface(')) def test_actual_preference_roundtrips_and_atomic_failure_rollback(self): store = (ROOT/'examples/companion_radio/DataStore.cpp').read_text() mesh = (ROOT/'examples/companion_radio/MyMesh.cpp').read_text() header = (ROOT/'examples/companion_radio/MyMesh.h').read_text() methods = esp_recovery_helpers(store) + '\n'.join(method(store, signature) for signature in ( 'bool DataStore::loadPrefs(', 'bool DataStore::loadPrefsInt(', 'bool DataStore::savePrefs(')) prefix = STORE_HARNESS.split('int main(')[0].replace('@METHODS@', methods) harness = PREFERENCE_HARNESS.replace('@SAVE_PREFERENCE@', method( header, 'template bool savePreference(')) harness = harness.replace('@PREFERENCE_METHODS@', '\n'.join(method(mesh, signature) for signature in ('bool MyMesh::setBluetoothEnabledPreference(', 'bool MyMesh::isBluetoothEnabledPreference('))) with tempfile.TemporaryDirectory() 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 ')) source = work/'preferences.cpp' source.write_text(prefix+harness) for flags in ('NRF52_PLATFORM', 'ESP32_PLATFORM', 'RP2040_PLATFORM', 'ESP32_PLATFORM,TBEAM_1W', 'RP2040_PLATFORM,ENABLE_WIFI_INTERFACE'): with self.subTest(platform=flags): binary = work/'preferences' built = subprocess.run([os.environ.get('CXX','g++'), '-std=c++17', *['-D'+flag+'=1' for flag in flags.split(',')], '-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(source), *[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()