115 lines
5.1 KiB
Python
115 lines
5.1 KiB
Python
#!/usr/bin/env python3
|
|
"""Execute real floor deadlines/transitions with counted host I/O adapters.
|
|
|
|
These tests prove scheduling and IRQ-ownership contracts, not actual current
|
|
consumption, RTOS sleep residency, or RF sample quality on untested hardware.
|
|
"""
|
|
|
|
from pathlib import Path
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
|
|
from test_radio_receive_contract import ROOT, method, transition_harness
|
|
|
|
|
|
class NoiseFloorSleepTests(unittest.TestCase):
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
cls.directory = tempfile.TemporaryDirectory(prefix='meshcore-floor-sleep-')
|
|
cls.addClassCleanup(cls.directory.cleanup)
|
|
work = Path(cls.directory.name)
|
|
source = (ROOT / 'src/helpers/radiolib/RadioLibWrappers.cpp').read_text()
|
|
header = (ROOT / 'src/helpers/radiolib/RadioLibWrappers.h').read_text()
|
|
getter = method(source, 'bool RadioLibWrapper::getNoiseFloorWakeDelay(')
|
|
harness = transition_harness(source).split('struct LegacyRadio {', 1)[0]
|
|
harness = harness.replace(getter, '', 1)
|
|
harness = harness.replace('#include <cassert>', '#include <cassert>\n#include <cstring>')
|
|
harness = harness.replace(' bool tryResetAGC();',
|
|
' bool isInRecvMode() const { return state == STATE_RX; }\n'
|
|
+ method(header, 'bool isCalibratingNoiseFloor() const').replace(' override', '')
|
|
+ '\n bool tryResetAGC();')
|
|
harness = harness.replace('bool isChipBusy() { return busy; }',
|
|
'unsigned busy_probes = 0, packet_probes = 0;\n'
|
|
' bool isChipBusy() { ++busy_probes; return busy; }')
|
|
harness = harness.replace('bool isReceivingPacket() { return packet; }',
|
|
'bool isReceivingPacket() { ++packet_probes; return packet; }')
|
|
fixture = (ROOT / 'test/fixtures/noise_floor_sleep.cpp').read_text()
|
|
variants = {
|
|
'production': getter,
|
|
'agc_forces_floor': getter,
|
|
'old_batch_veto': getter[:getter.index('{') + 1] + '''
|
|
(void)now;
|
|
if (!isCalibratingNoiseFloor()) return false;
|
|
delay_ms = 0;
|
|
return true;
|
|
}
|
|
''',
|
|
}
|
|
cls.binaries = {}
|
|
sanitize = [] if not sys.platform.startswith('linux') else [
|
|
'-fsanitize=address,undefined', '-fno-sanitize-recover=all',
|
|
'-fno-omit-frame-pointer', '-fno-pie', '-no-pie']
|
|
for family in ('sx1262', 'other'):
|
|
for name, implementation in variants.items():
|
|
filename = name + '_' + family
|
|
cpp, binary = work / (filename + '.cpp'), work / filename
|
|
variant_harness = harness.replace('resetAGCIfIdle(false)', 'resetAGCIfIdle()') \
|
|
if name == 'agc_forces_floor' else harness
|
|
cpp.write_text((variant_harness + implementation + fixture).replace('\u2014', '-'),
|
|
encoding='ascii')
|
|
built = subprocess.run([os.environ.get('CXX', 'g++'), '-std=c++17',
|
|
'-O1', '-g', '-Wall', '-Wextra', '-Werror',
|
|
*(['-DUSE_SX1262=1'] if family == 'sx1262' else []),
|
|
*sanitize, '-I', str(ROOT / 'src'), str(cpp), '-o', str(binary)],
|
|
capture_output=True, text=True, timeout=60)
|
|
if built.returncode:
|
|
raise AssertionError(built.stdout + built.stderr)
|
|
cls.binaries[name, family] = binary
|
|
|
|
def run_case(self, scenario, variant='production', failure=None):
|
|
for family in ('sx1262', 'other'):
|
|
with self.subTest(family=family):
|
|
result = subprocess.run([str(self.binaries[variant, family]), scenario],
|
|
capture_output=True, text=True, timeout=20)
|
|
if failure:
|
|
self.assertNotEqual(result.returncode, 0, result.stdout + result.stderr)
|
|
self.assertIn(failure, result.stderr)
|
|
else:
|
|
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
|
self.assertIn('PASS: ' + scenario, result.stdout)
|
|
|
|
def test_quiet_batches_allow_idle_between_all_original_samples(self):
|
|
self.run_case('quiet')
|
|
|
|
def test_deadlines_and_settling_survive_rollover_including_zero(self):
|
|
self.run_case('rollover')
|
|
|
|
def test_deadline_queries_have_no_io_or_irq_consumption(self):
|
|
self.run_case('queries')
|
|
|
|
def test_rejected_samples_can_idle_and_keep_timeout_bound(self):
|
|
self.run_case('rejected')
|
|
|
|
def test_success_becomes_lazy_but_held_rises_and_timeouts_retry_sooner(self):
|
|
self.run_case('lazy')
|
|
|
|
def test_timeout_timestamp_zero_is_not_an_unarmed_or_startup_sentinel(self):
|
|
self.run_case('timeout-zero')
|
|
|
|
def test_adaptive_agc_keeps_valid_floor_cadence_but_not_changed_paths(self):
|
|
self.run_case('agc-idle')
|
|
|
|
def test_old_agc_forced_collection_negative_control(self):
|
|
self.run_case('agc-idle', 'agc_forces_floor',
|
|
'maintenance must preserve the floor')
|
|
|
|
def test_old_whole_batch_sleep_veto_negative_control(self):
|
|
self.run_case('quiet', 'old_batch_veto', 'pending batch must permit idle')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|