#!/usr/bin/env python3 import json from pathlib import Path import subprocess import sys import tempfile import unittest import struct import io import zipfile import gzip ROOT = Path(__file__).resolve().parents[1] CHECKER = ROOT / "scripts" / "check_firmware_capabilities.py" sys.path.insert(0, str(ROOT / "tools" / "mota")) import motalib as mota class FirmwareCapabilityCheckerTest(unittest.TestCase): def run_checker(self, image_bytes, *extra_args, artifacts=None): temp_dir = tempfile.TemporaryDirectory() self.addCleanup(temp_dir.cleanup) directory = Path(temp_dir.name) image = directory / "firmware.elf" manifest = directory / "firmware.capabilities.json" image.write_bytes(image_bytes) artifact_args = [] for option, data in (artifacts or {}).items(): path = directory / option path.write_bytes(data) artifact_args.extend(["--" + option, str(path)]) result = subprocess.run( [ sys.executable, str(CHECKER), "--image", str(image), "--output", str(manifest), "--target", "test_target", "--platform", "ESP32_PLATFORM", "--build-profile", "auto", *extra_args, *artifact_args, ], capture_output=True, text=True, check=False, ) return result, json.loads(manifest.read_text()) def test_writes_verified_manifest(self): result, manifest = self.run_checker( b"prefix retry.preset suffix", "--capability", "cli.retry_preset", "--expect", "cli.retry_preset=retry.preset", ) self.assertEqual(result.returncode, 0, result.stderr) self.assertTrue(manifest["verified"]) self.assertEqual(manifest["build_profile"], "auto") def test_fails_when_promised_marker_is_missing(self): result, manifest = self.run_checker( b"unrelated image", "--expect", "web.webconfig=start webconfig", ) self.assertEqual(result.returncode, 1) self.assertFalse(manifest["verified"]) self.assertIn("promised", result.stderr) def test_gzip_asset_requires_complete_validated_stream(self): page = gzip.compress(b"MeshCore firmware update", mtime=0) for image, accepted in [(b"ELF" + page + b"symbols", True), (page[:-1], False), (page[:-8] + b"\x00" * 8, False), (b"MeshCore firmware update", False), (gzip.compress(b"different page"), False), (gzip.compress(b"MeshCore firmware update" + b"x" * 1048576), False)]: with self.subTest(accepted=accepted, size=len(image)): result, manifest = self.run_checker( image, "--expect-gzip", "ota.update.wifi=MeshCore firmware update") self.assertEqual(result.returncode, 0 if accepted else 1, result.stderr) self.assertEqual(manifest["verified"], accepted) self.assertEqual(manifest["verification"][0]["source"], "linked gzip asset") def test_application_gate_rejects_elf_only_logging_evidence(self): # Debug/symbol strings in the ELF must not qualify an image whose # uploadable binary has had the implementation compiled or linked out. result, manifest = self.run_checker( b"USB logger", "--expect-application", "logging.usb.packets=USB logger", artifacts={"firmware-bin": b"no logging implementation"}) self.assertEqual(result.returncode, 1, result.stderr) self.assertFalse(manifest["verified"]) self.assertIn("packaged application", result.stderr) def test_application_gate_accepts_packaged_code_without_elf_marker(self): result, manifest = self.run_checker( b"stripped symbols", "--expect-application", "logging.usb.packets=USB logger", artifacts={"firmware-bin": b"USB logger"}) self.assertEqual(result.returncode, 0, result.stderr) self.assertTrue(manifest["verified"]) self.assertIn("logging.usb.packets", manifest["capabilities"]) self.assertEqual(manifest["verification"][0]["source"], "packaged application") def test_full_app_wifi_gate_rejects_missing_read_or_save_operation(self): operations = { "get_enabled": "get wifi.enabled", "set_enabled": "set wifi.enabled ", "get_ssid": "> (not set)", "set_ssid": "OK - WiFi SSID saved", "get_password": "get wifi.pwd", "set_password": "OK - WiFi password saved", "get_status": "wifi.status", "get_ip": "get wifi.ip", } arguments = [] for operation, marker in operations.items(): arguments += ["--expect-application", f"companion.app_wifi.{operation}={marker}"] # A complete ELF must not hide a missing operation in the shipped BIN. linked = "\0".join(operations.values()).encode() for missing in [None, *operations]: application = "\0".join(marker for operation, marker in operations.items() if operation != missing).encode() with self.subTest(missing=missing): result, manifest = self.run_checker( linked, *arguments, artifacts={"firmware-bin": application}) self.assertEqual(result.returncode, 0 if missing is None else 1, result.stderr) self.assertEqual(manifest["verified"], missing is None) def test_full_logging_requires_packet_code_as_well_as_the_setting(self): expectations = ["--expect-application", "logging.usb.control=usb.logging", "--expect-application", "logging.usb.packets=packet logger"] for body, accepted in [(b"usb.logging debug logger", False), (b"packet logger", False), (b"usb.logging packet logger", True)]: with self.subTest(body=body): result, manifest = self.run_checker(b"", *expectations, artifacts={"firmware-bin": body}) self.assertEqual(result.returncode, 0 if accepted else 1, result.stderr) self.assertEqual(manifest["verified"], accepted) def test_application_checks_require_the_installable_artifact(self): result, manifest = self.run_checker( b"USB logger", "--expect-application", "logging.usb.packets=USB logger") self.assertEqual(result.returncode, 2) self.assertFalse(manifest["verified"]) self.assertIn("require --firmware-bin or --dfu-package", result.stderr) def test_nrf52_logging_is_checked_inside_the_dfu_application(self): for body, accepted in [(b"packet logger", True), (b"no logger", False)]: archive = io.BytesIO() with zipfile.ZipFile(archive, "w") as package: package.writestr("manifest.json", json.dumps({"manifest": { "application": {"bin_file": "app.bin"}}})) package.writestr("app.bin", body) # A marker elsewhere in the ZIP is not proof of running code. package.writestr("notes.txt", "packet logger") result, manifest = self.run_checker( b"packet logger", "--platform", "NRF52_PLATFORM", "--expect-application", "logging.usb.packets=packet logger", artifacts={"dfu-package": archive.getvalue()}) self.assertEqual(result.returncode, 0 if accepted else 1, result.stderr) self.assertEqual(manifest["verified"], accepted) def ota_artifacts(self, *, second_slot=True, image_size=48): def entry(kind, subtype, address, size): return struct.pack("