276 lines
13 KiB
Python
276 lines
13 KiB
Python
"""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()==242);
|
|
#elif defined(RP2040_PLATFORM) && defined(ENABLE_WIFI_INTERFACE)
|
|
assert(on_disk.size()==332);
|
|
#else
|
|
assert(on_disk.size()==235);
|
|
#endif
|
|
const size_t bluetooth_offset=on_disk.size()-4;
|
|
assert(on_disk[bluetooth_offset]==1&&on_disk.back()==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.back()==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 <cassert>
|
|
#include <cstdint>
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <initializer_list>
|
|
#include <helpers/MultiSerialInterface.h>
|
|
#include <helpers/BluetoothMac.h>
|
|
#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<class... T> 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 <cstdint>\n#include <cstddef>\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 <typename T> 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 <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>'))
|
|
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()
|