134 lines
6.6 KiB
Python
134 lines
6.6 KiB
Python
"""Run complete infrastructure UI begin methods and headers with denied strdup.
|
|
|
|
Only board, display, button and radio-page boundaries are adapters. This does
|
|
not simulate total-heap exhaustion or qualify hardware/display initialization.
|
|
"""
|
|
from pathlib import Path
|
|
import os
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
|
|
from test_replay_reset_integration import extract_braced
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
ROLES = ('simple_sensor', 'simple_repeater', 'simple_room_server')
|
|
FIXTURE = ROOT / 'test/fixtures/infrastructure_ui_version.cpp'
|
|
|
|
# Original identical formatter from all three roles at 7acd76d0950e89d2324191c9359053b364e03db5.
|
|
COPY_FORMAT = ''' // strip off dash and commit hash by changing dash to null terminator
|
|
// e.g: v1.2.3-abcdef -> v1.2.3
|
|
char *version = strdup(firmware_version);
|
|
char *dash = strchr(version, '-');
|
|
if(dash){
|
|
*dash = 0;
|
|
}
|
|
|
|
// v1.2.3 (1 Jan 2025)
|
|
snprintf(_version_info, sizeof(_version_info), "%s (%s)", version, build_date);
|
|
free(version);
|
|
'''
|
|
|
|
|
|
class InfrastructureUiVersionTest(unittest.TestCase):
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
compiler = os.environ.get('CXX') or shutil.which('g++') or shutil.which('clang++')
|
|
if not compiler:
|
|
raise unittest.SkipTest('a host C++17 compiler is required')
|
|
cls.temp = tempfile.TemporaryDirectory(prefix='infrastructure-ui-version-')
|
|
cls.addClassCleanup(cls.temp.cleanup)
|
|
work = Path(cls.temp.name)
|
|
(work / 'helpers/ui').mkdir(parents=True)
|
|
for stub in ('helpers/ui/DisplayDriver.h', 'helpers/CommonCLI.h',
|
|
'helpers/RadioActivityWindow.h', 'helpers/ui/ObserverDashboard.h',
|
|
'helpers/ui/CHSC6XTouch.h'):
|
|
(work / stub).write_text('#pragma once\n', encoding='ascii')
|
|
sanitize = [] if not sys.platform.startswith('linux') else [
|
|
'-fsanitize=address,undefined', '-fno-sanitize-recover=all',
|
|
'-fno-omit-frame-pointer', '-fno-pie', '-no-pie']
|
|
cls.binaries = {}
|
|
for role in ROLES:
|
|
directory = work / role
|
|
directory.mkdir()
|
|
source = (ROOT / 'examples' / role / 'UITask.cpp').read_text(encoding='ascii')
|
|
header = (ROOT / 'examples' / role / 'UITask.h').read_text(encoding='ascii')
|
|
# Access changes only; preserve the actual declarations and array.
|
|
if header.count('class UITask {') != 1:
|
|
raise AssertionError('expected one actual UI class declaration')
|
|
(directory / 'UITask.h').write_text(
|
|
header.replace('class UITask {', 'struct UITask {'), encoding='ascii')
|
|
begin = extract_braced(source, 'void UITask::begin(')
|
|
start = begin.index(' // strip off dash')
|
|
end = (begin.index(' // Make the first read/frame', start)
|
|
if role == 'simple_sensor' else begin.rfind('}'))
|
|
legacy = begin[:start] + COPY_FORMAT + '\n' + begin[end:]
|
|
for kind, method in (('production', begin), ('copy', legacy)):
|
|
cpp = directory / (kind + '.cpp')
|
|
cpp.write_text(FIXTURE.read_text(encoding='ascii').replace('@BEGIN@', method),
|
|
encoding='ascii')
|
|
for features in (0, 1, 2):
|
|
macros = ['-DTEST_SENSOR=' + str(int(role == 'simple_sensor')),
|
|
'-DTEST_REPEATER=' + str(int(role == 'simple_repeater')),
|
|
'-DTEST_FEATURES=' + str(features)]
|
|
if features:
|
|
macros += ['-DPIN_USER_BTN=4', '-DDISPLAY_CLASS=DisplayDriver',
|
|
'-DMOMENTARY_BUTTON_WAKE_FROM_SLEEP=1']
|
|
if features == 2:
|
|
macros += ['-DDISPLAY_TOUCH_TOGGLE=1', '-DDISPLAY_ACTIVITY_DASHBOARD=1',
|
|
'-DDISPLAY_REDRAW_ON_CHANGE=1']
|
|
binary = directory / (kind + '-' + str(features)
|
|
+ ('.exe' if os.name == 'nt' else ''))
|
|
built = subprocess.run([
|
|
compiler, '-std=c++17', '-O1', '-g', '-Wall', '-Wextra', '-Werror',
|
|
*sanitize, *macros, '-I' + str(work), str(cpp), '-o', str(binary)],
|
|
capture_output=True, text=True, timeout=60)
|
|
if built.returncode:
|
|
raise AssertionError(built.stdout + built.stderr)
|
|
cls.binaries[role, kind, features] = binary
|
|
|
|
def check_case(self, case, kind='production', expected_failure=False):
|
|
for role in ROLES:
|
|
for features in (0, 1, 2):
|
|
with self.subTest(role=role, features=features, case=case):
|
|
checked = subprocess.run([str(self.binaries[role, kind, features]), case],
|
|
capture_output=True, text=True, timeout=10)
|
|
if expected_failure:
|
|
self.assertNotEqual(checked.returncode, 0)
|
|
self.assertRegex(checked.stderr,
|
|
r'runtime error: null pointer|AddressSanitizer:')
|
|
else:
|
|
self.assertEqual(checked.returncode, 0, checked.stdout + checked.stderr)
|
|
self.assertIn('PASS ' + case, checked.stdout)
|
|
|
|
def test_displayed_labels_bounds_and_startup_side_effects(self):
|
|
self.check_case('format')
|
|
|
|
def test_formatting_needs_no_mutable_version_copy(self):
|
|
self.check_case('no-copy')
|
|
|
|
def test_denied_version_allocation_cannot_crash_startup(self):
|
|
self.check_case('denied')
|
|
|
|
@unittest.skipUnless(sys.platform.startswith('linux'), 'sanitizer diagnostic control')
|
|
def test_original_copy_failure_is_reproduced_not_silently_skipped(self):
|
|
self.check_case('denied', 'copy', expected_failure=True)
|
|
|
|
def test_actual_capacity_live_startup_and_ci_wiring(self):
|
|
for role in ROLES:
|
|
with self.subTest(role=role):
|
|
folder = ROOT / 'examples' / role
|
|
header = (folder / 'UITask.h').read_text(encoding='ascii')
|
|
self.assertRegex(header, r'char _version_info\[32\];')
|
|
main = (folder / 'main.cpp').read_text(encoding='ascii')
|
|
self.assertIn('ui_task.begin(', extract_braced(main, 'void setup()'))
|
|
workflow = (ROOT / '.github/workflows/run-unit-tests.yml').read_text(encoding='ascii')
|
|
self.assertIn('python3 -B test/test_infrastructure_ui_version.py -v', workflow)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|