Files
MeshCore/test/test_ota_apply_reentry.py
T

118 lines
5.8 KiB
Python

"""Run production apply admission and preserve an already-armed OTA handoff."""
from pathlib import Path
import os
import subprocess
import tempfile
import unittest
from test_replay_reset_integration import extract_braced
ROOT = Path(__file__).resolve().parents[1]
def actual_harness(context=None, cli=None):
if context is None:
context = (ROOT / 'src/helpers/ota/OtaContext.h').read_text()
if cli is None:
cli = (ROOT / 'src/helpers/ota/OtaCli.cpp').read_text()
apply = (ROOT / 'src/helpers/ota/OtaApply.cpp').read_text()
methods = '\n'.join(extract_braced(context, signature) for signature in (
'bool hwMatches(', 'bool apply_fetched(', 'bool apply_fetched_trusted_auto(',
'bool apply_fetched_rescue(', 'bool apply_fetched_impl(',
'bool apply_fetched_bootloader('))
# Compile the exact applier entry and its early read failure. Successful
# crypto/flash approval is a boundary stub; this suite must not be mistaken
# for full package verification or physical bootloader qualification.
entry = extract_braced(apply, 'bool ota_apply_mota_nrf52(OtaStoreFlashNrf52& store,')
entry = entry[:entry.index('\n MotaManifest m;')] + r'''
if (!boundaryApply(st, msg)) return false;
g_nrf52_apply_store = &store;
return true;
}
'''
code = (ROOT / 'test/fixtures/ota_apply_reentry.cpp').read_text(encoding='ascii')
for key, value in {
'ENTRY': entry,
'REBOOT': extract_braced(
apply, 'void ota_reboot_to_apply() { // public:'),
'BOOT_REBOOT': extract_braced(apply, 'void ota_reboot_to_bootloader_update() {\n'),
'METHODS': methods,
'IS_CMD': extract_braced(cli, 'static bool is_cmd('),
'INSTALL': extract_braced(cli, 'if (is_cmd(a, "install|apply|applydelta", &rest))'),
}.items():
code = code.replace('@' + key + '@', value)
return code
class OtaApplyReentryTest(unittest.TestCase):
def test_armed_application_and_bootloader_admission_preserves_handoff(self):
self.run_harness(actual_harness())
def test_application_guard_negative_control(self):
self.run_without_guard('context', 'bool apply_fetched_impl(')
def test_bootloader_guard_negative_control(self):
self.run_without_guard('context', 'bool apply_fetched_bootloader(')
def test_cli_guard_negative_control(self):
self.run_without_guard('cli', 'if (is_cmd(a, "install|apply|applydelta", &rest))')
def run_without_guard(self, owner, signature):
context = (ROOT / 'src/helpers/ota/OtaContext.h').read_text()
cli = (ROOT / 'src/helpers/ota/OtaCli.cpp').read_text()
source = context if owner == 'context' else cli
body = extract_braced(source, signature)
guard = extract_braced(body, 'if (' + ('c.' if owner == 'cli' else '') + 'apply_pending')
self.assertIn('bootloader_apply_pending', guard)
self.assertIn('return', guard)
without = source.replace(body, body.replace(guard, '', 1), 1)
self.run_harness(actual_harness(
without if owner == 'context' else context,
without if owner == 'cli' else cli), expect_failure=True)
def run_harness(self, code, expect_failure=False):
variants = (
('rak', ('OTA_RAK_AUTO_STORE', 'OTA_INTERNAL_BOOTLOADER_UPDATE', 'RAK_4631'), 144, 19),
('rak_sd', ('OTA_RAK_AUTO_STORE', 'OTA_INTERNAL_BOOTLOADER_UPDATE', 'RAK_4631',
'OTA_SD_STORE', 'OTA_RAK15002_SD_STORE'), 192, 25),
('internal_rescue', (), 96, 13),
)
sanitizers = [] if os.name == 'nt' else [
'-fsanitize=address,undefined', '-fno-sanitize-recover=all',
'-fno-pie', '-no-pie']
with tempfile.TemporaryDirectory(prefix='meshcore-ota-apply-reentry-') as directory:
work = Path(directory)
source = work / 'test.cpp'
source.write_text(code, encoding='ascii')
for name, macros, pending, healthy in variants:
with self.subTest(variant=name):
binary = work / (name + ('.exe' if os.name == 'nt' else ''))
built = subprocess.run([
os.environ.get('CXX', 'c++'), '-std=c++17',
'-ffunction-sections', '-fdata-sections', '-Wl,--gc-sections',
*sanitizers, '-DNRF52_PLATFORM=1', '-DOTA_FLASH_STORE=1',
*['-D' + macro + '=1' for macro in macros],
'-I' + str(ROOT / 'src'), '-I' + str(ROOT / 'test'),
'-I' + str(ROOT / 'test/mocks'), str(source),
str(ROOT / 'src/helpers/ota/MotaContainer.cpp'), '-o', str(binary),
], capture_output=True, text=True, timeout=60)
self.assertEqual(built.returncode, 0, built.stdout + built.stderr)
tested = subprocess.run([str(binary)], capture_output=True,
text=True, timeout=20)
if expect_failure:
self.assertNotEqual(tested.returncode, 0, 'unguarded path escaped regression')
# A compile failure or incidental sanitizer crash is
# not evidence that the intended guard was exercised.
self.assertIn('Assertion', tested.stderr)
self.assertNotIn('AddressSanitizer:', tested.stderr)
self.assertNotIn('runtime error:', tested.stderr)
else:
self.assertEqual(tested.returncode, 0, tested.stdout + tested.stderr)
self.assertIn(
f'PASS: {pending} armed admission cases; {healthy} first-apply/retry controls',
tested.stdout)
if __name__ == '__main__':
unittest.main()