Files
MeshCore/test/test_companion_ethernet.py
T

567 lines
29 KiB
Python

"""Exercise actual Companion Ethernet settings/lifecycle at host boundaries.
No PlatformIO, hardware, network listener or NVS device is used. The portable
settings image and extracted Companion method bodies remain production code.
"""
import os
from pathlib import Path
import shutil
import subprocess
import tempfile
import unittest
from cpp_source import body
ROOT = Path(__file__).resolve().parents[1]
SETTINGS_HARNESS = r'''
#include <cassert>
#include <cmath>
#include <cstring>
#include <initializer_list>
#include <limits>
#include <helpers/EthernetTunnelSettings.h>
using namespace mesh;
using namespace mesh::ethernet;
static void checksum(uint8_t* bytes) {
const uint32_t crc=settingsChecksum(bytes,SettingsSize-4);
memcpy(bytes+SettingsSize-4,&crc,sizeof(crc));
}
static Settings configured() {
Settings value;
value.configured=true;
value.radio.freq=909.5;
value.radio.bw=500;
value.radio.sf=5;
value.radio.cr=5;
value.radio.preamble=32;
return value;
}
int main() {
uint8_t bytes[SettingsSize];
Settings value,loaded;
assert(validSettings(value));
encodeSettings(bytes,value);
assert(decodeSettings(bytes,sizeof(bytes),loaded));
assert(!loaded.enabled && !loaded.configured);
assert(loaded.radio.protocol==RadioProfileProtocol::Ethernet);
assert(!decodeSettings(nullptr,sizeof(bytes),loaded));
for (size_t size=0;size<SettingsSize;++size) assert(!decodeSettings(bytes,size,loaded));
assert(!decodeSettings(bytes,SettingsSize+1,loaded));
value=configured(); value.enabled=true;
encodeSettings(bytes,value);
assert(decodeSettings(bytes,sizeof(bytes),loaded));
assert(loaded.enabled && loaded.configured && loaded.radio==value.radio);
const auto original=loaded;
for(size_t at=0;at<sizeof(bytes);++at) {
bytes[at]^=1;
assert(!decodeSettings(bytes,sizeof(bytes),loaded));
assert(loaded.enabled==original.enabled && loaded.configured==original.configured
&& loaded.radio==original.radio);
bytes[at]^=1;
}
for(size_t at : {size_t(4),size_t(5)}) {
bytes[at]=2; checksum(bytes); assert(!decodeSettings(bytes,sizeof(bytes),loaded));
encodeSettings(bytes,value);
}
for(size_t at=18;at<SettingsSize-4;++at) {
bytes[at]=1; checksum(bytes); assert(!decodeSettings(bytes,sizeof(bytes),loaded));
encodeSettings(bytes,value);
}
value.configured=false;
assert(!validSettings(value));
encodeSettings(bytes,value); assert(!decodeSettings(bytes,sizeof(bytes),loaded));
value=configured();
for(float bad : {0.f,149.f,2501.f,std::numeric_limits<float>::infinity(),
std::numeric_limits<float>::quiet_NaN()}) {
value.radio.freq=bad; assert(!validSettings(value));
encodeSettings(bytes,value); assert(!decodeSettings(bytes,sizeof(bytes),loaded));
}
value=configured();
for(float bad : {0.f,124.f,std::numeric_limits<float>::infinity(),
std::numeric_limits<float>::quiet_NaN()}) {
value.radio.bw=bad; assert(!validSettings(value));
encodeSettings(bytes,value); assert(!decodeSettings(bytes,sizeof(bytes),loaded));
}
value=configured();
for(uint16_t bad : {0,1,7,65529,65535}) {
value.radio.preamble=bad; assert(!validSettings(value));
encodeSettings(bytes,value); assert(!decodeSettings(bytes,sizeof(bytes),loaded));
}
value=configured(); value.radio.protocol=RadioProfileProtocol::MeshCore;
assert(!validSettings(value));
// Foreign profiles bypass MeshCore's scan-preamble recommendation, not the
// RadioLib 32-bit microsecond airtime bound. Reject even a CRC-valid tuple
// that would overflow that clock before it can be persisted or used at boot.
value=configured(); value.radio.bw=7.8f; value.radio.sf=12;
value.radio.preamble=65528;
assert(!validSettings(value));
encodeSettings(bytes,value); assert(!decodeSettings(bytes,sizeof(bytes),loaded));
value=configured();
for(uint8_t bad : {0,4,13,255}) {
value.radio.sf=bad; assert(!validSettings(value));
encodeSettings(bytes,value); assert(!decodeSettings(bytes,sizeof(bytes),loaded));
}
value=configured();
for(uint8_t bad : {0,4,9,255}) {
value.radio.cr=bad; assert(!validSettings(value));
encodeSettings(bytes,value); assert(!decodeSettings(bytes,sizeof(bytes),loaded));
}
}
'''
COMPANION_HARNESS = r'''
@RADIO_FIXTURE@
#include <helpers/EthernetTunnelSettings.h>
#include <helpers/EthernetKiss.h>
#include <helpers/CLICommandUtils.h>
#include <helpers/CompanionChannelAlias.h>
#include <deque>
#include <vector>
struct Preferences {
static inline std::vector<uint8_t> blob;
static inline bool fail_begin=false,fail_write=false,fail_read=false,fail_remove=false;
static inline unsigned writes=0;
bool begin(const char* name,bool read_only) {
assert(!strcmp(name,"mesh-eth") && !read_only); return !fail_begin;
}
size_t getBytesLength(const char* key) { assert(!strcmp(key,"settings")); return blob.size(); }
size_t getBytes(const char*,void* out,size_t size) {
if(fail_read)return 0;
size=std::min(size,blob.size()); memcpy(out,blob.data(),size); return size;
}
size_t putBytes(const char*,const void* in,size_t size) {
++writes; if(fail_write)return 0;
const auto* bytes=static_cast<const uint8_t*>(in); blob.assign(bytes,bytes+size); return size;
}
bool remove(const char*) { if(fail_remove)return false; blob.clear(); return true; }
void end() {}
static void reset() { blob.clear(); writes=0; fail_begin=fail_write=fail_read=fail_remove=false; }
};
namespace mesh { namespace ethernet {
// Socket lifecycle is a boundary substitute. Every byte in both directions
// still traverses the production bounded KISS codec.
class KissServer {
KissCodec codec;
public:
static constexpr uint16_t Port=5003;
struct Status { bool enabled=false,listening=false,connected=false; uint32_t accepted=0,disconnects=0; };
Status state;
bool wifi=true;
bool attach_pending=false,detach_pending=false;
unsigned resets=0;
void resetPending() {
++resets;
if(state.connected)++state.disconnects;
state.connected=false;
codec.resetPending();
}
void loop(bool enabled) {
state.enabled=enabled; state.listening=enabled && wifi;
if(!state.listening) {resetPending();attach_pending=detach_pending=false;return;}
if(detach_pending) {resetPending();detach_pending=false;}
if(attach_pending) {codec.resetPending();state.connected=true;++state.accepted;attach_pending=false;}
}
Status status() const { return state; }
void connect() { assert(state.listening);attach_pending=true; }
void disconnect() { detach_pending=true; }
bool peekTransmit(const uint8_t*& bytes,size_t& size) const { return codec.peekTransmit(bytes,size); }
void consumeTransmit() { codec.consumeTransmit(); }
bool queueReceive(const uint8_t* bytes,size_t size) { return state.connected && codec.queueReceive(bytes,size); }
void feed(const std::vector<uint8_t>& frame) {
assert(state.connected);
for(uint8_t byte:frame)assert(codec.feed(byte));
}
std::vector<uint8_t> output() {
const uint8_t* bytes;size_t size;
if(!codec.peekOutput(bytes,size))return {};
std::vector<uint8_t> out(bytes,bytes+size);codec.consumeOutput(size);return out;
}
};
} }
static constexpr int MAX_GROUP_CHANNELS=4;
struct ChannelDetails {char name[32]={};};
struct BaseChatMesh {
ChannelDetails channels[MAX_GROUP_CHANNELS];
unsigned channel_reads=0;
bool getChannel(int index,ChannelDetails& out) {++channel_reads;out=channels[index];return true;}
bool setChannel(int index,const ChannelDetails& value) {channels[index]=value;return true;}
};
struct EthernetRadio : Radio {
bool supported=true;
mutable unsigned validations=0;
bool supportsEthernetTunnel() const override {return supported;}
bool validateProfile(const mesh::RadioProfileParams& value) const override {
++validations;return Radio::validateProfile(value);
}
};
class MyMesh : public BaseChatMesh {
public:
MemoryFS fs;Clock clock;EthernetRadio radio;mesh::Radio* _radio=&radio;
mesh::RadioProfileCLI _radio_profiles;
mesh::ethernet::Settings _ethernet_settings;
bool _ethernet_settings_valid=true,_ethernet_active=false,_ethernet_owned=false;
uint32_t _ethernet_generation=0;
uint32_t _ethernet_intent_generation=0,_ethernet_channel_generation=0;
uint32_t _ethernet_checked_channels=UINT32_MAX;
bool _ethernet_channels_conflict=false;
const char* _ethernet_status="off";
mesh::ethernet::KissServer _ethernet_kiss;
uint32_t _ethernet_received=0,_ethernet_sent=0,_ethernet_failed=0;
bool _radio_available=true,foreign_pending=false,admit_foreign=true;
mesh::RadioProfileProtocol foreign_protocol=mesh::RadioProfileProtocol::MeshCore;
uint8_t foreign_profile=255;
uint32_t foreign_generation=0;
unsigned admissions=0,discards=0;
std::vector<uint8_t> foreign_bytes;
MyMesh() {g_mock_millis=0;_radio_profiles.begin(&fs,&radio,&clock);}
void beginEthernetTunnel();
bool ethernetChannelConflict();
void releaseEthernetProfile();
bool saveEthernetSettings(const mesh::ethernet::Settings&);
bool eraseEthernetSettings();
bool ethernetRadioConflict(const mesh::ethernet::Settings&);
bool handleEthernetTunnelCommand(const char*,char*,size_t);
void loopEthernetTunnel();
bool onEthernetFrame(const uint8_t*,size_t,uint8_t,uint32_t);
bool setChannel(int,const ChannelDetails&);
bool onForeignRadioFrame(const uint8_t*,size_t,uint8_t,uint32_t,float,float);
void onForeignRadioSendComplete(bool);
mesh::RadioProfileProtocol queuedForeignRadioProtocol() const {return foreign_protocol;}
bool hasForeignRadioFrame() const {return foreign_pending;}
void discardQueuedForeignRadioFrame(mesh::RadioProfileProtocol protocol) {
if(foreign_pending && protocol==foreign_protocol) {foreign_pending=false;++discards;}
}
bool queueForeignRadioFrame(const uint8_t* bytes,size_t size,uint8_t profile,uint32_t generation,
mesh::RadioProfileProtocol protocol) {
if(foreign_pending || !admit_foreign)return false;
foreign_pending=true;++admissions;foreign_bytes.assign(bytes,bytes+size);
foreign_profile=profile;foreign_generation=generation;foreign_protocol=protocol;return true;
}
const char* command(const char* text) {
static char reply[256];memset(reply,0x55,sizeof(reply));
assert(handleEthernetTunnelCommand(text,reply,sizeof(reply)));
assert(memchr(reply,0,sizeof(reply)));return reply;
}
void cli(const char* text) {
char reply[160];assert(_radio_profiles.handle(text,reply,sizeof(reply)));
if(strncmp(reply,"OK",2)) {fprintf(stderr,"%s -> %s\n",text,reply);assert(false);}
g_mock_millis+=2000;clock.epoch+=2;_radio_profiles.loop();
}
};
@METHODS@
static void ok(MyMesh& node,const char* text) {assert(!strncmp(node.command(text),"OK",2));}
static void error(MyMesh& node,const char* text) {assert(!strncmp(node.command(text),"Error",5));}
static void active(MyMesh& node) {
node.beginEthernetTunnel();
ok(node,"set ethernet.radio 909.5,500,5,5");
ok(node,"set ethernet on");node.loopEthernetTunnel();
assert(node._ethernet_active && node._ethernet_owned && node.radio.p.enabled());
assert(node.radio.p.secondary.params.protocol==mesh::RadioProfileProtocol::Ethernet);
}
static std::vector<uint8_t> ethernet(uint16_t type=0x0800,size_t size=244) {
std::vector<uint8_t> frame(size);
for(size_t i=0;i<size;++i)frame[i]=static_cast<uint8_t>(i);
frame[12]=type>>8;frame[13]=type&255;frame[14]=0xc0;frame[15]=0xdb;
return frame;
}
static std::vector<uint8_t> kiss(const std::vector<uint8_t>& ethernet) {
std::vector<uint8_t> out={0xc0,0};
for(auto byte:ethernet) {
if(byte==0xc0)out.insert(out.end(),{0xdb,0xdc});
else if(byte==0xdb)out.insert(out.end(),{0xdb,0xdd});
else out.push_back(byte);
}
out.push_back(0xc0);return out;
}
static void defaults_cli_and_reboot() {
Preferences::reset();MyMesh node;node.beginEthernetTunnel();node.loopEthernetTunnel();
assert(node._ethernet_settings_valid && !node._ethernet_active && !node.radio.p.enabled());
assert(strstr(node.command("get ethernet"),"ethernet=off"));
assert(strstr(node.command("get ethernet.radio"),"unset"));
error(node,"set ethernet on");assert(Preferences::writes==0);
for(const char* bad:{"set ethernet","set ethernet ON","set ethernet on extra",
"set ethernet.radio","set ethernet.radio 909.5,500,5",
"set ethernet.radio 909.5,500,5,5,garbage","set ethernet.radio 909.5,500,4,5",
"set ethernet.radio 909.5,500,5,5,7","set ethernet.radio 0,500,5,5"})error(node,bad);
assert(Preferences::writes==0);
char reply[8];assert(!node.handleEthernetTunnelCommand("get ethernetx",reply,sizeof(reply)));
assert(!node.handleEthernetTunnelCommand("set ethernetx on",reply,sizeof(reply)));
ok(node,"set ethernet.radio 909.5,500,5,5");
assert(node._ethernet_settings.radio.preamble==32 && !node._ethernet_settings.enabled);
ok(node,"set ethernet on");node.loopEthernetTunnel();
const auto generation=node.radio.p.generation[1];
assert(node._ethernet_active && node._ethernet_kiss.status().listening);
assert(node._ethernet_settings.radio.protocol==mesh::RadioProfileProtocol::Ethernet);
assert(node.fs.files.empty());
const auto validations=node.radio.validations;
const auto scans=node.channel_reads;
for(int i=0;i<100;++i)node.loopEthernetTunnel();
assert(node.radio.p.generation[1]==generation && Preferences::writes==2);
assert(node.radio.validations==validations && node.channel_reads==scans);
for(const char* command:{"get ethernet","get ethernet.radio","get ethernet.status"}) {
char bounded[4]={'a','b','c','d'};
assert(node.handleEthernetTunnelCommand(command,bounded+1,1));
assert(bounded[0]=='a' && bounded[1]==0 && bounded[2]=='c' && bounded[3]=='d');
bounded[1]='b';assert(node.handleEthernetTunnelCommand(command,bounded+1,0));
assert(bounded[0]=='a' && bounded[1]=='b' && bounded[2]=='c' && bounded[3]=='d');
}
MyMesh reboot;reboot.beginEthernetTunnel();reboot.loopEthernetTunnel();
assert(reboot._ethernet_active && reboot._ethernet_settings.radio==node._ethernet_settings.radio);
ok(node,"set ethernet off");node.loopEthernetTunnel();
assert(!node._ethernet_active && !node.radio.p.enabled() && !node._ethernet_kiss.status().listening);
MyMesh off;off.beginEthernetTunnel();off.loopEthernetTunnel();assert(!off._ethernet_active);
}
static void persistence_faults() {
for(int fault=0;fault<3;++fault) {
Preferences::reset();MyMesh node;active(node);
const auto before=Preferences::blob;
if(fault==0)Preferences::fail_begin=true;
if(fault==1)Preferences::fail_write=true;
if(fault==2)Preferences::fail_read=true;
error(node,"set ethernet off");
if(fault==0)assert(node._ethernet_active && node._ethernet_settings_valid);
else assert(!node._ethernet_active && !node._ethernet_settings_valid && !node.radio.p.enabled());
Preferences::fail_begin=Preferences::fail_write=Preferences::fail_read=false;
if(fault<2)assert(Preferences::blob==before);
MyMesh reboot;reboot.beginEthernetTunnel();reboot.loopEthernetTunnel();
assert(reboot._ethernet_active==(fault!=2));
}
Preferences::reset();MyMesh source;active(source);const auto good=Preferences::blob;
for(size_t at=0;at<good.size();++at) {
Preferences::blob=good;Preferences::blob[at]^=1;
MyMesh boot;boot.beginEthernetTunnel();boot.loopEthernetTunnel();
assert(!boot._ethernet_settings_valid && !boot._ethernet_active && !boot.radio.p.enabled());
}
for(size_t size:{size_t(1),size_t(31),size_t(33)}) {
Preferences::blob=good;Preferences::blob.resize(size);
MyMesh boot;boot.beginEthernetTunnel();boot.loopEthernetTunnel();assert(!boot._ethernet_active);
}
Preferences::blob=good;MyMesh erase;erase.beginEthernetTunnel();erase.loopEthernetTunnel();
Preferences::fail_remove=true;assert(!erase.eraseEthernetSettings());
assert(!erase._ethernet_active && !erase._ethernet_settings_valid);
Preferences::fail_remove=false;assert(erase.eraseEthernetSettings());
assert(Preferences::blob.empty() && !erase._ethernet_settings.enabled && erase._ethernet_settings_valid);
MyMesh reboot;reboot.beginEthernetTunnel();reboot.loopEthernetTunnel();assert(!reboot._ethernet_active);
}
static void profile_conflicts_and_recovery() {
for(const char* name:{"#r2-emergency","#mt-longfast"}) {
Preferences::reset();MyMesh node;node.beginEthernetTunnel();
ok(node,"set ethernet.radio 909.5,500,5,5,32");strcpy(node.channels[0].name,name);++node._ethernet_channel_generation;
error(node,"set ethernet on");assert(!node._ethernet_active);
node.channels[0].name[0]=0;++node._ethernet_channel_generation;ok(node,"set ethernet on");node.loopEthernetTunnel();
assert(node._ethernet_active);strcpy(node.channels[0].name,name);++node._ethernet_channel_generation;node.loopEthernetTunnel();
assert(!node._ethernet_active && !node.radio.p.enabled());
node.channels[0].name[0]=0;++node._ethernet_channel_generation;node.loopEthernetTunnel();assert(node._ethernet_active);
}
for(const char* tuple:{"set radio2 off","set radio2 909.5,500,5,5,rx,32",
"set radio2 909.6,500,5,5,rxtx,32","set radio2 909.5,500,5,5,rxtx,40"}) {
Preferences::reset();MyMesh node;node.beginEthernetTunnel();node.cli(tuple);
const auto saved=node.radio.p.secondary;
ok(node,"set ethernet.radio 909.5,500,5,5,32");error(node,"set ethernet on");
assert(node.radio.p.secondary.mode==saved.mode && node.radio.p.secondary.params==saved.params);
}
Preferences::reset();MyMesh manual;manual.beginEthernetTunnel();
manual.cli("set radio2 909.5,500,5,5,rxtx,32");const auto saved_files=manual.fs.files;
ok(manual,"set ethernet.radio 909.5,500,5,5,32");ok(manual,"set ethernet on");manual.loopEthernetTunnel();
assert(manual._ethernet_active && manual.fs.files==saved_files);
ok(manual,"set ethernet off");manual.loopEthernetTunnel();
assert(manual.radio.p.secondary.mode==mesh::RadioProfileMode::RxTx
&& manual.radio.p.secondary.params.protocol==mesh::RadioProfileProtocol::MeshCore);
assert(manual.fs.files==saved_files);
Preferences::reset();MyMesh unavailable;active(unavailable);
unavailable._radio_available=false;unavailable.loopEthernetTunnel();assert(!unavailable._ethernet_active);
unavailable._radio_available=true;unavailable.loopEthernetTunnel();assert(unavailable._ethernet_active);
unavailable.radio.supported=false;unavailable.loopEthernetTunnel();assert(!unavailable._ethernet_active);
unavailable.radio.supported=true;unavailable.loopEthernetTunnel();assert(unavailable._ethernet_active);
unavailable._ethernet_kiss.wifi=false;unavailable.loopEthernetTunnel();
assert(unavailable._ethernet_active && !unavailable._ethernet_kiss.status().listening);
unavailable._ethernet_kiss.wifi=true;unavailable.loopEthernetTunnel();
assert(unavailable._ethernet_kiss.status().listening);
}
static void opaque_frames_and_no_primary_fallback() {
Preferences::reset();MyMesh node;active(node);node._ethernet_kiss.connect();node.loopEthernetTunnel();
for(uint16_t type:{0x0800,0x0806,0x86dd,0x1234}) {
const auto frame=ethernet(type);
node._ethernet_kiss.feed(kiss(frame));node.admit_foreign=false;node.loopEthernetTunnel();
assert(!node.foreign_pending);node.admit_foreign=true;node.loopEthernetTunnel();
assert(node.foreign_pending && node.foreign_bytes==frame && node.foreign_profile==1);
assert(node.foreign_protocol==mesh::RadioProfileProtocol::Ethernet);
assert(node.foreign_generation==node.radio.p.generation[1]);
error(node,"set ethernet off");assert(node._ethernet_active);
node.foreign_pending=false;
assert(node.onEthernetFrame(frame.data(),frame.size(),1,node.radio.p.generation[1]));
assert(node._ethernet_kiss.output()==kiss(frame));
assert(!node.onEthernetFrame(frame.data(),frame.size(),0,node.radio.p.generation[0]));
assert(!node.onEthernetFrame(frame.data(),frame.size(),1,node.radio.p.generation[1]-1));
}
assert(node.admissions==4 && node._ethernet_received==4);
node.foreign_protocol=mesh::RadioProfileProtocol::Ethernet;
node.onForeignRadioSendComplete(true);node.onForeignRadioSendComplete(false);
assert(node._ethernet_sent==1 && node._ethernet_failed==1);
node.foreign_protocol=mesh::RadioProfileProtocol::Meshtext;
node.onForeignRadioSendComplete(true);node.onForeignRadioSendComplete(false);
assert(node._ethernet_sent==1 && node._ethernet_failed==1);
for(size_t size:{size_t(13),size_t(256)}) {
const std::vector<uint8_t> invalid(size,0x11);
assert(!node.onEthernetFrame(invalid.data(),invalid.size(),1,node.radio.p.generation[1]));
}
const auto frame=ethernet();node._ethernet_kiss.feed(kiss(frame));node.loopEthernetTunnel();
const auto admissions=node.admissions;assert(node.foreign_pending);
node._radio_available=false;node.loopEthernetTunnel();
assert(!node.foreign_pending && node.discards>=1 && node.admissions==admissions && !node._ethernet_active);
node._radio_available=true;node.loopEthernetTunnel();
assert(node._ethernet_active && !node._ethernet_kiss.status().connected);
assert(!node.onEthernetFrame(frame.data(),frame.size(),1,node.radio.p.generation[1]));
node._ethernet_kiss.connect();node.loopEthernetTunnel();
node._ethernet_kiss.feed(kiss(frame));node.loopEthernetTunnel();assert(node.foreign_pending);
node._ethernet_kiss.disconnect();node.loopEthernetTunnel();
assert(!node.foreign_pending && !node._ethernet_kiss.status().connected);
node._ethernet_kiss.connect();node.loopEthernetTunnel();
node._ethernet_kiss.feed(kiss(frame));node.loopEthernetTunnel();assert(node.foreign_pending);
assert(node.eraseEthernetSettings());assert(!node.foreign_pending && !node._ethernet_active);
}
static void temporary_and_scheduled_ownership() {
Preferences::reset();MyMesh node;active(node);
node.cli("set tempradio2 910.5,500,5,5,rxtx,1,32");node.loopEthernetTunnel();
assert(!node._ethernet_active && node.radio.p.secondary_temporary
&& node.radio.p.secondary.params.freq==910.5f);
const auto temporary=node.radio.p.secondary.params;
error(node,"set ethernet on");assert(node.radio.p.secondary.params==temporary);
node.cli("set tempradio2 off");node.loopEthernetTunnel();assert(node._ethernet_active);
char command[128];
snprintf(command,sizeof(command),"set radioat2 911.5,500,5,5,rxtx,%lu,32",
static_cast<unsigned long>(node.clock.epoch+120));
node.cli(command);node.loopEthernetTunnel();assert(!node._ethernet_active);
assert(!node.radio.p.enabled());error(node,"set ethernet on");
node.cli("del radioat2 all");node.loopEthernetTunnel();assert(node._ethernet_active);
}
static void unused_meshtext_owner_handoff() {
Preferences::reset();MyMesh node;node.beginEthernetTunnel();
mesh::RadioProfileConfig old;
old.mode=mesh::RadioProfileMode::RxTx;old.params.freq=906.875;old.params.bw=250;
old.params.sf=11;old.params.cr=5;old.params.preamble=16;
old.params.protocol=mesh::RadioProfileProtocol::Meshtext;
assert(node._radio_profiles.requestAutoSecondary(old));
assert(node._radio_profiles.autoSecondaryActive());
ok(node,"set ethernet.radio 909.5,500,5,5,32");
assert(node.radio.p.secondary.params.protocol==mesh::RadioProfileProtocol::Meshtext);
ok(node,"set ethernet on");node.loopEthernetTunnel();
assert(node._ethernet_active && node.radio.p.secondary.params.protocol==mesh::RadioProfileProtocol::Ethernet);
}
static void real_channel_setter_invalidates_cache_without_evicting() {
Preferences::reset();MyMesh node;active(node);
const auto generation=node._ethernet_channel_generation;
ChannelDetails candidate;
for(const char* name:{"#r2-emergency","#mt-longfast","#r2-"}) {
strcpy(candidate.name,name);assert(!node.setChannel(0,candidate));
assert(node._ethernet_channel_generation==generation && !node.channels[0].name[0]);
}
strcpy(candidate.name,"#emergency");assert(node.setChannel(0,candidate));
assert(node._ethernet_channel_generation==generation+1);
node.loopEthernetTunnel();assert(node._ethernet_active && !node.ethernetChannelConflict());
candidate={};assert(node.setChannel(0,candidate));
assert(node._ethernet_channel_generation==generation+2);
node.loopEthernetTunnel();assert(node._ethernet_active);
}
int main() {
defaults_cli_and_reboot();persistence_faults();profile_conflicts_and_recovery();
opaque_frames_and_no_primary_fallback();temporary_and_scheduled_ownership();unused_meshtext_owner_handoff();
real_channel_setter_invalidates_cache_without_evicting();
}
'''
class CompanionEthernetTest(unittest.TestCase):
def compile_run(self, text, sources=(), includes=(), definitions=()):
compiler = os.environ.get("CXX") or shutil.which("g++") or shutil.which("clang++")
if not compiler:
self.skipTest("a host C++ compiler is required")
with tempfile.TemporaryDirectory(prefix="meshcore-ethernet-companion-") as directory:
work = Path(directory)
(work / "test.cpp").write_text(text, encoding="utf-8")
command = [compiler, "-std=c++17", "-Wall", "-Wextra",
"-fsanitize=address,undefined", "-fno-pie", "-no-pie"]
command.extend("-D" + item for item in definitions)
for path in (*includes, ROOT / "src"):
command.extend(("-I", str(path)))
command.extend((str(work / "test.cpp"), *(str(path) for path in sources),
"-o", str(work / "test")))
build = subprocess.run(command, capture_output=True, text=True)
diagnostics = build.stdout + build.stderr
errors = "\n".join(line for line in diagnostics.splitlines()
if "error:" in line or "undefined reference" in line)
self.assertEqual(build.returncode, 0, errors or diagnostics)
run = subprocess.run([str(work / "test")], capture_output=True, text=True)
self.assertEqual(run.returncode, 0, run.stdout + run.stderr)
def test_checked_settings_image_is_atomic_and_fail_closed(self):
self.compile_run(SETTINGS_HARNESS)
def test_production_companion_lifecycle_routing_and_persistence(self):
source = (ROOT / "examples/companion_radio/EthernetCompanion.cpp").read_text(encoding="utf-8")
methods = source[source.index("void MyMesh::beginEthernetTunnel()"):].rsplit("#endif", 1)[0]
companion = (ROOT / "examples/companion_radio/MyMesh.cpp").read_text(encoding="utf-8")
methods += "\n" + body(companion, "bool MyMesh::setChannel(")
fixture = (ROOT / "test/fixtures/radio_profiles/cli_test.cpp").read_text(encoding="utf-8")
fixture = fixture[:fixture.index("struct Fixture {")]
harness = COMPANION_HARNESS.replace("@RADIO_FIXTURE@", fixture).replace("@METHODS@", methods)
self.compile_run(
harness,
sources=(ROOT / "src/helpers/RadioProfileCLI.cpp",),
includes=(ROOT / "test/fixtures/radio_profiles/mocks", ROOT / "test/mocks"),
definitions=("MESH_ENABLE_MESHTEXT_CHAT=0",
"MESH_ENABLE_TX_BEACONS=0", "MESH_ENABLE_ETHERNET_TUNNEL=1"),
)
def test_full_esp32_wifi_only_compile_gate(self):
harness = ("#include <helpers/EthernetTunnelConfig.h>\n"
"static_assert(MESH_ENABLE_ETHERNET_TUNNEL==EXPECTED,\"feature gate\");\n"
"int main() {}\n")
for definitions, expected in (
(("ESP32_PLATFORM=1", "COMPANION_RADIO_FULL=1", "WIFI_SSID=1"), 1),
(("ESP32_PLATFORM=1", "COMPANION_RADIO_FULL=1"), 0),
(("ESP32_PLATFORM=1", "WIFI_SSID=1"), 0),
(("NRF52_PLATFORM=1", "COMPANION_RADIO_FULL=1", "WIFI_SSID=1"), 0),
(("ESP32_PLATFORM=1", "COMPANION_RADIO_FULL=1", "WIFI_SSID=1", "MESH_PRIMARY_ESPNOW=1"), 0),
):
with self.subTest(definitions=definitions):
self.compile_run(harness, definitions=(*definitions, f"EXPECTED={expected}"))
def test_production_companion_wiring_keeps_transports_isolated(self):
source = (ROOT / "examples/companion_radio/MyMesh.cpp").read_text(encoding="utf-8")
# begin() contains old alternative platform branches that intentionally
# split a common outer body; take its stable next-method boundary.
begin = source[source.index("void MyMesh::begin("):source.index("bool MyMesh::handleTxRoutingCommand(")]
self.assertLess(begin.index("beginEthernetTunnel();"), begin.index("beginMeshtext();"))
loop = body(source, "void MyMesh::loop()")
self.assertLess(loop.index("loopEthernetTunnel();"), loop.index("BaseChatMesh::loop();"))
self.assertIn("handleEthernetTunnelCommand(command, reply, reply_size)",
body(source, "bool MyMesh::handleLocalControlCommand("))
self.assertEqual(source.count("if (!eraseEthernetSettings())"), 1)
self.assertEqual(source.count("if (success && !eraseEthernetSettings())"), 1)
setting = body(source, "bool MyMesh::setChannel(")
self.assertIn("_ethernet_settings.enabled", setting)
self.assertIn("CompanionChannelKind::MeshCore", setting)
mt = (ROOT / "examples/companion_radio/MeshtextCompanion.cpp").read_text(encoding="utf-8")
refresh = body(mt, "void MyMesh::refreshMeshtextProfile()")
self.assertLess(refresh.index("if (_ethernet_settings.enabled)"),
refresh.index("_radio_profiles.clearAutoSecondary();"))
self.assertIn("onEthernetFrame(bytes, length, profile, generation)",
body(mt, "bool MyMesh::onForeignRadioFrame("))
self.assertIn("queuedForeignRadioProtocol() == mesh::RadioProfileProtocol::Ethernet",
body(mt, "void MyMesh::onForeignRadioSendComplete("))
if __name__ == "__main__":
unittest.main()