Files
MeshCore/test/test_bridge_recovery.py

289 lines
16 KiB
Python

"""Fault-inject real bridge sources: pool pressure, TX backoff and lost callbacks."""
from pathlib import Path
import os
import re
import subprocess
import tempfile
import unittest
from test_radio_receive_contract import method
ROOT = Path(__file__).resolve().parents[1]
MOCKS = r'''
#include <vector>
#include <algorithm>
#include <array>
#include <cstdio>
#include <helpers/bridges/RS232Heartbeat.h>
#define MAX_TRANS_UNIT 255
using portMUX_TYPE=int;
#define portMUX_INITIALIZER_UNLOCKED 0
struct esp_now_recv_info_t {const uint8_t* src_addr;};
struct esp_now_send_info_t {const uint8_t* des_addr;};
constexpr int ESP_ERR_ESPNOW_NO_MEM=-77;
uint32_t now_ms=100;
uint32_t millis(){return now_ms;}
int send_result=0,send_calls=0;
std::vector<std::vector<uint8_t>> sent;
int esp_now_send(const uint8_t*,const uint8_t* bytes,size_t len){
assert(espnow_active);++send_calls;
if(send_result!=ESP_OK)return send_result;
sent.emplace_back(bytes,bytes+len);return ESP_OK;
}
namespace mesh {
struct RTCClock {};
struct Packet {
uint8_t data[MAX_TRANS_UNIT]={};uint16_t length=4;
bool readFrom(const uint8_t* bytes,size_t len){
if(len==0||len>sizeof(data)||bytes[0]==0xee)return false;
memcpy(data,bytes,len);length=len;return true;
}
int getRawLength()const{return length;}
uint16_t writeTo(uint8_t* out)const{memcpy(out,data,length);return length;}
bool violatesRoutePolicy()const{return false;}
};
struct PacketManager {
std::array<Packet,8> pool;std::array<bool,8> used{};
bool exhausted=false;unsigned frees=0,allocs=0;
std::vector<uint8_t> received;
Packet* allocNew(){
if(exhausted)return nullptr;
for(size_t i=0;i<used.size();++i)if(!used[i]){used[i]=true;++allocs;return &pool[i];}
return nullptr;
}
void free(Packet* p){size_t i=p-pool.data();assert(i<used.size()&&used[i]);used[i]=false;++frees;}
void queueInbound(Packet* p,uint32_t){received.push_back(p->data[0]);free(p);}
};
}
struct NodePrefs {
int bridge_format=mesh::bridge::ESPNOW_FORMAT_RAW;
uint8_t bridge_channel=6;uint32_t bridge_baud=115200,bridge_delay=1;
char bridge_secret[32]="fleet-test";
};
struct Seen {
std::vector<uint8_t> ids;
bool wasSeen(const mesh::Packet* p){return std::find(ids.begin(),ids.end(),p->data[0])!=ids.end();}
void markSeen(const mesh::Packet* p){if(!wasSeen(p))ids.push_back(p->data[0]);}
void clear(const mesh::Packet* p){ids.erase(std::remove(ids.begin(),ids.end(),p->data[0]),ids.end());}
};
class BridgeBase {
public:
static constexpr uint16_t BRIDGE_PACKET_MAGIC=0xc03e;
static constexpr uint16_t BRIDGE_MAGIC_SIZE=2,BRIDGE_LENGTH_SIZE=2,BRIDGE_CHECKSUM_SIZE=2;
NodePrefs* _prefs;mesh::PacketManager* _mgr;mesh::RTCClock* _rtc;
bool _initialized=false,allowed=true;Seen _seen_packets;
BridgeBase(NodePrefs* prefs,mesh::PacketManager* mgr,mesh::RTCClock* rtc):_prefs(prefs),_mgr(mgr),_rtc(rtc){}
virtual ~BridgeBase()=default;
virtual void begin()=0;virtual void end()=0;virtual void loop()=0;
virtual void sendPacket(mesh::Packet*)=0;virtual void onPacketReceived(mesh::Packet*)=0;
bool allowsPacket(const mesh::Packet*)const{return allowed;}
static uint16_t fletcher16(const uint8_t*,size_t);
bool validateChecksum(const uint8_t*,size_t,uint16_t);
void handleReceivedPacket(mesh::Packet*);
};
class Stream {
public:
std::vector<uint8_t> input,output;size_t position=0,write_limit=1000;unsigned reads=0;
int available(){return input.size()-position;}
int availableForWrite(){return static_cast<int>(write_limit);}
int read(){++reads;return position<input.size()?input[position++]:-1;}
size_t write(const uint8_t* bytes,size_t length){
length=std::min(length,write_limit);output.insert(output.end(),bytes,bytes+length);return length;
}
};
class HardwareSerial:public Stream {
public:
bool setPins(int16_t,int16_t){return true;}
void begin(uint32_t){}void end(){}explicit operator bool()const{return true;}
};
'''
def code():
# Reuse the existing SDK/facade model, with its separate caches and shared
# infrastructure owner. Only hardware APIs and Packet allocation are mocked.
sdk = (ROOT / "test/fixtures/espnow_wifi_handoff.cpp").read_text().split("class Bridge {")[0]
sdk = sdk.replace("int esp_now_deinit() { assert(espnow_active); espnow_active=false; return ESP_OK; }",
"int esp_now_deinit() { assert(espnow_active); if(failure==18)return -1; espnow_active=false; return ESP_OK; }")
base = (ROOT / "src/helpers/bridges/BridgeBase.cpp").read_text()
base_methods = "\n".join(method(base, signature) for signature in (
"uint16_t BridgeBase::fletcher16(", "bool BridgeBase::validateChecksum(",
"void BridgeBase::handleReceivedPacket("))
headers, sources = [], []
for name in ("RS232Bridge", "ESPNowBridge"):
header = (ROOT / f"src/helpers/bridges/{name}.h").read_text()
source = (ROOT / f"src/helpers/bridges/{name}.cpp").read_text()
headers.append(re.sub(r"^\s*#(?:include|pragma).*?$", "", header, flags=re.MULTILINE).replace("private:", "public:"))
sources.append(re.sub(r"^\s*#include.*?$", "", source, flags=re.MULTILINE))
return sdk + MOCKS + "\n".join(headers) + base_methods + "\n".join(sources)
COMMON = r'''
static std::vector<uint8_t> uart_frame(uint8_t id){
uint8_t payload[4]={id,1,2,3};uint16_t sum=BridgeBase::fletcher16(payload,4);
return {0xc0,0x3e,0,4,id,1,2,3,uint8_t(sum>>8),uint8_t(sum)};
}
static void append(HardwareSerial& serial,const std::vector<uint8_t>& frame){
serial.input.insert(serial.input.end(),frame.begin(),frame.end());
}
static mesh::Packet packet(uint8_t id,size_t length=4){
mesh::Packet p;p.length=length;memset(p.data,id,sizeof(p.data));return p;
}
static std::vector<uint8_t> uart_frame(const mesh::Packet& p){
const uint16_t sum=BridgeBase::fletcher16(p.data,p.length);
std::vector<uint8_t> result={0xc0,0x3e,uint8_t(p.length>>8),uint8_t(p.length)};
result.insert(result.end(),p.data,p.data+p.length);
result.push_back(uint8_t(sum>>8));result.push_back(uint8_t(sum));return result;
}
static void complete(ESPNowBridge& bridge,int status=ESP_NOW_SEND_SUCCESS){
const uint8_t mac[6]={255,255,255,255,255,255};
bridge.onDataSent(mac,static_cast<esp_now_send_status_t>(status));bridge.loop();
}
'''
class BridgeRecoveryTests(unittest.TestCase):
def run_fixture(self, checks, versions=(50200,)):
with tempfile.TemporaryDirectory(prefix="meshcore-bridge-recovery-") as tmp:
cpp, exe = Path(tmp) / "test.cpp", Path(tmp) / "test"
cpp.write_text(code() + COMMON + checks)
for version in versions:
with self.subTest(idf=version):
result = subprocess.run([
os.environ.get("CXX", "g++"), "-std=c++17", "-O1", "-Wall", "-Wextra",
"-fsanitize=address,undefined", "-fno-omit-frame-pointer", "-fno-pie", "-no-pie",
"-DESP32=1", "-DWITH_ESPNOW_BRIDGE=1", "-DWITH_RS232_BRIDGE=1",
f"-DESP_IDF_VERSION={version}", f"-DESP_IDF_VERSION_MAJOR={version//10000}",
"-I", str(ROOT / "src"), str(cpp), "-o", str(exe),
], capture_output=True, text=True)
self.assertEqual(result.returncode, 0, result.stderr)
result = subprocess.run([str(exe)], capture_output=True, text=True)
self.assertEqual(result.returncode, 0, result.stderr)
def test_uart_pool_pressure_order_parser_bounds_and_restart(self):
self.run_fixture(r'''
int main(){
NodePrefs prefs;mesh::PacketManager mgr;mesh::RTCClock rtc;HardwareSerial serial;
RS232Bridge bridge(&prefs,serial,1,2,&mgr,&rtc);bridge.begin();
append(serial,uart_frame(1));append(serial,uart_frame(2));mgr.exhausted=true;
bridge.loop();assert(bridge._rx_packet_ready&&serial.available()==10&&mgr.received.empty());
const auto read=serial.reads;bridge.loop();assert(serial.reads==read&&mgr.received.empty());
mgr.exhausted=false;bridge.loop();assert((mgr.received==std::vector<uint8_t>{1,2}));
append(serial,{0xc0,0x3e,0,0}); // empty length must resynchronize
append(serial,{0xc0,0x3e,1,1}); // oversized length must resynchronize
auto corrupt=uart_frame(3);corrupt.back()^=1;append(serial,corrupt);
auto invalid=uart_frame(0xee);append(serial,invalid);append(serial,uart_frame(4));
for(unsigned i=0;i<4;++i)bridge.loop();
assert((mgr.received==std::vector<uint8_t>{1,2,4}));
serial.input.insert(serial.input.end(),2000,0x00);const auto before=serial.reads;
bridge.loop();assert(serial.reads-before==512); // noisy UART has bounded loop work
serial.position=serial.input.size();append(serial,uart_frame(5));mgr.exhausted=true;
bridge.loop();assert(bridge._rx_packet_ready);bridge.end();mgr.exhausted=false;bridge.begin();
bridge.loop();assert((mgr.received==std::vector<uint8_t>{1,2,4})); // held frame cannot cross restart
auto outgoing=packet(6),waiting=packet(8);serial.write_limit=2;bridge.sendPacket(&outgoing);
assert(bridge._seen_packets.wasSeen(&outgoing)&&bridge._tx_size==10&&bridge._tx_pos==2);
// A short hardware write retains the accepted complete frame. A caller's
// additional packet cannot overwrite or interleave it, and remains retryable.
bridge.sendPacket(&waiting);assert(!bridge._seen_packets.wasSeen(&waiting));
assert(bridge._tx_size==10&&bridge._tx_pos==4);
serial.write_limit=1000;bridge.loop();assert(serial.output==uart_frame(outgoing)&&bridge._tx_size==0);
bridge.sendPacket(&outgoing);assert(serial.output==uart_frame(outgoing)); // duplicate does not replay the short-written prefix
bridge.sendPacket(&waiting);assert(bridge._seen_packets.wasSeen(&waiting));
auto expected=uart_frame(outgoing);auto tail=uart_frame(waiting);expected.insert(expected.end(),tail.begin(),tail.end());
assert(serial.output==expected); // retry preserves byte-exact ordering
bridge.allowed=false;auto blocked=packet(7);auto total=serial.output.size();
bridge.sendPacket(&blocked);assert(serial.output.size()==total);
bridge.allowed=true;serial.input.clear();serial.position=0;
for(uint8_t id=10;id<40;++id)append(serial,uart_frame(id));
bridge.loop();assert(serial.available()==0&&mgr.received.size()==33); // one bounded batch preserves throughput
}
''')
def test_espnow_rx_pool_pressure_reassembly_and_filter_ownership(self):
self.run_fixture(r'''
int main(){
Backend backend;backend.wifi=true;mesh::wireless::control().begin(backend);
sdk_initialized=sdk_started=true;sdk_mode=WIFI_STA;
NodePrefs prefs;mesh::PacketManager mgr;mesh::RTCClock rtc;ESPNowBridge bridge(&prefs,&mgr,&rtc);bridge.begin();
const uint8_t mac[6]={1,2,3,4,5,6};auto p=packet(1);
bridge.queueReceivedFrame(mac,p.data,p.length);mgr.exhausted=true;bridge.loop();
assert(bridge._rx_count==1&&mgr.received.empty());
mgr.exhausted=false;bridge.loop();assert((mgr.received==std::vector<uint8_t>{1}));
mesh::espnow::ESPNowRawFrames fragments;auto full=packet(2,255);
assert(mesh::espnow::encodeEspNowRawFrames(full.data,full.length,fragments));
bridge.queueReceivedFrame(mac,fragments.data[0],fragments.lengths[0]);bridge.loop();
bridge.queueReceivedFrame(mac,fragments.data[1],fragments.lengths[1]);mgr.exhausted=true;
bridge.loop();assert(bridge._rx_count==1&&mgr.received.size()==1);
mgr.exhausted=false;bridge.loop();assert((mgr.received==std::vector<uint8_t>{1,2}));
auto blocked=packet(3);bridge.allowed=false;bridge.queueReceivedFrame(mac,blocked.data,blocked.length);
bridge.loop();assert(mgr.received.size()==2);bridge.allowed=true;
auto bad=packet(0xee);bridge.queueReceivedFrame(mac,bad.data,bad.length);bridge.loop();
assert(mgr.received.size()==2&&mgr.allocs==mgr.frees);
bridge.end();prefs.bridge_format=mesh::bridge::ESPNOW_FORMAT_WRAPPED;bridge.begin();
auto wrapped=packet(4);uint8_t bytes[8]={0xc0,0x3e};uint16_t sum=BridgeBase::fletcher16(wrapped.data,4);
bytes[2]=sum>>8;bytes[3]=sum;memcpy(bytes+4,wrapped.data,4);assert(bridge.xorCrypt(bytes+2,6));
bridge.queueReceivedFrame(mac,bytes,sizeof(bytes));mgr.exhausted=true;bridge.loop();
assert(bridge._rx_count==1);mgr.exhausted=false;bridge.loop();
assert((mgr.received==std::vector<uint8_t>{1,2,4}));
bridge.end();assert(mgr.allocs==mgr.frees&&sdk_started); // shared WiFi survives
}
''')
def test_espnow_start_retry_budget_wrap_and_fragment_order(self):
self.run_fixture(r'''
int main(){
Backend backend;backend.wifi=true;mesh::wireless::control().begin(backend);
sdk_initialized=sdk_started=true;sdk_mode=WIFI_STA;
NodePrefs prefs;mesh::PacketManager mgr;mesh::RTCClock rtc;ESPNowBridge bridge(&prefs,&mgr,&rtc);bridge.begin();
auto p=packet(1,255);bridge.sendPacket(&p);send_result=ESP_ERR_ESPNOW_NO_MEM;
now_ms=UINT32_MAX-19;bridge.loop();assert(send_calls==1&&bridge._tx_count==1);
for(unsigned i=0;i<3;++i){now_ms+=19;bridge.loop();assert(send_calls==int(i+1));++now_ms;bridge.loop();}
assert(send_calls==4&&bridge._tx_count==0&&!bridge._seen_packets.wasSeen(&p));
send_result=ESP_OK;bridge.sendPacket(&p);bridge.loop();assert(sent.size()==1);
complete(bridge);assert(sent.size()==2&&bridge._tx_count==1);complete(bridge);assert(bridge._tx_count==0);
mesh::espnow::ESPNowRawReassembler assembler;uint8_t out[255],mac[6]={};size_t size=0;
assembler.acceptFrame(mac,sent[0].data(),sent[0].size(),now_ms,out,sizeof(out),size);
assert(assembler.acceptFrame(mac,sent[1].data(),sent[1].size(),now_ms,out,sizeof(out),size)==mesh::espnow::ESPNowRawReassemblyResult::PACKET_COMPLETE);
assert(size==255&&!memcmp(out,p.data,size));
auto fail=packet(2,255);bridge.sendPacket(&fail);bridge.loop();const size_t prior=sent.size();
for(unsigned i=0;i<3;++i){
complete(bridge,ESP_NOW_SEND_FAIL);assert(sent.size()==prior+i&&bridge._tx_count==1);
now_ms+=19;bridge.loop();assert(sent.size()==prior+i);
++now_ms;bridge.loop();assert(sent.size()==prior+i+1);
}
complete(bridge,ESP_NOW_SEND_FAIL);assert(bridge._tx_count==0); // bounded budget, no orphan sibling
for(size_t i=prior-1;i<sent.size();++i)assert(sent[i]==sent[prior-1]); // retries are part0 only
auto two=packet(3,255);bridge.sendPacket(&two);bridge.loop();send_result=ESP_ERR_ESPNOW_NO_MEM;
complete(bridge);assert(bridge._tx_count==1&&bridge._tx_queue[bridge._tx_tail].next_frame==1);
send_result=ESP_OK;now_ms+=20;bridge.loop();complete(bridge);assert(bridge._tx_count==0);
bridge.end();
}
''', (40400, 50200, 50500))
def test_missing_callback_restarts_session_and_failed_teardown_blocks_admission(self):
self.run_fixture(r'''
int main(){
Backend backend;backend.wifi=true;mesh::wireless::control().begin(backend);
sdk_initialized=sdk_started=true;sdk_mode=WIFI_STA;
NodePrefs prefs;mesh::PacketManager mgr;mesh::RTCClock rtc;ESPNowBridge bridge(&prefs,&mgr,&rtc);bridge.begin();
auto p=packet(1,255),waiting=packet(2);bridge.sendPacket(&p);bridge.sendPacket(&waiting);bridge.loop();
now_ms+=999;bridge.loop();assert(send_calls==1&&bridge._tx_count==2);
++now_ms;bridge.loop();assert(bridge._initialized&&bridge._tx_count==0&&send_calls==1);
assert(!bridge._seen_packets.wasSeen(&waiting)&&sdk_started); // untouched WiFi, unsent can retry
complete(bridge);assert(send_calls==1); // callback arriving before a new send cannot fake progress
bridge.sendPacket(&waiting);bridge.loop();complete(bridge);assert(bridge._tx_count==0);
auto bad=packet(3,255);bridge.sendPacket(&bad);bridge.loop();failure=18;now_ms+=1000;bridge.loop();
assert(!bridge._initialized&&bridge._sdk_teardown_failed&&espnow_active);
const int started=send_calls;bridge.sendPacket(&p);bridge.loop();complete(bridge);
bridge.begin();assert(!bridge._initialized&&send_calls==started); // old driver still owns accepted TX
failure=0;bridge.begin();assert(bridge._initialized&&!bridge._sdk_teardown_failed);
auto good=packet(4);bridge.sendPacket(&good);bridge.loop();complete(bridge);
assert(bridge._tx_count==0&&send_calls==started+1);bridge.end();
}
''')
if __name__ == "__main__":
unittest.main()