Files
MeshCore/test/test_companion_usb_logging.py
mikecarper 3e8c637866 Add Companion lost-status replies and repair room mailbox test adapters
Save an opt-in off/no/yes answer to private Am I lost questions from known
chat contacts. Keep reply traffic and ACK state bounded, preserve pending
user delivery, and expose the setting through local CLI and ESP32 WebConfig.

Retain compact STM32 application/filesystem boundaries, qualify the Tag BLE
queue allowance, and exercise saved-image compatibility and browser controls.
Fix stale room mailbox protocol dependencies in the ACL/route test fixtures.

Validation: ten Companion builds, sanitized production-path regressions,
preference fault/compatibility tests, browser tests, and 86 capacity/build
recipe checks passed. No live radio was flashed for this feature.
2026-10-10 19:24:42 -07:00

466 lines
22 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()==243);
#elif defined(RP2040_PLATFORM) && defined(ENABLE_WIFI_INTERFACE)
assert(original.size()==333);
#else
assert(original.size()==236);
#endif
const size_t debug_offset=original.size()-2;
assert(original[debug_offset]==0&&original[debug_offset-3]==0);
assert(original[debug_offset-2]==1&&original[debug_offset-1]==1);
assert(original[debug_offset-4]==1&&original[158]==1);
assert(original.back()==0);
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[debug_offset]==1);
for(size_t i=0;i<original.size();++i)
if(i!=debug_offset)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(debug_offset)}){
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"][debug_offset]=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);
#if MESH_ENABLE_LOST_REPLY
assert(text.find(" get lost.reply\r\n set lost.reply <off|no|yes>\r\n")!=std::string::npos);
#else
assert(text.find("lost.reply")==std::string::npos);
#endif
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):
for lost in (0, 1):
with self.subTest(native=native, lost=lost):
self.compile_and_run(work, harness,
[flag + '=1' for flag in features] + ['WIFI_SSID="test"',
'MESH_USB_CONSOLE_COOPERATIVE=' + str(native),
'MESH_ENABLE_LOST_REPLY=' + str(lost)])
if __name__ == '__main__':
unittest.main()