323 lines
14 KiB
Python
323 lines
14 KiB
Python
"""Execute the actual role ownership vetoes for USB watchdog recovery."""
|
|
from itertools import product
|
|
from pathlib import Path
|
|
import os
|
|
import subprocess
|
|
import tempfile
|
|
import unittest
|
|
|
|
from test_radio_receive_contract import method
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
COMMON_HARNESS = r'''
|
|
#include <cassert>
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <initializer_list>
|
|
#include <helpers/CompanionDelayedReplies.h>
|
|
struct Board {
|
|
bool updating=false, testing=false;
|
|
bool isOTAUpdateRunning() const{return updating;}
|
|
bool isRadioTestActive() const{return testing;}
|
|
} board;
|
|
struct RadioDriver {
|
|
bool observing=false, calibrating=false;
|
|
bool isWatchdogObserving() const{return observing;}
|
|
bool isCalibratingNoiseFloor() const{return calibrating;}
|
|
} radio_driver;
|
|
namespace mesh {
|
|
struct Status {bool reader_connected=true, stalled=false;};
|
|
static Status status;
|
|
Status usbLoggingStatus(){return status;}
|
|
}
|
|
struct Store {
|
|
bool dirty=false;
|
|
bool hasPendingContactWrites() const{return dirty;}
|
|
};
|
|
struct SerialInterface {
|
|
bool pending=false, connected=false, passthrough=true;
|
|
bool hasPendingIO() const{return pending;}
|
|
bool isConnected() const{return connected;}
|
|
bool isReplyRouteAvailable(const SerialInterface* route) const {
|
|
return route==this&&connected;
|
|
}
|
|
bool isPassthroughMode() const{return passthrough;}
|
|
};
|
|
struct RoleMesh {
|
|
uint32_t dirty_contacts_expiry=0, _ota_update_at=0, set_radio_at=0,
|
|
_scheduled_reboot_at=0;
|
|
bool outbound=false, temporary=false, ota_apply=false,
|
|
saved_radio_apply_pending=false, temp_radio_handoff_pending=false,
|
|
command_radio_apply_pending=false,
|
|
_iter_started=false, _terminal_trace_pending=false, terminal=true;
|
|
mesh::CompanionDelayedReplies _delayed_replies;
|
|
void* sign_data=nullptr;
|
|
Store store;
|
|
Store* _store=&store;
|
|
SerialInterface serial;
|
|
SerialInterface* _serial=&serial;
|
|
SerialInterface* _iter_reply_route=&serial;
|
|
bool hasOutbound() const{return outbound;}
|
|
bool isAnyTempRadioActive() const{return temporary;}
|
|
bool hasPendingOtaApply() const{return ota_apply;}
|
|
bool hasPendingReqs() const{return _delayed_replies.hasRequest();}
|
|
bool isTerminalMode() const{return terminal;}
|
|
#if MESH_USB_CONSOLE_COOPERATIVE
|
|
bool functional_output=false;
|
|
bool hasPendingSerialOutput() const{return functional_output;}
|
|
#endif
|
|
@ACCESSOR_DECLARATION@
|
|
} the_mesh;
|
|
@ACCESSOR_DEFINITION@
|
|
static char command[32]={};
|
|
static bool command_overflow=false;
|
|
#if defined(ENABLE_USB_INTERFACE)
|
|
static SerialInterface usb_serial_interface;
|
|
static bool usb_connected=false;
|
|
static bool usb_mota_mode=false;
|
|
static unsigned usb_terminal_line_len=0;
|
|
static bool usb_terminal_discard_line=false;
|
|
bool isUsbTerminalDataConnected(){return usb_connected;}
|
|
#endif
|
|
@CALLBACK@
|
|
|
|
void reset(){
|
|
board=Board{};radio_driver=RadioDriver{};mesh::status=mesh::Status{};
|
|
the_mesh=RoleMesh{};
|
|
// Assignment copies the fixture's dependency pointers; point them back at
|
|
// the live fixture objects, not the temporary used to reset scalar state.
|
|
the_mesh._store=&the_mesh.store;the_mesh._serial=&the_mesh.serial;
|
|
the_mesh._iter_reply_route=&the_mesh.serial;
|
|
command[0]=0;command_overflow=false;
|
|
#if defined(ENABLE_USB_INTERFACE)
|
|
usb_serial_interface=SerialInterface{};usb_connected=false;
|
|
usb_mota_mode=false;usb_terminal_line_len=0;usb_terminal_discard_line=false;
|
|
#endif
|
|
}
|
|
|
|
int main(){
|
|
reset();assert(the_mesh.canRecoverUsbLogging());
|
|
assert(usbLoggingRecoverySafe(nullptr));
|
|
// Each protection must survive a physically absent/stalled reader. That
|
|
// exception is only for stale ASCII work, never radio, flash or protocol.
|
|
for(unsigned state=0;state<3;++state){
|
|
reset();mesh::status.reader_connected=state!=1;
|
|
mesh::status.stalled=state==2;
|
|
board.updating=true;assert(!usbLoggingRecoverySafe(nullptr));board.updating=false;
|
|
board.testing=true;assert(!usbLoggingRecoverySafe(nullptr));board.testing=false;
|
|
radio_driver.observing=true;assert(!usbLoggingRecoverySafe(nullptr));
|
|
radio_driver.observing=false;
|
|
radio_driver.calibrating=true;assert(!usbLoggingRecoverySafe(nullptr));
|
|
radio_driver.calibrating=false;
|
|
for(auto member : {&RoleMesh::outbound,&RoleMesh::temporary,&RoleMesh::ota_apply,
|
|
&RoleMesh::saved_radio_apply_pending}){
|
|
the_mesh.*member=true;
|
|
assert(!the_mesh.canRecoverUsbLogging());
|
|
assert(!usbLoggingRecoverySafe(nullptr));
|
|
the_mesh.*member=false;
|
|
}
|
|
the_mesh.dirty_contacts_expiry=1;
|
|
assert(!the_mesh.canRecoverUsbLogging());assert(!usbLoggingRecoverySafe(nullptr));
|
|
the_mesh.dirty_contacts_expiry=0;
|
|
@ROLE_VETOES@
|
|
}
|
|
@COMMAND_VETOES@
|
|
@COMPANION_VETOES@
|
|
reset();assert(usbLoggingRecoverySafe(nullptr));
|
|
}
|
|
'''
|
|
|
|
|
|
class UsbLoggingWatchdogRoleTests(unittest.TestCase):
|
|
def test_actual_ordinary_role_callbacks_with_and_without_native_console(self):
|
|
for role, header, specific in (
|
|
('simple_repeater', 'MyMesh.h', '''
|
|
the_mesh.temp_radio_handoff_pending=true;
|
|
assert(!usbLoggingRecoverySafe(nullptr));
|
|
the_mesh.temp_radio_handoff_pending=false;
|
|
the_mesh._ota_update_at=1;assert(!usbLoggingRecoverySafe(nullptr));
|
|
the_mesh._ota_update_at=0;
|
|
'''),
|
|
('simple_room_server', 'MyMesh.h', '''
|
|
the_mesh.set_radio_at=1;assert(!usbLoggingRecoverySafe(nullptr));
|
|
the_mesh.set_radio_at=0;
|
|
the_mesh._ota_update_at=1;assert(!usbLoggingRecoverySafe(nullptr));
|
|
the_mesh._ota_update_at=0;
|
|
'''),
|
|
('simple_sensor', 'SensorMesh.h', '''
|
|
the_mesh.set_radio_at=1;assert(!usbLoggingRecoverySafe(nullptr));
|
|
the_mesh.set_radio_at=0;
|
|
''')):
|
|
directory = ROOT / 'examples' / role
|
|
accessor = method((directory / header).read_text(),
|
|
'bool canRecoverUsbLogging() const')
|
|
callback = method((directory / 'main.cpp').read_text(),
|
|
'static bool usbLoggingRecoverySafe(void*)')
|
|
command_checks = '''
|
|
reset();command[0]='x';assert(!usbLoggingRecoverySafe(nullptr));
|
|
mesh::status.reader_connected=false;assert(usbLoggingRecoverySafe(nullptr));
|
|
mesh::status.reader_connected=true;mesh::status.stalled=true;
|
|
assert(usbLoggingRecoverySafe(nullptr));
|
|
reset();
|
|
'''
|
|
if role == 'simple_repeater':
|
|
command_checks += '''
|
|
command_overflow=true;assert(!usbLoggingRecoverySafe(nullptr));
|
|
mesh::status.reader_connected=false;assert(usbLoggingRecoverySafe(nullptr));
|
|
reset();
|
|
'''
|
|
if role != 'simple_sensor':
|
|
command_checks += '''
|
|
#if MESH_USB_CONSOLE_COOPERATIVE
|
|
the_mesh.functional_output=true;assert(!usbLoggingRecoverySafe(nullptr));
|
|
mesh::status.reader_connected=false;assert(usbLoggingRecoverySafe(nullptr));
|
|
mesh::status.reader_connected=true;mesh::status.stalled=true;
|
|
assert(usbLoggingRecoverySafe(nullptr));
|
|
reset();
|
|
#endif
|
|
'''
|
|
harness = self._harness(accessor, '', callback, specific,
|
|
command_checks, '')
|
|
for cooperative in (0, 1):
|
|
with self.subTest(role=role, cooperative=cooperative):
|
|
self._compile_and_run(harness, cooperative=cooperative)
|
|
|
|
def test_actual_companion_protocol_flash_radio_and_command_ownership(self):
|
|
directory = ROOT / 'examples/companion_radio'
|
|
accessor = method((directory / 'MyMesh.cpp').read_text(),
|
|
'bool MyMesh::canRecoverUsbLogging() const')
|
|
accessor = accessor.replace('MyMesh::', 'RoleMesh::', 1)
|
|
callback = method((directory / 'main.cpp').read_text(),
|
|
'static bool usbLoggingRecoverySafe(void*)')
|
|
role_checks = r'''
|
|
the_mesh.command_radio_apply_pending=true;assert(!usbLoggingRecoverySafe(nullptr));
|
|
the_mesh.command_radio_apply_pending=false;
|
|
for(auto slot : {&the_mesh._delayed_replies.request, &the_mesh._delayed_replies.trace}){
|
|
slot->phase=mesh::CompanionDelayedReplies::AwaitRadio;
|
|
assert(!usbLoggingRecoverySafe(nullptr));
|
|
slot->phase=mesh::CompanionDelayedReplies::AwaitAdmission;
|
|
assert(!usbLoggingRecoverySafe(nullptr));
|
|
slot->phase=mesh::CompanionDelayedReplies::Empty;
|
|
}
|
|
// Terminal TRACE owns only a history reservation, not a binary reply slot.
|
|
// Its deadline still must be serviced before USB recovery can reset it.
|
|
the_mesh._terminal_trace_pending=true;
|
|
#if COMPANION_FEATURE_TEXT_TERMINAL
|
|
assert(!the_mesh.canRecoverUsbLogging());
|
|
assert(!usbLoggingRecoverySafe(nullptr));
|
|
#else
|
|
assert(the_mesh.canRecoverUsbLogging());
|
|
assert(usbLoggingRecoverySafe(nullptr));
|
|
#endif
|
|
the_mesh._terminal_trace_pending=false;
|
|
the_mesh.store.dirty=true;assert(!usbLoggingRecoverySafe(nullptr));
|
|
the_mesh.store.dirty=false;
|
|
the_mesh._scheduled_reboot_at=1;assert(!usbLoggingRecoverySafe(nullptr));
|
|
the_mesh._scheduled_reboot_at=0;
|
|
int route=1;
|
|
the_mesh.sign_data=&route;assert(!usbLoggingRecoverySafe(nullptr));
|
|
the_mesh.sign_data=nullptr;
|
|
the_mesh.serial.pending=true;assert(!usbLoggingRecoverySafe(nullptr));
|
|
the_mesh.serial.pending=false;
|
|
the_mesh._iter_started=true;the_mesh.serial.connected=true;
|
|
assert(!usbLoggingRecoverySafe(nullptr));
|
|
the_mesh.serial.connected=false;assert(usbLoggingRecoverySafe(nullptr));
|
|
the_mesh._iter_started=false;
|
|
the_mesh._serial=nullptr;assert(usbLoggingRecoverySafe(nullptr));
|
|
the_mesh._serial=&the_mesh.serial;
|
|
'''
|
|
companion_checks = r'''
|
|
#if defined(ENABLE_USB_INTERFACE)
|
|
// A live binary client or buffered protocol transaction is always protected,
|
|
// even if the dedicated logging CDC is disconnected or its output is stuck.
|
|
for(unsigned state=0;state<3;++state){
|
|
reset();mesh::status.reader_connected=state!=1;mesh::status.stalled=state==2;
|
|
the_mesh.terminal=false;usb_serial_interface.passthrough=false;usb_connected=true;
|
|
assert(!usbLoggingRecoverySafe(nullptr));
|
|
usb_connected=false;assert(usbLoggingRecoverySafe(nullptr));
|
|
usb_serial_interface.pending=true;assert(!usbLoggingRecoverySafe(nullptr));
|
|
usb_serial_interface.pending=false;
|
|
#if COMPANION_FEATURE_USB_MOTA_SOURCE
|
|
usb_mota_mode=true;assert(!usbLoggingRecoverySafe(nullptr));
|
|
usb_mota_mode=false;
|
|
#endif
|
|
}
|
|
// A network CLI owns the role without owning a log-only USB transport.
|
|
// The new network parking state must not make a stalled logger look like a
|
|
// live Binary Companion, including a host which keeps DTR asserted.
|
|
for(unsigned state=0;state<3;++state){
|
|
reset();the_mesh.terminal=false;usb_connected=true;
|
|
mesh::status.reader_connected=state!=1;mesh::status.stalled=state==2;
|
|
assert(usb_serial_interface.isPassthroughMode());
|
|
assert(usbLoggingRecoverySafe(nullptr));
|
|
}
|
|
reset();usb_terminal_line_len=1;assert(!usbLoggingRecoverySafe(nullptr));
|
|
mesh::status.reader_connected=false;assert(usbLoggingRecoverySafe(nullptr));
|
|
mesh::status.reader_connected=true;mesh::status.stalled=true;
|
|
assert(usbLoggingRecoverySafe(nullptr));
|
|
reset();usb_terminal_discard_line=true;assert(!usbLoggingRecoverySafe(nullptr));
|
|
mesh::status.reader_connected=false;assert(usbLoggingRecoverySafe(nullptr));
|
|
mesh::status.reader_connected=true;mesh::status.stalled=true;
|
|
assert(usbLoggingRecoverySafe(nullptr));
|
|
#endif
|
|
'''
|
|
harness = self._harness('bool canRecoverUsbLogging() const;', accessor,
|
|
callback, role_checks, '', companion_checks)
|
|
for usb, mota, terminal in product((0, 1), repeat=3):
|
|
with self.subTest(usb=usb, mota=mota, terminal=terminal):
|
|
self._compile_and_run(harness, cooperative=usb, usb=usb, mota=mota,
|
|
terminal=terminal)
|
|
|
|
def test_every_role_loads_durable_state_and_services_recovery_in_normal_loop(self):
|
|
for role in ('companion_radio', 'simple_repeater', 'simple_room_server',
|
|
'simple_sensor'):
|
|
with self.subTest(role=role):
|
|
source = (ROOT / 'examples' / role / 'main.cpp').read_text()
|
|
setup = method(source, 'void setup()')
|
|
loop = method(source[source.rindex('\nvoid loop()'):], 'void loop()')
|
|
self.assertIn('loadUsbLoggingWatchdog(', setup)
|
|
self.assertTrue('!volatile_primary_fs' in setup or
|
|
'!store.isVolatilePrimaryFS()' in setup)
|
|
self.assertEqual(setup.count('[]() -> uint32_t { return rtc_clock.getCurrentTime(); }'), 2)
|
|
self.assertNotIn('getCurrentTimeUnique()', setup)
|
|
self.assertIn('mesh::serviceUsbLoggingPort();', loop)
|
|
self.assertIn('if (mesh::serviceUsbLoggingWatchdog(usbLoggingRecoverySafe)) '
|
|
'board.reboot();', loop)
|
|
self.assertIn('!mesh::isUsbLoggingWatchdogArmed()', source)
|
|
|
|
@staticmethod
|
|
def _harness(declaration, definition, callback, role, commands, companion):
|
|
return (COMMON_HARNESS.replace('@ACCESSOR_DECLARATION@', declaration)
|
|
.replace('@ACCESSOR_DEFINITION@', definition)
|
|
.replace('@CALLBACK@', callback).replace('@ROLE_VETOES@', role)
|
|
.replace('@COMMAND_VETOES@', commands)
|
|
.replace('@COMPANION_VETOES@', companion))
|
|
|
|
def _compile_and_run(self, harness, cooperative, usb=0, mota=0, terminal=0):
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
work = Path(directory)
|
|
cpp = work / 'roles.cpp'
|
|
cpp.write_text(harness)
|
|
binary = work / 'roles'
|
|
flags = ['-DMESH_USB_CONSOLE_COOPERATIVE=' + str(cooperative),
|
|
'-DCOMPANION_FEATURE_USB_MOTA_SOURCE=' + str(mota),
|
|
'-DCOMPANION_FEATURE_TEXT_TERMINAL=' + str(terminal)]
|
|
if usb:
|
|
flags.append('-DENABLE_USB_INTERFACE=1')
|
|
built = subprocess.run([os.environ.get('CXX', 'g++'), '-std=c++17',
|
|
'-Wall', '-Wextra', '-Werror', '-Wno-unused-variable',
|
|
'-fsanitize=address,undefined', '-fno-sanitize-recover=all',
|
|
'-fno-pie', '-no-pie', *flags,
|
|
'-isystem', str(ROOT / "test/mocks"), f'-I{ROOT / "src"}',
|
|
str(cpp), str(ROOT / 'src/helpers/CompanionDelayedReplies.cpp'),
|
|
'-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)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|