432 lines
20 KiB
Python
432 lines
20 KiB
Python
"""Exercise real Companion preferences saves and startup recovery with I/O faults."""
|
|
from pathlib import Path
|
|
import os
|
|
import subprocess
|
|
import tempfile
|
|
import unittest
|
|
|
|
from test_radio_receive_contract import method
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
HARNESS = r'''
|
|
#include <cassert>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <new>
|
|
#include <cerrno>
|
|
#include <helpers/IdentityStore.h>
|
|
#include "ContactFileTransaction.h"
|
|
#if defined(STM32_PLATFORM) || defined(NRF52_PLATFORM)
|
|
#define ATOMIC_FILE_WRITER_IMPLEMENTATION
|
|
#include <helpers/AtomicFileWriter.h>
|
|
#endif
|
|
#include "examples/companion_radio/NodePrefs.h"
|
|
#include "examples/companion_radio/PrefsStorageLayout.h"
|
|
#if defined(RAK_4631)
|
|
#include <helpers/PeripheralModePrefs.h>
|
|
#endif
|
|
#define MESH_DEBUG_PRINTLN(...) ((void)0)
|
|
struct DataStore {
|
|
MemoryFS fs;
|
|
FILESYSTEM* _fs=&fs;
|
|
bool _prefs_load_incomplete=false, _primary_storage_unavailable=false;
|
|
bool _secondary_authority_unknown=false;
|
|
const char* _prefs_recovery_source=nullptr;
|
|
DataStore(){
|
|
#if defined(STM32_PLATFORM) || defined(NRF52_PLATFORM)
|
|
fs.rename_replaces=true;
|
|
#endif
|
|
}
|
|
bool loadPrefs(CompanionNodePrefs&, double&, double&);
|
|
bool loadPrefsInt(const char*, CompanionNodePrefs&, double&, double&);
|
|
bool savePrefs(const CompanionNodePrefs&, double, double);
|
|
};
|
|
File openRead(FILESYSTEM* fs,const char* path){return fs->open(path,"r");}
|
|
File openWrite(FILESYSTEM* fs,const char* path){return fs->open(path,"w");}
|
|
bool contactPathPresence(FILESYSTEM* fs,const char* path,bool& present){
|
|
present=fs->exists(path);return true;
|
|
}
|
|
@METHODS@
|
|
int main(int argc,char** argv){
|
|
const int scenario=argc>1?atoi(argv[1]):0;
|
|
DataStore store;CompanionNodePrefs original;
|
|
strcpy(original.node_name,"durable node");original.freq=910.525f;
|
|
original.ble_pin=876543;original.bluetooth_stealth_mode=2;
|
|
original.gps_interval=123;original.sf=7;original.cr=7;original.bw=62.5f;
|
|
original.tx_delay_factor=1.25f;original.direct_tx_delay_factor=0.75f;
|
|
original.interference_threshold=8;original.agc_reset_interval=15;original.tz_offset=-7;
|
|
original.flood_retry_attempts=4;original.flood_retry_max_path=3;
|
|
original.flood_retry_group_max_path=2;original.flood_retry_advert_enabled=0;
|
|
original.one_key_dm_enabled=1;
|
|
original.gps_sync_interval_hours=336;
|
|
original.usb_debug_enabled=1;
|
|
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
|
|
#ifdef TBEAM_1W
|
|
strcpy(original.fan_mode,"on");original.fan_lo=33;original.fan_hi=42;
|
|
#endif
|
|
#if defined(RP2040_PLATFORM) && defined(ENABLE_WIFI_INTERFACE)
|
|
strcpy(original.wifi_ssid,"test-network");strcpy(original.wifi_pwd,"test-password");
|
|
#endif
|
|
assert(store.savePrefs(original,47.1,-122.2));
|
|
const auto disk=store.fs.files["/new_prefs"];
|
|
const size_t historical_size=disk.size()-mesh::companion::traffic::FooterSize
|
|
#if defined(RAK_4631)
|
|
-mesh::peripherals::FooterSize
|
|
#endif
|
|
;
|
|
CompanionNodePrefs changed=original;changed.freq=910.1f;
|
|
if(scenario==0){
|
|
store.fs.fail_write=true;
|
|
assert(!store.savePrefs(changed,42.3,-121.2));
|
|
assert(store.fs.files["/new_prefs"]==disk);
|
|
store.fs.fail_write=false;store.fs.fail_write_after=17;
|
|
assert(!store.savePrefs(changed,42.3,-121.2));
|
|
assert(store.fs.files["/new_prefs"]==disk);
|
|
store.fs.fail_write_after=-1;
|
|
#if defined(STM32_PLATFORM) || defined(NRF52_PLATFORM)
|
|
const int rename_steps[]={1};
|
|
#else
|
|
const int rename_steps[]={1,2};
|
|
#endif
|
|
for(int fail_step : rename_steps){
|
|
store.fs.fail_rename=fail_step;
|
|
assert(!store.savePrefs(changed,42.3,-121.2));
|
|
assert(store.fs.files["/new_prefs"]==disk);
|
|
}
|
|
assert(store.savePrefs(changed,42.3,-121.2));
|
|
CompanionNodePrefs loaded;double lat=0,lon=0;
|
|
assert(store.loadPrefs(loaded,lat,lon));
|
|
assert(loaded.freq==changed.freq&&loaded.ble_pin==original.ble_pin);
|
|
assert(loaded.tx_delay_factor==1.25f&&loaded.direct_tx_delay_factor==0.75f);
|
|
assert(loaded.interference_threshold==8&&loaded.agc_reset_interval==15&&loaded.tz_offset==-7);
|
|
assert(loaded.flood_retry_attempts==4&&loaded.flood_retry_max_path==3);
|
|
assert(loaded.flood_retry_group_max_path==2&&loaded.flood_retry_advert_enabled==0);
|
|
assert(loaded.one_key_dm_enabled==1);
|
|
assert(loaded.gps_sync_interval_hours==336);
|
|
assert(loaded.usb_debug_enabled==1);
|
|
assert(loaded.lost_reply==2);
|
|
assert(loaded.tx_media==mesh::RADIO_TX_SECONDARY&&loaded.tx_ssh==mesh::RADIO_TX_OFF);
|
|
#if defined(RAK_4631)
|
|
assert(loaded.peripherals_shared==1);
|
|
#endif
|
|
#ifdef TBEAM_1W
|
|
assert(!strcmp(loaded.fan_mode,"on")&&loaded.fan_lo==33&&loaded.fan_hi==42);
|
|
#endif
|
|
#if defined(RP2040_PLATFORM) && defined(ENABLE_WIFI_INTERFACE)
|
|
assert(!strcmp(loaded.wifi_ssid,"test-network")&&!strcmp(loaded.wifi_pwd,"test-password"));
|
|
#endif
|
|
assert(lat==42.3&&lon==-121.2);
|
|
} else if(scenario==1){
|
|
for(int fault : {0,1,2,3,4}){
|
|
for(const char* path : {"/new_prefs","/node_prefs"}){
|
|
DataStore boot;boot.fs.files[path]=disk;
|
|
CompanionNodePrefs live;double lat=1,lon=2;
|
|
if(fault==0)boot.fs.fail_read_open=true;
|
|
if(fault==1)boot.fs.fail_read_after=0;
|
|
if(fault==2)boot.fs.fail_read_after=84;
|
|
if(fault==3)boot.fs.fail_read_after=214;
|
|
if(fault==4)boot.fs.files[path].resize(87);
|
|
const auto durable=boot.fs.files[path];
|
|
#if defined(ESP32_PLATFORM)
|
|
assert(boot.loadPrefs(live,lat,lon)); // unrecoverable settings permit defaults
|
|
#else
|
|
assert(!boot.loadPrefs(live,lat,lon));
|
|
#endif
|
|
assert(live.node_name[0]==0&&lat==1&&lon==2);
|
|
assert(boot.fs.files[path]==durable); // no destructive read-side reset
|
|
boot.fs.fail_read_after=-1;boot.fs.fail_read_open=false;
|
|
#if defined(ESP32_PLATFORM)
|
|
assert(boot.savePrefs(live,lat,lon));
|
|
assert(boot.fs.files["/new_prefs"]!=disk);
|
|
#else
|
|
assert(!boot.loadPrefs(live,lat,lon)); // quarantine lasts this boot
|
|
assert(!boot.savePrefs(live,lat,lon));
|
|
assert(boot.fs.files[path]==durable);
|
|
#endif
|
|
}
|
|
}
|
|
DataStore fresh;CompanionNodePrefs defaults;double lat=0,lon=0;
|
|
assert(fresh.loadPrefs(defaults,lat,lon));
|
|
assert(defaults.wifi_enabled==0); // Fresh installs never enable WiFi implicitly.
|
|
assert(fresh.savePrefs(defaults,lat,lon));
|
|
} else if(scenario==2){
|
|
#if defined(ESP32_PLATFORM) || defined(RP2040_PLATFORM)
|
|
store.fs.fail_rename_from={"/new_prefs.tmp","/new_prefs.bak"};
|
|
assert(!store.savePrefs(changed,42.3,-121.2));
|
|
assert(!store.fs.exists("/new_prefs"));
|
|
assert(store.fs.files["/new_prefs.bak"]==disk);
|
|
DataStore failed_boot;failed_boot.fs=store.fs;
|
|
CompanionNodePrefs loaded;double lat=0,lon=0;
|
|
#if defined(ESP32_PLATFORM)
|
|
assert(failed_boot.loadPrefs(loaded,lat,lon)); // verified backup is usable in RAM
|
|
assert(loaded.freq==original.freq&&loaded.ble_pin==original.ble_pin);
|
|
assert(!failed_boot.savePrefs(loaded,lat,lon)); // preserve source while rename fails
|
|
assert(failed_boot.fs.files["/new_prefs.bak"]==disk);
|
|
failed_boot.fs.fail_rename_from.clear();
|
|
assert(failed_boot.savePrefs(loaded,lat,lon));
|
|
#else
|
|
assert(!failed_boot.loadPrefs(loaded,lat,lon));
|
|
failed_boot.fs.fail_rename_from.clear();
|
|
assert(!failed_boot.loadPrefs(loaded,lat,lon));
|
|
assert(!failed_boot.savePrefs(changed,42.3,-121.2));
|
|
assert(failed_boot.fs.files["/new_prefs.bak"]==disk);
|
|
#endif
|
|
DataStore reboot;reboot.fs=failed_boot.fs;
|
|
assert(reboot.loadPrefs(loaded,lat,lon));
|
|
assert(loaded.freq==original.freq&&loaded.ble_pin==original.ble_pin);
|
|
assert(lat==47.1&&lon==-122.2);
|
|
assert(loaded.lost_reply==2);
|
|
#endif
|
|
} else if(scenario==3){
|
|
// Copy the real adapter-bearing preferences from dirty storage. This
|
|
// catches reads of uninitialized serializer bools under UBSan reliably.
|
|
alignas(CompanionNodePrefs) unsigned char memory[sizeof(CompanionNodePrefs)];
|
|
memset(memory,0xa5,sizeof(memory));
|
|
auto* prefs=new(memory)CompanionNodePrefs();
|
|
CompanionNodePrefs snapshot=*prefs;
|
|
assert(!snapshot.isDirty());
|
|
prefs->~CompanionNodePrefs();
|
|
} else if(scenario==5){
|
|
DataStore legacy;legacy.fs.files["/new_prefs"]=disk;
|
|
legacy.fs.files["/new_prefs"].resize(215);
|
|
CompanionNodePrefs loaded;double lat=0,lon=0;
|
|
assert(legacy.loadPrefs(loaded,lat,lon));
|
|
assert(loaded.tx_delay_factor==0.5f&&loaded.direct_tx_delay_factor==0.2f);
|
|
assert(loaded.interference_threshold==0&&loaded.agc_reset_interval==0&&loaded.tz_offset==0);
|
|
assert(loaded.flood_retry_attempts==15&&loaded.flood_retry_max_path==1);
|
|
assert(loaded.flood_retry_group_max_path==0xff&&loaded.flood_retry_advert_enabled==1);
|
|
assert(loaded.one_key_dm_enabled==0);
|
|
assert(loaded.freq==original.freq&&loaded.ble_pin==original.ble_pin);
|
|
} else if(scenario==6){
|
|
DataStore legacy;legacy.fs.files["/new_prefs"]=disk;
|
|
#if defined(TBEAM_1W)
|
|
legacy.fs.files["/new_prefs"].resize(233);
|
|
#elif defined(RP2040_PLATFORM) && defined(ENABLE_WIFI_INTERFACE)
|
|
legacy.fs.files["/new_prefs"].resize(323);
|
|
#else
|
|
legacy.fs.files["/new_prefs"].resize(226);
|
|
#endif
|
|
CompanionNodePrefs loaded;double lat=0,lon=0;
|
|
assert(legacy.loadPrefs(loaded,lat,lon));
|
|
assert(loaded.flood_retry_attempts==15&&loaded.flood_retry_max_path==1);
|
|
assert(loaded.flood_retry_group_max_path==0xff&&loaded.flood_retry_advert_enabled==1);
|
|
assert(loaded.one_key_dm_enabled==0);
|
|
} else if(scenario==7){
|
|
// Pre-consent image: all earlier settings survive with default-off consent.
|
|
DataStore legacy;legacy.fs.files["/new_prefs"]=disk;
|
|
legacy.fs.files["/new_prefs"].resize(historical_size-6);
|
|
CompanionNodePrefs loaded;double lat=0,lon=0;
|
|
assert(legacy.loadPrefs(loaded,lat,lon));
|
|
assert(loaded.flood_retry_advert_enabled==0);
|
|
assert(loaded.one_key_dm_enabled==0);
|
|
} else if(scenario==9){
|
|
// The previous complete image loads with the board policy even when the
|
|
// receiving prefs object previously held an explicit configured interval.
|
|
DataStore legacy;legacy.fs.files["/new_prefs"]=disk;
|
|
legacy.fs.files["/new_prefs"].resize(historical_size-4);
|
|
CompanionNodePrefs loaded;loaded.gps_sync_interval_hours=24;loaded.usb_debug_enabled=1;
|
|
double lat=0,lon=0;
|
|
assert(legacy.loadPrefs(loaded,lat,lon));
|
|
assert(loaded.gps_sync_interval_hours==0 && loaded.one_key_dm_enabled==1);
|
|
assert(loaded.usb_debug_enabled==0);
|
|
// Half of the two-byte append is never accepted as a complete image.
|
|
DataStore torn;torn.fs.files["/new_prefs"]=disk;
|
|
torn.fs.files["/new_prefs"].resize(historical_size-3);
|
|
assert(!torn.loadPrefsInt("/new_prefs",loaded,lat,lon));
|
|
// The complete published GPS image is accepted without borrowing either
|
|
// GPS byte as the new debug preference.
|
|
DataStore gps_image;gps_image.fs.files["/new_prefs"]=disk;
|
|
gps_image.fs.files["/new_prefs"].resize(historical_size-2);
|
|
loaded.usb_debug_enabled=1;
|
|
assert(gps_image.loadPrefsInt("/new_prefs",loaded,lat,lon));
|
|
assert(loaded.gps_sync_interval_hours==336&&loaded.usb_debug_enabled==0);
|
|
// The compact known-size table must never narrow the actual file length.
|
|
// A large unknown image sharing a recognized low 16 bits remains invalid.
|
|
DataStore oversized;oversized.fs.files["/new_prefs"]=disk;
|
|
oversized.fs.files["/new_prefs"].resize(disk.size()+65536);
|
|
assert(!oversized.loadPrefsInt("/new_prefs",loaded,lat,lon));
|
|
assert(loaded.gps_sync_interval_hours==336&&loaded.usb_debug_enabled==0);
|
|
} else if(scenario==10){
|
|
// Even a checksum-consistent unknown policy tag must not become AUTO or
|
|
// update just one output. Version/size and both enum fields are checked.
|
|
for(unsigned value=0;value<256;++value){
|
|
uint8_t footer[mesh::companion::traffic::FooterSize];
|
|
mesh::companion::traffic::encodeFooter(footer,mesh::RADIO_TX_SECONDARY,mesh::RADIO_TX_OFF);
|
|
for(unsigned field : {4u,5u}){
|
|
mesh::companion::traffic::encodeFooter(footer,mesh::RADIO_TX_SECONDARY,mesh::RADIO_TX_OFF);
|
|
footer[field]=uint8_t(value);
|
|
uint16_t crc=mesh::companion::traffic::checksum(footer,6);
|
|
footer[6]=uint8_t(crc>>8);footer[7]=uint8_t(crc);
|
|
uint8_t media=mesh::RADIO_TX_BOTH,ssh=mesh::RADIO_TX_PRIMARY;
|
|
bool valid=value==0||(value>=0xa1&&value<=0xa4);
|
|
assert(mesh::companion::traffic::decodeFooter(footer,sizeof(footer),media,ssh)==valid);
|
|
if(!valid)assert(media==mesh::RADIO_TX_BOTH&&ssh==mesh::RADIO_TX_PRIMARY);
|
|
}
|
|
}
|
|
// Legacy preference files cannot borrow a configured live traffic route.
|
|
for(size_t size : {historical_size, disk.size()}){
|
|
DataStore boot;boot.fs.files["/new_prefs"]=disk;
|
|
boot.fs.files["/new_prefs"].resize(size);
|
|
CompanionNodePrefs loaded;loaded.tx_media=mesh::RADIO_TX_BOTH;
|
|
loaded.tx_ssh=mesh::RADIO_TX_PRIMARY;double lat=1,lon=2;
|
|
assert(boot.loadPrefsInt("/new_prefs",loaded,lat,lon));
|
|
assert(loaded.tx_media==(size==disk.size()?mesh::RADIO_TX_SECONDARY:mesh::RADIO_TX_AUTO));
|
|
assert(loaded.tx_ssh==(size==disk.size()?mesh::RADIO_TX_OFF:mesh::RADIO_TX_AUTO));
|
|
assert(loaded.freq==original.freq&&loaded.ble_pin==original.ble_pin);
|
|
}
|
|
#if defined(RAK_4631)
|
|
DataStore old_rak;old_rak.fs.files["/new_prefs"]=disk;
|
|
old_rak.fs.files["/new_prefs"].resize(disk.size()-mesh::companion::traffic::FooterSize);
|
|
CompanionNodePrefs rak;rak.tx_media=mesh::RADIO_TX_BOTH;double lat=1,lon=2;
|
|
assert(old_rak.loadPrefsInt("/new_prefs",rak,lat,lon));
|
|
assert(rak.peripherals_shared==1&&rak.tx_media==mesh::RADIO_TX_AUTO);
|
|
#endif
|
|
// Every torn and bit-flipped checked footer fails without any live mutation.
|
|
const size_t start=disk.size()-mesh::companion::traffic::FooterSize;
|
|
for(size_t offset=0;offset<mesh::companion::traffic::FooterSize;++offset){
|
|
for(bool truncate : {false,true}){
|
|
DataStore boot;auto candidate=disk;
|
|
if(truncate)candidate.resize(start+offset);
|
|
else candidate[start+offset]^=0x80;
|
|
boot.fs.files["/new_prefs"]=candidate;
|
|
CompanionNodePrefs loaded;loaded.freq=900;loaded.tx_media=mesh::RADIO_TX_BOTH;
|
|
loaded.tx_ssh=mesh::RADIO_TX_PRIMARY;double lat=1,lon=2;
|
|
const bool legacy=truncate&&offset==0;
|
|
assert(boot.loadPrefsInt("/new_prefs",loaded,lat,lon)==legacy);
|
|
if(!legacy)assert(loaded.freq==900&&loaded.tx_media==mesh::RADIO_TX_BOTH
|
|
&&loaded.tx_ssh==mesh::RADIO_TX_PRIMARY&&lat==1&&lon==2);
|
|
assert(boot.fs.files["/new_prefs"]==candidate);
|
|
}
|
|
}
|
|
// Check every valid independent policy pair through real persistence.
|
|
for(uint8_t media=0;media<=mesh::RADIO_TX_OFF;++media){
|
|
for(uint8_t ssh=0;ssh<=mesh::RADIO_TX_OFF;++ssh){
|
|
original.tx_media=media;original.tx_ssh=ssh;
|
|
assert(store.savePrefs(original,47.1,-122.2));
|
|
CompanionNodePrefs loaded;double lat=1,lon=2;
|
|
assert(store.loadPrefs(loaded,lat,lon));
|
|
assert(loaded.tx_media==media&&loaded.tx_ssh==ssh);
|
|
}
|
|
}
|
|
} else if(scenario==8){
|
|
for(uint8_t enabled : {0,1}){
|
|
CompanionNodePrefs saved=original;saved.wifi_enabled=enabled;
|
|
assert(store.savePrefs(saved,47.1,-122.2));
|
|
CompanionNodePrefs loaded;double lat=0,lon=0;
|
|
loaded.wifi_enabled=1-enabled;
|
|
assert(store.loadPrefs(loaded,lat,lon));
|
|
assert(loaded.wifi_enabled==enabled); // Saved choices override the default.
|
|
assert(store.fs.files["/new_prefs"][156]==enabled); // Established byte offset.
|
|
}
|
|
for(unsigned size : {84,144,155,156}){
|
|
DataStore legacy;legacy.fs.files["/new_prefs"]=store.fs.files["/new_prefs"];
|
|
legacy.fs.files["/new_prefs"].resize(size);
|
|
CompanionNodePrefs loaded;double lat=0,lon=0;
|
|
assert(legacy.loadPrefs(loaded,lat,lon));
|
|
assert(loaded.wifi_enabled==0); // Old files with no WiFi field use opt-in.
|
|
}
|
|
} else if(scenario==4){
|
|
DataStore legacy;legacy.fs.files["/node_prefs"]=disk;
|
|
legacy.fs.fail_write_after=17;
|
|
#if defined(ESP32_PLATFORM)
|
|
legacy.fs.fail_rename_from={"/node_prefs"};
|
|
#endif
|
|
CompanionNodePrefs loaded;double lat=0,lon=0;
|
|
assert(legacy.loadPrefs(loaded,lat,lon));
|
|
assert(loaded.freq==original.freq);
|
|
assert(legacy.fs.files["/node_prefs"]==disk&&!legacy.fs.exists("/new_prefs"));
|
|
legacy.fs.fail_write_after=-1;
|
|
legacy.fs.fail_rename_from.clear();
|
|
assert(legacy.loadPrefs(loaded,lat,lon));
|
|
assert(!legacy.fs.exists("/node_prefs")&&legacy.fs.files["/new_prefs"]==disk);
|
|
}
|
|
}
|
|
'''
|
|
|
|
|
|
def esp_recovery_helpers(source):
|
|
"""Compile production recovery, substituting only its OS metadata call.
|
|
|
|
SPIFFS namespace presence is independent of open/read failures. Binding
|
|
the fake syscall to the filesystem argument also supports several device
|
|
instances in the same host test without a process-global mount mock.
|
|
"""
|
|
presence = method(source, 'static bool companionPathPresence(')
|
|
presence = presence.replace('struct stat info;', 'struct FixtureStat info;')
|
|
presence = presence.replace('::stat(vfs_path, &info)', 'fixtureStat(fs, vfs_path, &info)')
|
|
return '''
|
|
#if defined(ESP32_PLATFORM)
|
|
struct FixtureStat {};
|
|
static int fixtureStat(FILESYSTEM* fs, const char* path, FixtureStat*) {
|
|
assert(!strncmp(path, "/spiffs", 7));
|
|
if (!fs->files.count(path + 7)) { errno=ENOENT; return -1; }
|
|
return 0;
|
|
}
|
|
''' + presence + '\n' + method(source, 'static bool promoteCompanionRecoveryFile(') + '\n#endif\n'
|
|
|
|
|
|
class CompanionPreferencesTransactionTests(unittest.TestCase):
|
|
def test_real_preferences_io_failures(self):
|
|
source = (ROOT / 'examples/companion_radio/DataStore.cpp').read_text()
|
|
methods = esp_recovery_helpers(source) + '\n'.join(method(source, signature) for signature in (
|
|
'bool DataStore::loadPrefs(', 'bool DataStore::loadPrefsInt(',
|
|
'bool DataStore::savePrefs('))
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
work = Path(directory)
|
|
# Binary preference persistence never invokes JSON blob conversion.
|
|
# Satisfy those unrelated serializer links without crypto libraries.
|
|
(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>'))
|
|
(work / 'test.cpp').write_text(HARNESS.replace('@METHODS@', methods))
|
|
for platform in ('ESP32_PLATFORM', 'RP2040_PLATFORM', 'STM32_PLATFORM', 'NRF52_PLATFORM',
|
|
'ESP32_PLATFORM,TBEAM_1W', 'RP2040_PLATFORM,ENABLE_WIFI_INTERFACE',
|
|
'NRF52_PLATFORM,RAK_4631'):
|
|
with self.subTest(platform=platform):
|
|
binary = work / 'test'
|
|
sanitizer_flags = [] if os.name == 'nt' else [
|
|
'-fsanitize=address,undefined', '-fno-sanitize-recover=all',
|
|
'-fno-pie', '-no-pie']
|
|
build = subprocess.run(['g++', '-std=c++17',
|
|
*['-D'+flag+'=1' for flag in platform.split(',')],
|
|
*sanitizer_flags, '-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(work / 'test.cpp'),
|
|
str(ROOT / 'src/helpers/ConfigSerializer.cpp'),
|
|
str(ROOT / 'src/helpers/DynamicConfigSerializer.cpp'),
|
|
str(ROOT / 'src/helpers/CommonRadioPrefs.cpp'),
|
|
str(ROOT / 'src/helpers/TxtDataHelpers.cpp'),
|
|
'-o', str(binary)], capture_output=True, text=True)
|
|
self.assertEqual(build.returncode, 0, build.stdout + build.stderr)
|
|
for scenario in range(11):
|
|
with self.subTest(scenario=scenario):
|
|
run = subprocess.run([str(binary), str(scenario)], capture_output=True, text=True)
|
|
self.assertEqual(run.returncode, 0, run.stdout + run.stderr)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|