Files
MeshCore/test/test_mcfirmware_bench_app_flash.py
T

236 lines
12 KiB
Python

"""Offline app-only flash contracts; every flash operation is a temporary file.
Reuse the data-restoration test's pinned fake parser/flasher boundary. The real
adapter runs unchanged, including input validation, guards, sole app write,
readbacks and runtime finish. No installed flasher, serial port or PIO is used.
"""
import hashlib
import json
import os
from pathlib import Path
import shutil
import struct
import unittest
import zlib
import test_mcfirmware_bench_data_restore as fixture
TOOL = fixture.ROOT / 'tools/hil/mcfirmware_bench_app_flash.sh'
IMAGE_PARSER = r'''
def image_header(image):
if len(image)<36 or image[0]!=0xe9: raise ExpansionError('bad image header')
return struct.unpack_from('<H',image,12)[0],1<<(20+(image[3]>>4))
def endf_identity(image):
trailer=image[-56:]
if len(trailer)!=56 or trailer[:4]!=b'EndF' or struct.unpack_from('<I',trailer,4)[0]!=len(image)-56:
raise ExpansionError('bad EndF length')
if trailer[8:16]!=hashlib.sha256(image[:-56]).digest()[:8]: raise ExpansionError('bad EndF hash')
version,target=struct.unpack_from('<II',trailer,16)
hardware=trailer[24:].rstrip(b'\0').decode('ascii')
if not version or not target or not hardware: raise ExpansionError('bad EndF identity')
return version,target,hardware
'''
def image(capacity, *, target=0, hardware='MOCK_BOARD', chip=9, mode=2, length=256):
body = bytearray(length - 56)
body[0:4] = bytes([0xe9, 1, mode, (capacity.bit_length() - 21) << 4])
struct.pack_into('<H', body, 12, chip)
struct.pack_into('<I', body, 32, 0xabcd5432)
return (bytes(body) + b'EndF' + struct.pack('<I', len(body)) + hashlib.sha256(body).digest()[:8]
+ struct.pack('<II', 0x01120001, target) + hardware.encode().ljust(32, b'\0'))
def ota(slot):
data = bytearray(b'\xff' * 8192)
sequence = slot + 1
struct.pack_into('<I', data, 0, sequence)
struct.pack_into('<I', data, 28, zlib.crc32(struct.pack('<I', sequence), 0xffffffff) & 0xffffffff)
return bytes(data)
@unittest.skipUnless(os.name == 'posix' and all(shutil.which(c) for c in
('bash', 'python3', 'awk', 'stat', 'sha256sum', 'realpath', 'timeout')),
'POSIX shell tools required; no physical device required')
class AppFlashTests(unittest.TestCase):
def setUp(self):
# This creates only private temporary files and PATH command stubs.
fixture.DataRestoreTests.setUp(self)
helper = self.flasher.parent / 'tools/meshcore_partition_expansion.py'
helper.write_text(fixture.PARSER + IMAGE_PARSER)
source = self.flasher.read_text().replace(fixture.digest(fixture.PARSER.encode()),
fixture.digest(helper.read_bytes()))
source = source.replace('Detected flash size:8MB\\n\' "${MOCK_CHIP:-ESP32-S3}"',
'Detected flash size:%sMB\\n\' "${MOCK_CHIP:-ESP32-S3}" "${MOCK_MB:-8}"')
source = source.replace("'nvs-readback'", "'application-readback'")
source = source.replace("int(sys.argv[2],0)==0x9000 and os.environ.get('MOCK_MUTATE_AFTER_WRITE')",
"os.environ.get('MOCK_MUTATE_AFTER_WRITE')")
self.flasher.write_text(source)
self.image_path = self.root / 'application.bin'
self.configure()
def configure(self, *, layout='xiao', active=0, relocated=False):
self.capacity = 0x1000000 if layout == 'v4' else 0x800000
if layout == 'v4':
self.apps = [(0x10000, 0x640000), (0x650000, 0x640000)]
data = [('spiffs', 1, 0x82, 0xc90000, 0x360000), ('coredump', 1, 3, 0xff0000, 0x10000)]
elif layout == 'indicator':
self.apps = [(0x10000, 0x280000), (0x400000, 0x280000)]
data = [('spiffs', 1, 0x82, 0x290000, 0x160000), ('coredump', 1, 3, 0x3f0000, 0x10000)]
else:
self.apps = [(0x10000, 0x330000), (0x340000, 0x330000)]
data = [('spiffs', 1, 0x82, 0x670000, 0x190000)]
self.active = active
self.ota_address = 0xc000 if relocated else 0xe000
entries = [('nvs', 1, 2, 0x9000, 0x3000 if relocated else 0x5000),
('otadata', 1, 0, self.ota_address, 0x2000)]
entries += [('app' + str(i), 0, 0x10 + i, *part) for i, part in enumerate(self.apps)] + data
self.table = b''.join(struct.pack('<2sBBII16sI', b'\xaa\x50', kind, sub, offset, length,
name.encode(), 0)
for name, kind, sub, offset, length in entries).ljust(4096, b'\xff')
before = bytearray(b'\xff' * self.capacity)
before[0x8000:0x9000] = self.table
before[self.ota_address:self.ota_address + 8192] = ota(active)
self.before = bytes(before)
self.backup.write_bytes(before)
self.flash.write_bytes(before)
self.trace.unlink(missing_ok=True)
self.manifest.write_text(json.dumps(dict(complete=True, operation='read-only-flash-backup',
chip='ESP32-S3', device_mac=fixture.MAC, offset=0, length=self.capacity,
flash_bytes=self.capacity, sha256=fixture.digest(before), artifact=self.backup.name)))
self.image = image(self.capacity)
self.image_path.write_bytes(self.image)
address, size = self.apps[active]
self.args = ['bash', str(TOOL), '--port', '/dev/ttyACM99', '--usb-path', 'exact-path',
'--usb-id', 'exact-id', '--device', 'MOCK', '--bench-dir', str(self.bench),
'--expect-mac', fixture.MAC, '--backup', str(self.backup), '--backup-manifest', str(self.manifest),
'--backup-sha256', fixture.digest(before), '--expect-partition-sha256', fixture.digest(self.table),
'--address', hex(address), '--slot-size', hex(size), '--image', str(self.image_path),
'--image-sha256', fixture.digest(self.image), '--expect-target-id', '0',
'--expect-hardware-id', 'MOCK_BOARD', '--allow-write', '--flasher', str(self.flasher),
'--flasher-sha256', fixture.digest(self.flasher.read_bytes())]
def argument(self, option, value):
self.args[self.args.index(option) + 1] = str(value)
def run_tool(self, **env):
self.trace.unlink(missing_ok=True)
return fixture.DataRestoreTests.run_tool(self, MOCK_MB=str(self.capacity // 0x100000), **env)
def refuse(self, *, before_prepare=False, **env):
result = self.run_tool(**env)
fixture.DataRestoreTests.assert_no_writes(self, result)
self.assertEqual(self.flash.read_bytes(), self.before)
if before_prepare:
self.assertNotIn('prepare', self.actions)
def assert_success(self, **env):
result = self.run_tool(**env)
self.assertEqual(result.returncode, 0, result.stderr)
writes = [line.split() for line in self.actions if 'write-flash' in line]
self.assertEqual([int(line[6]) for line in writes], [self.apps[self.active][0]])
expected = bytearray(self.before)
offset = self.apps[self.active][0]
expected[offset:offset + len(self.image)] = self.image
self.assertEqual(self.flash.read_bytes(), expected)
self.assertEqual(self.actions[-1], 'finish')
self.assertFalse(any('erase' in line for line in self.actions))
return result
def test_only_active_app_written_in_both_slots_of_three_real_layout_shapes(self):
for layout in ('v4', 'xiao', 'indicator'):
for active in (0, 1):
with self.subTest(layout=layout, active=active):
self.configure(layout=layout, active=active)
self.assert_success()
self.assertFalse(any('hard-reset' in action for action in self.actions))
def test_relocated_ota_read_and_uart_final_probe_reset_then_finish(self):
self.configure(layout='indicator', active=1, relocated=True)
self.assert_success(MOCK_AFTER='hard-reset')
self.assertIn('--after hard-reset --baud 115200 read-mac', self.actions[-2])
reads = [int(line.split()[6], 0) for line in self.actions if 'read-flash' in line]
self.assertEqual(reads.count(0xc000), 2)
self.assertNotIn(0xe000, reads)
def test_live_chip_or_capacity_mismatch_refused_before_write(self):
self.refuse(MOCK_CHIP='ESP32-C3')
# Keep the image/backup capacity at8MiB, change only the live probe.
self.capacity = 0x1000000
self.refuse()
def test_changed_live_table_or_active_ota_refused_before_write(self):
for address, replacement in ((0x8000, b'\x00'), (self.ota_address, ota(1))):
with self.subTest(address=address):
changed = bytearray(self.before)
changed[address:address + len(replacement)] = replacement
self.flash.write_bytes(changed)
# The altered live state itself must also remain untouched.
self.before = bytes(changed)
self.refuse()
self.configure()
def test_wrong_requested_active_slot_or_size_refused_before_prepare(self):
self.argument('--address', hex(self.apps[1][0]))
self.refuse(before_prepare=True)
self.configure()
self.argument('--slot-size', self.apps[0][1] - 4096)
self.refuse(before_prepare=True)
def test_image_hash_backup_hash_and_mac_refused_before_prepare(self):
for option, wrong in (('--image-sha256', '0' * 64), ('--backup-sha256', '0' * 64),
('--expect-mac', '112233445566'), ('--expect-partition-sha256', '0' * 64)):
with self.subTest(option=option):
self.configure()
self.argument(option, wrong)
self.refuse(before_prepare=True)
def test_full_zero_target_requires_exact_optin_and_hardware(self):
for option, wrong in (('--expect-target-id', '1234'), ('--expect-hardware-id', 'OTHER_BOARD')):
with self.subTest(option=option):
self.configure()
self.argument(option, wrong)
self.refuse(before_prepare=True)
def test_endf_body_corruption_and_oversize_image_refused_before_prepare(self):
for changed in (self.image[:40] + bytes([self.image[40] ^ 1]) + self.image[41:],
image(self.capacity, length=self.apps[0][1] + 1)):
with self.subTest(length=len(changed)):
self.image_path.write_bytes(changed)
self.argument('--image-sha256', fixture.digest(changed))
self.refuse(before_prepare=True)
def test_image_chip_capacity_dio_and_descriptor_refused_before_prepare(self):
images = [image(self.capacity, chip=5), image(0x1000000), image(self.capacity, mode=0)]
bad_descriptor = bytearray(self.image)
bad_descriptor[32] ^= 1
images.append(bytes(bad_descriptor))
for changed in images:
with self.subTest(header=changed[:4].hex()):
self.image_path.write_bytes(changed)
self.argument('--image-sha256', fixture.digest(changed))
self.refuse(before_prepare=True)
def test_app_readback_fault_or_changed_table_ota_prevents_runtime_finish(self):
for env in ({'MOCK_BAD_READBACK': '1'}, {'MOCK_MUTATE_AFTER_WRITE': '0x8000'},
{'MOCK_MUTATE_AFTER_WRITE': hex(self.ota_address)}):
with self.subTest(fault=env):
self.configure()
result = self.run_tool(**env)
self.assertNotEqual(result.returncode, 0)
self.assertEqual(sum('write-flash' in line for line in self.actions), 1)
self.assertNotIn('finish', self.actions)
def test_nonzero_infrastructure_target_keeps_existing_interface(self):
self.image = image(self.capacity, target=1234)
self.image_path.write_bytes(self.image)
self.argument('--image-sha256', fixture.digest(self.image))
for option in ('--expect-target-id', '--expect-hardware-id'):
index = self.args.index(option)
del self.args[index:index + 2]
self.assert_success()
if __name__ == '__main__':
unittest.main()