115 lines
6.0 KiB
Python
115 lines
6.0 KiB
Python
"""Compile actual guarded backlight polling with original timer initialization.
|
|
|
|
Both UI paths are fixture-enabled, plus feature-disabled controls. Current
|
|
normal BACKLIGHT_BTN recipes select ui-new; ui-tiny is consistency coverage.
|
|
Arduino clock/GPIO and display wake are host fakes, not hardware sleep tests.
|
|
"""
|
|
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/backlight_poll_rollover.cpp'
|
|
UIS = ('ui-new', 'ui-tiny')
|
|
CASES = ('zero', 'upper', 'upper-pressed', 'max', 'normal', 'signed-boundary',
|
|
'wrap-visible', 'wrap-wake', 'deadline-zero', 'held', 'unobserved-release',
|
|
'null-pressed', 'null-released', 'completion', 'completion-zero-wake',
|
|
'completion-zero-read', 'long-released', 'long-pressed', 'near-full-cycle',
|
|
'late-one-poll')
|
|
|
|
|
|
def production(ui):
|
|
prefix = ROOT / 'examples/companion_radio' / ui
|
|
source = (prefix / 'UITask.cpp').read_text(encoding='ascii')
|
|
header = (prefix / 'UITask.h').read_text(encoding='ascii')
|
|
first = source.index('#if defined(BACKLIGHT_BTN)')
|
|
block = source[first:source.index('#endif', first) + len('#endif')]
|
|
field = re.search(r'^ (?:int|uint32_t) (?:next|last)_backlight_btn_check = 0;',
|
|
header, re.MULTILINE).group()
|
|
return source, field, block
|
|
|
|
|
|
def harness(ui, mutation=None):
|
|
_, field, block = production(ui)
|
|
if mutation == 'level-wake':
|
|
block, count = re.subn(r'pressed\s*&&\s*button_state\s*!=\s*BacklightButtonState::Pressed\s*&&',
|
|
'pressed &&', block, count=1)
|
|
if count != 1:
|
|
raise AssertionError('backlight wake must recognize a newly sampled press')
|
|
elif mutation == 'entry-anchor':
|
|
old = 'last_backlight_btn_check = millis();'
|
|
if block.count(old) != 1:
|
|
raise AssertionError('backlight timestamp must be anchored after I/O and wake')
|
|
block = block.replace(old, 'last_backlight_btn_check = backlight_now;', 1)
|
|
return FIXTURE.read_text(encoding='ascii').replace('@FIELD@', field).replace('@BLOCK@', block)
|
|
|
|
|
|
class BacklightPollRolloverTest(unittest.TestCase):
|
|
def test_actual_polling_wrap_cadence_and_button_state(self):
|
|
self.run_variants()
|
|
|
|
def test_block_remains_in_live_loop_with_32bit_timer(self):
|
|
for ui in UIS:
|
|
with self.subTest(ui=ui):
|
|
source, field, block = production(ui)
|
|
self.assertEqual(field.strip(), 'uint32_t last_backlight_btn_check = 0;')
|
|
loop = extract_braced(source, 'void UITask::loop()')
|
|
self.assertEqual(loop.count(block), 1)
|
|
self.assertLess(loop.index(block), loop.index('userLedHandler();'))
|
|
self.assertIn('digitalRead(PIN_BUTTON2) == LOW', block)
|
|
self.assertIn('_display->wake(mesh::ui::DisplayWake::Button)', block)
|
|
|
|
def test_held_level_wake_regression_is_rejected(self):
|
|
self.run_variants('level-wake', ('held',), 'FAIL held: wakes == expected_wakes')
|
|
|
|
def test_entry_anchoring_regression_is_rejected(self):
|
|
self.run_variants('entry-anchor', ('completion',), 'FAIL completion: reads == expected_reads')
|
|
|
|
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='backlight-poll-rollover-') as directory:
|
|
work = Path(directory)
|
|
for ui in UIS:
|
|
source = work / (ui + '.cpp')
|
|
source.write_text(harness(ui, mutation), encoding='ascii')
|
|
for enabled in (True,) if mutation else (True, False):
|
|
with self.subTest(ui=ui, enabled=enabled):
|
|
binary = work / (ui + '-' + str(int(enabled)) +
|
|
('.exe' if os.name == 'nt' else ''))
|
|
# A disabled build deliberately has no PIN_BUTTON2/LOW
|
|
# definitions: accidental unguarded I/O cannot compile.
|
|
macros = ['-DBACKLIGHT_BTN=PIN_BUTTON2', '-DPIN_BUTTON2=14'] if enabled else []
|
|
built = subprocess.run([
|
|
compiler, '-std=c++17', '-O1', '-g', '-Wall', '-Wextra', '-Werror',
|
|
*sanitize, *macros, str(source), '-o', str(binary),
|
|
], capture_output=True, text=True, timeout=60)
|
|
self.assertEqual(built.returncode, 0, built.stdout + built.stderr)
|
|
# Preserve the real function-static sampled state by
|
|
# launching a fresh process, not resetting fake statics.
|
|
for case in cases if enabled else ('no-feature',):
|
|
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()
|