456 lines
21 KiB
Python
456 lines
21 KiB
Python
"""Execute Companion debug persistence/CLI and bounded packet logging paths."""
|
|
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 ConfigText : Stream {
|
|
std::string text;size_t offset=0;
|
|
int available() override{return int(text.size()-offset);}
|
|
int read() override{return offset<text.size()?uint8_t(text[offset++]):-1;}
|
|
int peek() override{return offset<text.size()?uint8_t(text[offset]):-1;}
|
|
size_t write(uint8_t value) override{text.push_back(char(value));return 1;}
|
|
size_t write(const uint8_t* value,size_t size) override{
|
|
text.append(reinterpret_cast<const char*>(value),size);return size;
|
|
}
|
|
size_t print(unsigned int value,int base) override{
|
|
assert(base==10);const auto number=std::to_string(value);
|
|
return write(reinterpret_cast<const uint8_t*>(number.data()),number.size());
|
|
}
|
|
};
|
|
namespace mesh {
|
|
bool logging_enabled=true, debug_enabled=false;
|
|
unsigned debug_applies=0;
|
|
bool isUsbLoggingEnabled(){return logging_enabled;}
|
|
bool isUsbDebugEnabled(){return debug_enabled;}
|
|
void setUsbDebugEnabled(bool enabled){debug_enabled=enabled;++debug_applies;}
|
|
}
|
|
struct {double node_lat=47.1,node_lon=-122.2;} sensors;
|
|
struct MyMesh {
|
|
CompanionNodePrefs _prefs;
|
|
DataStore store;
|
|
DataStore* _store=&store;
|
|
@SAVE_PREFS@
|
|
@SAVE_PREFERENCE@
|
|
bool command(const char* command,char* reply,size_t reply_size){
|
|
@DEBUG_COMMANDS@
|
|
return false;
|
|
}
|
|
};
|
|
int main(){
|
|
MyMesh node;
|
|
assert(node._prefs.usb_debug_enabled==0);
|
|
strcpy(node._prefs.node_name,"durable node");node._prefs.freq=910.525f;
|
|
node._prefs.ble_pin=876543;node._prefs.bluetooth_enabled=0;
|
|
node._prefs.one_key_dm_enabled=1;node._prefs.usb_logging_enabled=1;
|
|
node._prefs.gps_sync_interval_hours=257;
|
|
assert(node.savePrefs());
|
|
const auto original=node.store.fs.files["/new_prefs"];
|
|
#if defined(TBEAM_1W)
|
|
assert(original.size()==242);
|
|
#elif defined(RP2040_PLATFORM) && defined(ENABLE_WIFI_INTERFACE)
|
|
assert(original.size()==332);
|
|
#else
|
|
assert(original.size()==235);
|
|
#endif
|
|
assert(original.back()==0&&original[original.size()-4]==0);
|
|
assert(original[original.size()-3]==1&&original[original.size()-2]==1);
|
|
assert(original[original.size()-5]==1&&original[158]==1);
|
|
char reply[160]={};
|
|
for(const char* malformed : {"set usb.debug","set usb.debug ",
|
|
"set usb.debug 1","set usb.debug ON","set usb.debug on ",
|
|
"set usb.debug on reboot","set usb.debug off extra", "set usb.debug off\n"}){
|
|
const auto before=node.store.fs.files["/new_prefs"];
|
|
const unsigned applied=mesh::debug_applies;
|
|
assert(node.command(malformed,reply,sizeof(reply)));
|
|
assert(!strcmp(reply,"Error: use set usb.debug <on|off>"));
|
|
assert(node._prefs.usb_debug_enabled==0&&!mesh::debug_enabled);
|
|
assert(mesh::debug_applies==applied&&node.store.fs.files["/new_prefs"]==before);
|
|
}
|
|
for(const char* unrelated : {"set usb.debugger on","get usb.debugx","get usb.debug on"})
|
|
assert(!node.command(unrelated,reply,sizeof(reply)));
|
|
assert(node.command("set usb.debug\t on",reply,sizeof(reply)));
|
|
assert(!strncmp(reply,"OK - USB debug on (saved)",24));
|
|
assert(node._prefs.usb_debug_enabled==1&&mesh::debug_enabled);
|
|
const auto enabled_disk=node.store.fs.files["/new_prefs"];
|
|
assert(enabled_disk.back()==1);
|
|
for(size_t i=0;i+1<original.size();++i)assert(enabled_disk[i]==original[i]);
|
|
assert(node.command("get usb.debug",reply,sizeof(reply)));
|
|
#if MESH_DEBUG
|
|
assert(!strcmp(reply,"usb.debug on"));
|
|
#else
|
|
assert(!strcmp(reply,"usb.debug on (core debug not compiled)"));
|
|
#endif
|
|
mesh::logging_enabled=false;
|
|
assert(node.command("get usb.debug",reply,sizeof(reply)));
|
|
#if MESH_DEBUG
|
|
assert(!strcmp(reply,"usb.debug on (USB logging off)"));
|
|
#endif
|
|
// Faults at early and newly appended bytes both preserve the committed image
|
|
// and requested runtime state. A subsequent successful save applies once.
|
|
for(int fail_after : {17,int(enabled_disk.size()-1)}){
|
|
node.store.fs.fail_write_after=fail_after;
|
|
const unsigned applied=mesh::debug_applies;
|
|
assert(node.command("set usb.debug off",reply,sizeof(reply)));
|
|
assert(!strcmp(reply,"Error: USB debug setting could not be saved"));
|
|
assert(node._prefs.usb_debug_enabled==1&&mesh::debug_enabled);
|
|
assert(mesh::debug_applies==applied);
|
|
assert(node.store.fs.files["/new_prefs"]==enabled_disk);
|
|
}
|
|
node.store.fs.fail_write_after=-1;
|
|
assert(node.command("set usb.debug off",reply,sizeof(reply)));
|
|
assert(node._prefs.usb_debug_enabled==0&&!mesh::debug_enabled);
|
|
for(uint8_t enabled : {0,1}){
|
|
node._prefs.usb_debug_enabled=enabled;assert(node.savePrefs());
|
|
assert(mesh::debug_enabled==(enabled!=0));
|
|
DataStore reboot;reboot.fs=node.store.fs;
|
|
CompanionNodePrefs loaded;loaded.usb_debug_enabled=1-enabled;
|
|
double lat=0,lon=0;assert(reboot.loadPrefs(loaded,lat,lon));
|
|
assert(loaded.usb_debug_enabled==enabled&&loaded.bluetooth_enabled==0);
|
|
assert(loaded.one_key_dm_enabled==1&&loaded.usb_logging_enabled==1);
|
|
assert(loaded.gps_sync_interval_hours==257);
|
|
assert(loaded.ble_pin==876543&&loaded.freq==910.525f);
|
|
}
|
|
// Existing complete tails retain their established offsets and always start
|
|
// quiet, even if the object was already using verbose runtime diagnostics.
|
|
for(unsigned size : {84u,156u,215u,226u,unsigned(original.size()-1)}){
|
|
DataStore legacy;legacy.fs.files["/new_prefs"]=enabled_disk;
|
|
legacy.fs.files["/new_prefs"].resize(size);
|
|
CompanionNodePrefs loaded;loaded.usb_debug_enabled=1;
|
|
double lat=0,lon=0;assert(legacy.loadPrefs(loaded,lat,lon));
|
|
assert(loaded.usb_debug_enabled==0&&loaded.ble_pin==876543);
|
|
assert(legacy.fs.files["/new_prefs"].size()==size);
|
|
}
|
|
// Normalize the new boolean only after the entire persisted image validates.
|
|
for(uint8_t byte : {2,255}){
|
|
DataStore corrupt;corrupt.fs.files["/new_prefs"]=enabled_disk;
|
|
corrupt.fs.files["/new_prefs"].back()=byte;
|
|
CompanionNodePrefs loaded;double lat=0,lon=0;
|
|
assert(corrupt.loadPrefsInt("/new_prefs",loaded,lat,lon));
|
|
assert(loaded.usb_debug_enabled==0);
|
|
}
|
|
DataStore extra;extra.fs.files["/new_prefs"]=enabled_disk;
|
|
extra.fs.files["/new_prefs"].push_back(0);
|
|
CompanionNodePrefs unchanged;unchanged.usb_debug_enabled=0;
|
|
double lat=1,lon=2;assert(!extra.loadPrefsInt("/new_prefs",unchanged,lat,lon));
|
|
assert(unchanged.usb_debug_enabled==0&&lat==1&&lon==2);
|
|
// Numeric dynamic configuration uses the actual preference, not a shadow
|
|
// string in the custom dictionary, and applies through the same save path.
|
|
char dynamic[8]={};
|
|
assert(node._prefs.getCustom()->setByKey("usb_debug","1"));
|
|
assert(node._prefs.usb_debug_enabled==1&&node._prefs.isDirty());
|
|
assert(node._prefs.getCustom()->getByKey("usb_debug",dynamic,sizeof(dynamic)));
|
|
assert(!strcmp(dynamic,"1"));
|
|
assert(node.savePrefs()&&mesh::debug_enabled&&!node._prefs.isDirty());
|
|
assert(node._prefs.getCustom()->setByKey("usb_debug","0"));
|
|
assert(node.savePrefs()&&!mesh::debug_enabled);
|
|
// The radio serializer exports the preference; custom JSON imports delegate
|
|
// to the same field rather than creating an unrelated string preference.
|
|
ConfigText imported;imported.text="{usb_debug:1}";
|
|
assert(node._prefs.getCustom()->getByKey("usb_debug",dynamic,sizeof(dynamic)));
|
|
auto* serializer=static_cast<DynamicConfigSerializer*>(node._prefs.getCustom());
|
|
assert(serializer->loadSerial(imported)&&node._prefs.usb_debug_enabled==1);
|
|
assert(node.savePrefs()&&mesh::debug_enabled);
|
|
ConfigText exported;assert(node._prefs.getRadioPrefs()->saveSerial(exported));
|
|
assert(exported.text.find("usb_debug:1")!=std::string::npos);
|
|
// Only an explicit 1 enables verbosity, including when a dynamic schema has
|
|
// assigned an out-of-range byte. Failed commits still leave runtime alone.
|
|
node._prefs.usb_debug_enabled=255;assert(node.savePrefs());
|
|
assert(node._prefs.usb_debug_enabled==0&&!mesh::debug_enabled);
|
|
node._prefs.usb_debug_enabled=255;node.store.fs.fail_write=true;
|
|
assert(!node.savePrefs()&&node._prefs.usb_debug_enabled==255&&!mesh::debug_enabled);
|
|
node.store.fs.fail_write=false;node._prefs.usb_debug_enabled=0;
|
|
// Bounded replies do not write beyond their supplied capacity.
|
|
char small[9];memset(small,'X',sizeof(small));
|
|
assert(node.command("get usb.debug",small,8));assert(small[7]==0&&small[8]=='X');
|
|
}
|
|
'''
|
|
|
|
PACKET_HARNESS = r'''
|
|
#include <cassert>
|
|
#include <cstring>
|
|
#include <string>
|
|
#include <vector>
|
|
#include <ctime>
|
|
#include <helpers/SerialPacketLog.h>
|
|
#include <helpers/BaseSerialInterface.h>
|
|
std::vector<std::string> events;
|
|
struct Port : Stream {
|
|
std::string output;
|
|
bool stalled=false;
|
|
int availableForWrite() override{return stalled?0:4096;}
|
|
size_t write(const uint8_t* bytes,size_t size) override{
|
|
events.push_back("ascii");output.append(reinterpret_cast<const char*>(bytes),size);return size;
|
|
}
|
|
} log_port;
|
|
namespace mesh {
|
|
bool enabled=true,debug_enabled=false;
|
|
bool isUsbLoggingEnabled(){return enabled;}
|
|
Port& usbLoggingPort(){return log_port;}
|
|
}
|
|
struct Transport {
|
|
bool connected=true;
|
|
std::vector<std::vector<uint8_t>> frames;
|
|
bool isConnected() const{return connected;}
|
|
size_t writeFrame(const uint8_t* bytes,size_t size){
|
|
events.push_back("binary");frames.emplace_back(bytes,bytes+size);return size;
|
|
}
|
|
};
|
|
struct Bridge {
|
|
bool running=true;
|
|
std::vector<std::vector<uint8_t>> raw;
|
|
float snr=0,rssi=0;
|
|
bool isRunning() const{return running;}
|
|
void storeRawRadioData(const uint8_t* bytes,int len,float s,float r){
|
|
events.push_back("mqtt");raw.emplace_back(bytes,bytes+len);snr=s;rssi=r;
|
|
}
|
|
};
|
|
@PUSH_CODE@
|
|
struct Clock {
|
|
uint32_t timestamp=1791030896;
|
|
uint32_t getCurrentTime() const{return timestamp;}
|
|
};
|
|
class DateTime {
|
|
std::tm value;
|
|
public:
|
|
explicit DateTime(uint32_t timestamp){const std::time_t time=timestamp;value=*std::gmtime(&time);}
|
|
unsigned hour() const{return value.tm_hour;}
|
|
unsigned minute() const{return value.tm_min;}
|
|
unsigned second() const{return value.tm_sec;}
|
|
unsigned day() const{return value.tm_mday;}
|
|
unsigned month() const{return value.tm_mon+1;}
|
|
unsigned year() const{return value.tm_year+1900;}
|
|
};
|
|
struct MyMesh {
|
|
Transport transport;Transport* _serial=&transport;
|
|
Bridge bridge;Bridge* _mqtt_bridge=&bridge;
|
|
uint8_t out_frame[MAX_FRAME_SIZE]={};
|
|
Clock clock;Clock* getRTCClock(){return &clock;}
|
|
#if MESH_PACKET_LOGGING && !MESH_PACKET_LOGGING_COMPACT
|
|
const char* getLogDateTime();
|
|
#else
|
|
const char* getLogDateTime(){return "";}
|
|
#endif
|
|
void logRxRaw(float,float,const uint8_t*,int);
|
|
};
|
|
@LOG_TIMESTAMP@
|
|
@LOG_RX_RAW@
|
|
void reset(){
|
|
events.clear();log_port.output.clear();log_port.stalled=false;
|
|
mesh::serialLogDroppedCount()=0;mesh::serialLogPortSeen()=false;
|
|
mesh::enabled=true;mesh::debug_enabled=false;g_mock_millis=0;
|
|
}
|
|
int main(){
|
|
const uint8_t raw[]={0x00,0xab,0xff};
|
|
MyMesh node;reset();node.logRxRaw(2.5f,-92.0f,raw,sizeof(raw));
|
|
#if MESH_PACKET_LOGGING
|
|
#if MESH_PACKET_LOGGING_COMPACT
|
|
assert(log_port.output=="R00ABFF\n");
|
|
#else
|
|
assert(log_port.output=="12:34:56 - 3/10/2026 U RAW: 00ABFF\r\n");
|
|
// A second invocation uses the current RTC and is the formatter dispatched
|
|
// by the shared RX/TX summary path as well as this RAW path.
|
|
assert(!strcmp(node.getLogDateTime(),"12:34:56 - 3/10/2026 U"));
|
|
node.clock.timestamp+=1;
|
|
assert(!strcmp(node.getLogDateTime(),"12:34:57 - 3/10/2026 U"));
|
|
node.clock.timestamp-=1;
|
|
#endif
|
|
assert(events.front()=="ascii"); // RAW precedes the unchanged downstream sinks.
|
|
#else
|
|
assert(log_port.output.empty());
|
|
#endif
|
|
assert(node.transport.frames.size()==1);
|
|
const std::vector<uint8_t> frame={PUSH_CODE_LOG_RX_DATA,10,uint8_t(-92),0x00,0xab,0xff};
|
|
assert(node.transport.frames[0]==frame);
|
|
#if defined(WITH_MQTT_BRIDGE) && defined(ESP32_PLATFORM) && defined(WIFI_SSID)
|
|
assert(node.bridge.raw.size()==1&&node.bridge.raw[0]==std::vector<uint8_t>(raw,raw+sizeof(raw)));
|
|
assert(node.bridge.snr==2.5f&&node.bridge.rssi==-92.0f);
|
|
assert(events[events.size()-2]=="mqtt");
|
|
#endif
|
|
reset();mesh::enabled=false;node.logRxRaw(2.5f,-92.0f,raw,sizeof(raw));
|
|
assert(log_port.output.empty()&&node.transport.frames.size()==2);
|
|
reset();node.transport.connected=false;node.logRxRaw(2.5f,-92.0f,raw,sizeof(raw));
|
|
assert(node.transport.frames.size()==2); // Disconnected binary session receives no push.
|
|
#if MESH_PACKET_LOGGING
|
|
assert(!log_port.output.empty()); // Independent diagnostics port still receives RAW.
|
|
#endif
|
|
reset();uint8_t maximum[255];memset(maximum,0xa5,sizeof(maximum));
|
|
node.logRxRaw(0,-100,maximum,sizeof(maximum));
|
|
assert(node.transport.frames.size()==2); // Never overflow the 176-byte frame buffer.
|
|
#if MESH_PACKET_LOGGING
|
|
assert(log_port.output.size()<=SERIAL_LOG_LINE_MAX);
|
|
std::string expected_hex;for(unsigned i=0;i<255;++i)expected_hex+="A5";
|
|
#if MESH_PACKET_LOGGING_COMPACT
|
|
assert(log_port.output=="R"+expected_hex+"\n");
|
|
#else
|
|
assert(log_port.output=="12:34:56 - 3/10/2026 U RAW: "+expected_hex+"\r\n");
|
|
#endif
|
|
reset();log_port.stalled=true;node.transport.connected=true;
|
|
const uint32_t before=millis();node.logRxRaw(2.5f,-92.0f,raw,sizeof(raw));
|
|
assert(log_port.output.empty()&&millis()==before);
|
|
assert(node.transport.frames.size()==3); // A stalled host cannot block other sinks.
|
|
#endif
|
|
}
|
|
'''
|
|
|
|
|
|
class CompanionUsbLoggingTests(unittest.TestCase):
|
|
def compile_and_run(self, work, text, flags, extra=()):
|
|
source = work / 'test.cpp'
|
|
source.write_text(text)
|
|
binary = work / 'test'
|
|
built = subprocess.run([os.environ.get('CXX', 'g++'), '-std=c++17',
|
|
'-fsanitize=address,undefined', '-fno-sanitize-recover=all',
|
|
'-fno-pie', '-no-pie', *['-D' + flag for flag in 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(source),
|
|
*[str(ROOT / 'src/helpers' / name) for name in extra],
|
|
'-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 prepare(self, work):
|
|
(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>'))
|
|
|
|
def test_real_preference_io_cli_and_saved_runtime_gate(self):
|
|
store = (ROOT / 'examples/companion_radio/DataStore.cpp').read_text()
|
|
source = (ROOT / 'examples/companion_radio/MyMesh.cpp').read_text()
|
|
header = (ROOT / 'examples/companion_radio/MyMesh.h').read_text()
|
|
store_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@', store_methods)
|
|
start = source.index(' if (strcmp(command, "get usb.debug") == 0)')
|
|
end = source.index(' if (strcmp(command, "get usb.logging") == 0)', start)
|
|
harness = PREFERENCE_HARNESS.replace('@DEBUG_COMMANDS@', source[start:end])
|
|
harness = harness.replace('@SAVE_PREFS@', method(header, ' bool savePrefs()'))
|
|
harness = harness.replace('@SAVE_PREFERENCE@', method(
|
|
header, 'template <typename T> bool savePreference('))
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
work = Path(directory)
|
|
self.prepare(work)
|
|
for platform in ('NRF52_PLATFORM', 'STM32_PLATFORM', 'ESP32_PLATFORM',
|
|
'RP2040_PLATFORM', 'ESP32_PLATFORM,TBEAM_1W',
|
|
'RP2040_PLATFORM,ENABLE_WIFI_INTERFACE'):
|
|
for debug in (0, 1):
|
|
with self.subTest(platform=platform, debug=debug):
|
|
self.compile_and_run(work, prefix + harness,
|
|
[flag + '=1' for flag in platform.split(',')] +
|
|
['MESH_USB_LOGGING_AVAILABLE=1', 'MESH_DEBUG=' + str(debug)],
|
|
('ConfigSerializer.cpp', 'DynamicConfigSerializer.cpp',
|
|
'CommonRadioPrefs.cpp', 'TxtDataHelpers.cpp'))
|
|
|
|
def test_real_raw_emitter_master_gate_compact_and_other_transports(self):
|
|
source = (ROOT / 'examples/companion_radio/MyMesh.cpp').read_text()
|
|
code = next(line for line in source.splitlines()
|
|
if line.startswith('#define PUSH_CODE_LOG_RX_DATA'))
|
|
harness = PACKET_HARNESS.replace('@PUSH_CODE@', code)
|
|
harness = harness.replace('@LOG_TIMESTAMP@',
|
|
'#if MESH_PACKET_LOGGING && !MESH_PACKET_LOGGING_COMPACT\n' +
|
|
method(source, 'const char* MyMesh::getLogDateTime()') + '\n#endif\n')
|
|
harness = harness.replace('@LOG_RX_RAW@', method(source, 'void MyMesh::logRxRaw('))
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
work = Path(directory)
|
|
self.prepare(work)
|
|
for packets, compact, mqtt in ((0, 0, 0), (0, 0, 1),
|
|
(1, 0, 0), (1, 0, 1), (1, 1, 0), (1, 1, 1)):
|
|
with self.subTest(packets=packets, compact=compact, mqtt=mqtt):
|
|
flags = ['MESH_PACKET_LOGGING=' + str(packets),
|
|
'MESH_PACKET_LOGGING_COMPACT=' + str(compact)]
|
|
if mqtt:
|
|
flags += ['WITH_MQTT_BRIDGE=1', 'ESP32_PLATFORM=1', 'WIFI_SSID="test"']
|
|
self.compile_and_run(work, harness, flags)
|
|
|
|
def test_begin_defaults_clamps_and_applies_debug_before_packet_master(self):
|
|
source = (ROOT / 'examples/companion_radio/MyMesh.cpp').read_text()
|
|
self.assertIn('_prefs.usb_debug_enabled = 0;', source)
|
|
self.assertIn('_prefs.usb_debug_enabled = _prefs.usb_debug_enabled == 1 ? 1 : 0;', source)
|
|
apply = source.index('mesh::setUsbDebugEnabled(_prefs.usb_debug_enabled != 0);')
|
|
self.assertLess(apply, source.index('applyUsbLoggingState(usb_logging_enabled);'))
|
|
header = (ROOT / 'examples/companion_radio/MyMesh.h').read_text()
|
|
self.assertIn('const char* getLogDateTime() override;', header)
|
|
|
|
def test_actual_help_chunks_keep_controls_and_bounded_native_writes(self):
|
|
source = (ROOT / 'examples/companion_radio/MyMesh.cpp').read_text()
|
|
start = source.index(' } else if (strcmp(command, "help") == 0) {')
|
|
start = source.index('{', start) + 1
|
|
end = source.index(' } else if (handleCommand(command, 0, local_reply))', start)
|
|
harness = r'''
|
|
#include <cassert>
|
|
#include <cstring>
|
|
#include <string>
|
|
struct Port {
|
|
std::string output;
|
|
void print(const char* text){assert(strlen(text)<=256);output+=text;}
|
|
void println(const char* text){print(text);output+="\r\n";}
|
|
};
|
|
struct MyMesh {
|
|
Port port;bool _terminal_mode=true;
|
|
Port& terminalOutput(){return port;}
|
|
void help(){ @HELP@ }
|
|
};
|
|
int main(){
|
|
MyMesh node;
|
|
for(bool terminal : {false,true}){
|
|
node._terminal_mode=terminal;node.port.output.clear();node.help();
|
|
const auto& text=node.port.output;
|
|
assert(text.find("Commands:\r\n stats-core / stats-radio / stats-radio-diag / stats-packets\r\n")==0);
|
|
assert(text.find(" get public.key\r\n")!=std::string::npos);
|
|
assert(text.find(" get usb.debug\r\n set usb.debug <on|off>\r\n")!=std::string::npos);
|
|
#if MESH_USB_CONSOLE_COOPERATIVE
|
|
assert(text.find(" get usb.watchdog\r\n get usb.watchdog.last\r\n set usb.watchdog <off|on|auto> (saved; default auto)\r\n")!=std::string::npos);
|
|
#else
|
|
assert(text.find("usb.watchdog")==std::string::npos);
|
|
#endif
|
|
assert(text.find(" reboot\r\n ver\r\n")!=std::string::npos);
|
|
assert(text.find(" +++MESHCORE-TERM-STOP\r\n")!=std::string::npos ? terminal : !terminal);
|
|
assert(text.find(" disconnect (closes the network terminal)\r\n")!=std::string::npos ? !terminal : terminal);
|
|
}
|
|
}
|
|
'''.replace('@HELP@', source[start:end])
|
|
features = ['MESH_USB_LOGGING_AVAILABLE', 'COMPANION_FEATURE_READER',
|
|
'COMPANION_FEATURE_MEMORY_DIAGNOSTICS', 'COMPANION_FEATURE_NOTIFICATIONS',
|
|
'COMPANION_RADIO_FULL', 'COMPANION_EXCLUSIVE_WIFI_BLE',
|
|
'WITH_WEBCONFIG', 'MESH_PRIMARY_ESPNOW', 'COMPANION_FEATURE_TEMP_RADIO',
|
|
'COMPANION_FEATURE_OTA_CLI', 'BLE_PIN_CODE', 'ESP32', 'NRF52_PLATFORM',
|
|
'EXTRAFS']
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
work = Path(directory)
|
|
self.prepare(work)
|
|
for native in (0, 1):
|
|
with self.subTest(native=native):
|
|
self.compile_and_run(work, harness,
|
|
[flag + '=1' for flag in features] + ['WIFI_SSID="test"',
|
|
'MESH_USB_CONSOLE_COOPERATIVE=' + str(native)])
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|