"""Exercise CPU selection transactions and the actual Companion preference codec.""" 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] POLICY = r''' #include #include #include #include #include using namespace mesh::esp32::cpuclock; struct Board { uint16_t actual=80; bool supports240=true, companion=true; unsigned applies=0; std::deque outcomes; bool supportsCpuFrequencyMHz(uint16_t value) const { return value==80 || value==160 || (value==240 && supports240); } uint16_t getCpuFrequencyMHz() const { return actual; } bool applyCpuFrequencyMHz(uint16_t selected,bool ps) { ++applies; actual=resolveMHz(selected,ps,companion); // partial mutation can precede failure if(outcomes.empty()) return true; bool ok=outcomes.front();outcomes.pop_front();return ok; } }; int main() { assert(nominalMHz()==160); assert(resolveMHz(0,true,true)==80 && resolveMHz(0,false,true)==160); assert(resolveMHz(0,true,false)==160 && resolveMHz(0,false,false)==160); for(uint16_t selected : {80,160,240}) for(bool ps : {false,true}) for(bool companion : {false,true}) assert(resolveMHz(selected,ps,companion)==selected); uint16_t selected=160,durable=160;Board board;board.actual=160; bool storage=true;unsigned saves=0;char reply[160]={}; auto command=[&](const char* text,bool ps=true) { return handleCommand(board,selected,ps,text,reply,sizeof(reply),[&] { ++saves;if(!storage)return false;durable=selected;return true; }); }; assert(command("get cpu.freq")); assert(std::string(reply)=="> selected=160 actual=160MHz supported=auto,80,160,240"); assert(!command("get cpu.frequency")); for(const char* invalid : {"", "0", "40", "200", "080", "160MHz", "160.0", "-80", "+80", "AUTO", "160 240", "99999999999999999999999"}) { const std::string text=std::string("set cpu.freq ")+invalid; unsigned old=board.applies,old_saves=saves; assert(command(text.c_str()));assert(std::string(reply).find("Error:")==0); assert(selected==160&&durable==160&&board.actual==160); assert(board.applies==old&&saves==old_saves); } board.supports240=false;assert(command("set cpu.freq 240")); assert(selected==160&&durable==160&&board.actual==160&&board.applies==0&&saves==0); assert(command("get cpu.freq")); assert(std::string(reply)=="> selected=160 actual=160MHz supported=auto,80,160"); board.supports240=true; assert(command("set cpu.freq 240"));assert(selected==240&&durable==240&&board.actual==240); for(bool ps : {false,true}) { assert(board.applyCpuFrequencyMHz(selected,ps));assert(board.actual==240); } storage=false;assert(command("set cpu.freq 80")); assert(selected==240&&durable==240&&board.actual==240); assert(std::string(reply)=="Error: CPU clock save failed; restored"); storage=true;board.outcomes={false,true};unsigned old_saves=saves; assert(command("set cpu.freq 160"));assert(saves==old_saves); assert(selected==240&&durable==240&&board.actual==240); board.outcomes={true,false};storage=false; assert(command("set cpu.freq 80")); assert(std::string(reply).find("save and restore failed")!=std::string::npos); assert(selected==240&&durable==240); storage=true;board.outcomes.clear(); assert(command("set cpu.freq auto"));assert(selected==0&&durable==0&&board.actual==80); assert(board.applyCpuFrequencyMHz(selected,false));assert(board.actual==160); assert(command("set cpu.freq \t160\t ")); assert(selected==160&&durable==160&&board.actual==160); for(uint16_t value : {0,80,160,240}) { uint8_t footer[FooterSize];encodeFooter(footer,value); uint16_t decoded=999;assert(decodeFooter(footer,sizeof(footer),decoded)&&decoded==value); for(size_t n=0;n>8);footer[7]=uint8_t(crc); decoded=240;assert(!decodeFooter(footer,sizeof(footer),decoded)&&decoded==0); } } ''' PERSISTENCE = r''' int main() { using namespace mesh::esp32::cpuclock; for(float timezone : {-7.0f,-7.5f}) { DataStore store;CompanionNodePrefs prefs; prefs.freq=910.525f;strcpy(prefs.node_name,"preserved");prefs.ble_pin=123456; prefs.tz_offset=timezone;prefs.tx_media=mesh::RADIO_TX_SECONDARY; assert(store.savePrefs(prefs,47.125,-122.25)); const auto legacy=store.fs.files["/new_prefs"]; assert(legacy.size()==236+mesh::companion::traffic::FooterSize +(timezone==-7.5f?mesh::companion::timezone::FooterSize:0)); for(uint16_t value : {80,160,240}) { prefs.cpu_freq_mhz=value;assert(store.savePrefs(prefs,47.125,-122.25)); const auto image=store.fs.files["/new_prefs"]; assert(image.size()==legacy.size()+FooterSize); assert(std::equal(legacy.begin(),legacy.end(),image.begin())); CompanionNodePrefs loaded;double lat=0,lon=0; assert(store.loadPrefsInt("/new_prefs",loaded,lat,lon)); assert(loaded.cpu_freq_mhz==value&&loaded.freq==prefs.freq&&loaded.ble_pin==123456); assert(loaded.tz_offset==timezone&&loaded.tx_media==mesh::RADIO_TX_SECONDARY); assert(lat==47.125&&lon==-122.25); CompanionNodePrefs snapshot; assert(snapshot.copyPersistedValuesFrom(loaded)&&snapshot.cpu_freq_mhz==value); // Every CPU-footer bit fault falls back to Auto without discarding the // independently checked radio, timezone, traffic, identity and location. for(size_t n=0;n>8);footer[7]=uint8_t(crc); bad.fs.files["/new_prefs"]=unsupported;loaded.cpu_freq_mhz=240; assert(bad.loadPrefsInt("/new_prefs",loaded,lat,lon)); assert(loaded.cpu_freq_mhz==0&&loaded.freq==prefs.freq&&loaded.tz_offset==timezone); assert(loaded.ble_pin==123456&&loaded.tx_media==mesh::RADIO_TX_SECONDARY); } for(size_t quota=0;quota #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 / "shim.h").write_text('''#include #include inline char* utoa(unsigned value,char* out,int base){ if(base!=10)abort();sprintf(out,"%u",value);return out; } ''') transaction = (ROOT / "src/helpers/ContactFileTransaction.h").read_text() (work / "ContactFileTransaction.h").write_text(transaction.replace( '#include "IdentityStore.h"', '#include ')) cpp, binary = work / "test.cpp", work / "test" cpp.write_text(code) command = [os.environ.get("CXX", "g++"), "-std=c++17", "-DESP32_PLATFORM=1", "-DESP32_CPU_FREQ=160", "-fsanitize=address,undefined", "-fno-sanitize-recover=all", "-fno-pie", "-no-pie", "-include", str(work / "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)] if persistence: command += [str(ROOT / "src/helpers" / name) for name in ( "ConfigSerializer.cpp", "DynamicConfigSerializer.cpp", "CommonRadioPrefs.cpp", "TxtDataHelpers.cpp")] built = subprocess.run(command + ["-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_policy_cli_transactions_and_checked_footer(self): self.compile_run(POLICY) def test_actual_companion_codec_roundtrip_legacy_and_write_faults(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(")) prefix = HARNESS.split("int main(")[0].replace("@METHODS@", methods) self.compile_run(prefix + PERSISTENCE, persistence=True) def test_actual_common_writers_match_and_footer_is_append_only(self): source = (ROOT / "src/helpers/CommonCLI.cpp").read_text() atomic = "template \n" + method(source, "static bool writeCommonPrefsImage(") retry = "template \n" + method(source, "static void writeCommonRetryPrefs(") save = method(source, "void CommonCLI::savePrefs(FILESYSTEM*") scalar = save[save.index(" uint8_t pad[8];"): save.index(" _common_save_succeeded = file.commit();")] fields_used = set(re.findall(r"(?:_prefs|prefs)->(\w+)", atomic + retry + scalar)) header = (ROOT / "src/helpers/CommonCLI.h").read_text() prefix = header.split("class NodePrefs :", 1)[1].split("private:", 1)[0] fields = [] for kind, declaration in re.findall( r"^\s*(float|double|char|u?int(?:8|16|32)_t)\s+([^;\n]+);", prefix, re.M): declaration = re.sub(r"\s*=\s*[^,;]+", "", declaration) names = re.findall(r"\b[A-Za-z_]\w*\b", declaration) if fields_used.intersection(names): fields.append(kind + " " + declaration + "{};") # Field-array dimensions are the existing shipping limits. Both real # emitters must retain every historical byte and append only CPUv1. constants = "\n".join(re.findall( r"^\s*#define FLOOD_RETRY_(?:PREFIX_SLOTS|PREFIX_LEN|BRIDGE_BUCKETS|BUCKET_PREFIXES|IGNORE_PREFIXES)\s+\d+", header, re.M)) constants += "\n" + "\n".join(re.findall( r"^#define DIRECT_RETRY_PREFS_MAGIC_[01]\s+0x[0-9A-Fa-f]+", header, re.M)) marker = method(source, "static void markDirectRetryPrefsValid(") harness = r''' #include #include #include #include #include #include @CONSTANTS@ struct NodePrefs { @FIELDS@ }; @MARKER@ struct File { std::vector bytes;size_t cursor=0;bool short_write=false; size_t write(const uint8_t* data,size_t size){ if(short_write&&size)--size; bytes.insert(bytes.end(),data,data+size);return size; } size_t size() const{return bytes.size();} bool seek(size_t value){if(value>size())return false;cursor=value;return true;} size_t read(uint8_t* out,size_t count){ count=std::min(count,size()-cursor);memcpy(out,bytes.data()+cursor,count);cursor+=count;return count; } }; @ATOMIC@ @RETRY@ void non_mqtt(File& file,NodePrefs* _prefs){ @SCALAR@ } int main(){ using namespace mesh::esp32::cpuclock; NodePrefs prefs{};prefs.freq=910.525f;prefs.bw=62.5f;prefs.sf=7;prefs.cr=5; prefs.powersaving_enabled=1;strcpy(prefs.node_name,"unchanged prefix"); File legacy;assert(writeCommonPrefsImage(legacy,&prefs)); File nonlegacy;non_mqtt(nonlegacy,&prefs);assert(nonlegacy.bytes==legacy.bytes); assert(readFooter(legacy)==0); for(uint16_t mhz : {80,160,240}){ prefs.cpu_freq_mhz=mhz;File a,b; assert(writeCommonPrefsImage(a,&prefs));non_mqtt(b,&prefs); assert(a.bytes==b.bytes&&a.size()==legacy.size()+FooterSize); assert(std::equal(legacy.bytes.begin(),legacy.bytes.end(),a.bytes.begin())); assert(readFooter(a)==mhz); for(size_t n=0;n