Files
MeshCore/test/test_mono_led_rollover.py
T

115 lines
6.3 KiB
Python

"""Exercise real monochrome LED timers, exact field defaults, and ownership gates.
The complete production handler is compiled for original/new/tiny UIs. RGB and
backlight are not configured. uint32_t millis and GPIO writes are host fakes;
this is not electrical, scheduler-latency, or hardware power-consumption proof.
"""
from pathlib import Path
import os
import re
import shutil
import subprocess
import tempfile
import unittest
from test_replay_reset_integration import extract_braced
ROOT = Path(__file__).resolve().parents[1]
FIXTURE = ROOT / 'test/fixtures/mono_led_rollover.cpp'
UIS = ('ui-orig', 'ui-new', 'ui-tiny')
CASES = ('zero', 'upper', 'max', 'normal', 'unread', 'signed', 'wrap', 'off-wrap',
'zero-stamp', 'message-on', 'message-off', 'owner-fresh', 'mask-fresh',
'owner-on-long', 'owner-off-long', 'mask-on-long', 'mask-off-long',
'suppressed-short', 'delayed-one-phase', 'both-vetoes')
def harness(ui, mutation=None):
prefix = ROOT / 'examples/companion_radio' / ui
source = (prefix / 'UITask.cpp').read_text(encoding='ascii')
header = (prefix / 'UITask.h').read_text(encoding='ascii')
abstract = (ROOT / 'examples/companion_radio/AbstractUITask.h').read_text(encoding='ascii')
handler = extract_braced(source, 'void UITask::userLedHandler()')
first_constant = source.index('#define LED_ON_MILLIS')
constants = source[source.rfind('#', 0, first_constant):
source.index('#endif', first_constant) + len('#endif')]
fields = ''
if ui != 'ui-orig':
first_field = header.index(' int led_state = 0;')
fields = header[header.rfind('#ifdef PIN_STATUS_LED', 0, first_field):
header.index('#endif', first_field) + len('#endif')]
defaults = '\n'.join(re.findall(r'^ (?:bool _notification_led_owned|uint8_t _notification_outputs) = .*;',
abstract, re.MULTILINE))
setters = '\n'.join(extract_braced(abstract, signature) for signature in (
'void setNotificationLedOwnership(', 'void setNotificationOutputs('))
if mutation == 'no-owner-guard':
guard = 'if (_notification_led_owned || !(_notification_outputs & mesh::notify::Led)) return;'
if handler.count(guard) != 1:
raise AssertionError('the actual ownership/output guard must be present')
handler = handler.replace(guard, '', 1)
elif mutation == 'non-strict-edge':
# Only mutate the monochrome branch, leaving the unrelated RGB text alone.
first = handler.index('#elif defined(PIN_STATUS_LED)') if ui == 'ui-orig' else 0
before, mono = handler[:first], handler[first:]
mono, count = re.subn(r'(\)\s*)>(\s*(?:led_)?phase_millis\))', r'\1>=\2', mono, count=1)
if count != 1:
raise AssertionError('monochrome phase timer must retain its strict edge')
handler = before + mono
code = FIXTURE.read_text(encoding='ascii')
for token, value in {'CONSTANTS': constants, 'FIELDS': fields, 'DEFAULTS': defaults,
'SETTERS': setters, 'HANDLER': handler}.items():
code = code.replace('@' + token + '@', value)
return code
class MonoLedRolloverTest(unittest.TestCase):
def test_actual_monochrome_handlers_preserve_cadence_and_ownership(self):
self.run_variants()
def test_missing_ownership_guard_fails_semantic_check(self):
self.run_variants('no-owner-guard', ('owner-fresh',), 'FAIL owner-fresh: writes == count')
def test_early_edge_regression_is_rejected(self):
self.run_variants('non-strict-edge', ('zero',), 'FAIL zero: writes == count')
def run_variants(self, mutation=None, cases=CASES, expected_failure=None):
compiler = shutil.which('g++') or shutil.which('clang++')
if compiler is None:
self.skipTest('a host C++17 compiler is required')
sanitize = [] if os.name == 'nt' else [
'-fsanitize=address,undefined', '-fno-sanitize-recover=all', '-fno-pie', '-no-pie']
with tempfile.TemporaryDirectory(prefix='mono-led-rollover-') as directory:
work = Path(directory)
for ui in UIS:
source = work / (ui + '.cpp')
source.write_text(harness(ui, mutation), encoding='ascii')
variants = (0, 1) if mutation else (0, 1, None)
for polarity in variants:
with self.subTest(ui=ui, polarity=polarity):
name = ui + '-' + ('none' if polarity is None else str(polarity))
binary = work / (name + ('.exe' if os.name == 'nt' else ''))
macros = (['-DPIN_STATUS_LED=24', '-DLED_STATE_ON=' + str(polarity)]
if polarity is not None else [])
built = subprocess.run([
compiler, '-std=c++17', '-O1', '-g', '-Wall', '-Wextra', '-Werror',
'-Wno-misleading-indentation', *sanitize, *macros,
'-I' + str(ROOT / 'src'), str(source), '-o', str(binary),
], capture_output=True, text=True, timeout=60)
self.assertEqual(built.returncode, 0, built.stdout + built.stderr)
# ui-orig really uses function statics: never reset them in a fake.
for case in cases if polarity is not None else ('no-led',):
with self.subTest(case=case):
tested = subprocess.run([str(binary), case], capture_output=True,
text=True, timeout=10)
if expected_failure:
self.assertEqual(tested.returncode, 1, tested.stdout + tested.stderr)
self.assertIn(expected_failure, tested.stderr)
self.assertNotIn('AddressSanitizer:', tested.stderr)
self.assertNotIn('runtime error:', tested.stderr)
else:
self.assertEqual(tested.returncode, 0, tested.stdout + tested.stderr)
self.assertRegex(tested.stdout, r'PASS ' + case + r': \d+ checks')
if __name__ == '__main__':
unittest.main()