#!/usr/bin/env python3 import importlib.util import json from pathlib import Path import tempfile import unittest import subprocess import struct import sys import hashlib from copy import deepcopy from unittest.mock import patch import zipfile ROOT = Path(__file__).resolve().parents[1] spec = importlib.util.spec_from_file_location("package_cascade_release", ROOT / "scripts/package_cascade_release.py") package = importlib.util.module_from_spec(spec) spec.loader.exec_module(package) from motalib import ENDF_LEN, FwIdent, Nrf52Layout, ensure_endf, ensure_nrf52_layout, target_id_for_env class ReleaseQualificationTest(unittest.TestCase): def test_portable_exclusions_require_a_complete_summary(self): self.assertEqual(package.portable_profile_exclusions({}), []) with tempfile.TemporaryDirectory() as temp: log = Path(temp) / "matrix.log" status = {"log": str(log)} header = "2 standard ESP32 target(s) exceeded the portable OTA slot and were deferred to the expanded FULL pass:\n" for body in ("DEFERRED: one (standard)\n", header + " one\n", header + " one\n one\n"): log.write_text(body) with self.assertRaisesRegex(ValueError, "exclusion summary"): package.portable_profile_exclusions(status) log.write_text(header + " one\n two\nLogging matrix completed successfully.\n") self.assertEqual([r["target"] for r in package.portable_profile_exclusions(status)], ["one", "two"]) def test_partial_matrix_requires_completion_and_an_explicit_opt_in(self): self.assertEqual(package.completed_matrix_failures({"state": "completed", "exit_code": "0"}), []) for state, code in (("running", "0"), ("starting", "0"), ("failed", "143")): with self.subTest(state=state, code=code), self.assertRaisesRegex(ValueError, "finished matrix"): package.completed_matrix_failures({"state": state, "exit_code": code}, True) with tempfile.TemporaryDirectory() as temp: log = Path(temp) / "matrix.log" status = {"state": "failed", "exit_code": "1", "log": str(log)} with self.assertRaisesRegex(ValueError, "not completed successfully"): package.completed_matrix_failures(status) for body in ("", "Building a target\n", "Logging matrix completed with 2 failed build(s):\n one (standard) -> /tmp/one.log\n"): log.write_text(body) with self.assertRaisesRegex(ValueError, "summary"): package.completed_matrix_failures(status, True) log.write_text("Logging matrix completed with 2 failed build(s):\n" " one (standard) -> /tmp/one.log\n" " two (full-usb-wifi-ota) -> /tmp/two.log\n") failures = package.completed_matrix_failures(status, True) self.assertEqual([f["target"] for f in failures], ["one", "two"]) self.assertEqual(failures[1]["profile"], "full-usb-wifi-ota") self.assertEqual(failures[1]["log_file"], "two.log") def manifest(self, target="test_repeater", **changes): return {"target": target, "platform": "ESP32_PLATFORM", "schema_version": 2, "verified": True, "ota_update_verified": True, "verification": [], **changes} def test_source_capability_cannot_qualify_infrastructure(self): with self.assertRaisesRegex(ValueError, "wireless updater"): package.validate_manifest(self.manifest(ota_update_verified=False)) def test_pending_hardware_qualification_cannot_be_published(self): for platform in ('NRF52_PLATFORM', 'ESP32_PLATFORM'): manifest = self.manifest(platform=platform, capabilities=['hardware.qualification.pending']) with self.subTest(platform=platform), self.assertRaisesRegex(ValueError, 'hardware qualification pending'): package.validate_manifest(manifest) def test_usb_companion_is_accepted(self): package.validate_manifest(self.manifest("test_companion_radio_usb", ota_update_verified=False)) def test_nrf52_sensor_profiles_require_actual_qualified_lora_receiver(self): for role in ('repeater', 'room_server', 'sensor'): for sensor in ('full', 'reduced'): manifest = self.manifest('test_' + role, platform='NRF52_PLATFORM', build_profile='auto', capabilities=['sensor.profile.' + sensor, 'ota.update.lora'], ota_update_methods=['lora'], verification=[dict(capability='ota.update.lora', present=True, source='linked image')]) package.validate_manifest(manifest) self.assertEqual(package.nrf52_sensor_profile(manifest), sensor) self.assertIn('sensors + LoRa OTA', package.release_profile_label(manifest)) self.assertEqual(package.category({'manifest': manifest}), 'lora-ota') for change in (dict(platform='ESP32_PLATFORM'), dict(target='test_companion_radio_full'), dict(ota_update_methods=['bluetooth']), dict(verification=[]), dict(verification=[dict(capability='ota.update.lora', present=True, source='filename')])): with self.subTest(change=change), self.assertRaisesRegex(ValueError, 'qualified nRF52'): package.validate_manifest({**manifest, **change}) with self.assertRaisesRegex(ValueError, 'ambiguous'): package.validate_manifest({**manifest, 'capabilities': manifest['capabilities'] + ['sensor.profile.' + ('reduced' if sensor == 'full' else 'full')]}) def test_full_companion_requires_linked_source_implementation(self): manifest = self.manifest("test_companion_radio_full", ota_update_verified=False) with self.assertRaisesRegex(ValueError, "MOTA sending"): package.validate_manifest(manifest) manifest["verification"] = [{"capability": name, "present": True} for name in ( "companion.usb_mota_source", "companion.mota_sender", "companion.temp_radio", "companion.ota_cli", "companion.wifi_ota_seeder")] package.validate_manifest(manifest) manifest["verification"][1]["present"] = False with self.assertRaisesRegex(ValueError, "MOTA sending"): package.validate_manifest(manifest) def sensor_record(self, directory, sensor, *, target='test_repeater', target_id=None, hw_id='TEST', app_base=0x26000, flags=0x10, fw_version=0x01110107, version='v1.17.1.7-dev-deadbeef', storage='internal_flash_and_retained_ram', extra_application=b'', artifact_variant=''): artifact_target = target + artifact_variant + '-' + sensor + '-ota' stem = artifact_target + '-' + version application, _ = ensure_endf(ensure_nrf52_layout(b'OTA: status\0invalid in-place patch geometry\0' + extra_application, Nrf52Layout(app_base, 0xED000, 0xED000, flags)), FwIdent(fw_version, target_id_for_env(target) if target_id is None else target_id, hw_id)) path = directory / (stem + '.zip') with zipfile.ZipFile(path, 'w') as archive: archive.writestr('application.bin', application) archive.writestr('manifest.json', json.dumps({'manifest': { 'application': {'bin_file': 'application.bin', 'dat_file': 'application.dat'}}})) archive.writestr('application.dat', b'fixture init') manifest = self.manifest(target, artifact_target=artifact_target, platform='NRF52_PLATFORM', build_profile='auto', capabilities=['sensor.profile.' + sensor, 'ota.update.lora'], ota_update_methods=['lora'], verification=[dict(capability='ota.update.lora', present=True, source='linked image')], ota_update_requirements={'lora': {'storage': storage}}) return {'manifest': manifest, 'files': [path]} def sensor_package_fixture(self, directory, target, sensor, *, external=False, merged_bridge=False, extra_capabilities=()): """Write fictional, structurally valid packages for the real CLI path.""" record = self.sensor_record(directory, sensor, target=target, version='v1.17.1.5-halo-keymind-cascade-dev-deadbeef', flags=0x04 if external else 0x10, storage='external_qspi' if external else 'internal_flash_and_retained_ram', extra_application=b'meshcore.capability.rs232.merged.v1\0' if merged_bridge else b'') manifest = record['manifest'] manifest['capabilities'].extend(extra_capabilities) if merged_bridge: for capability, source, evidence in ( ('bridge.rs232', 'linked image', '_ZN11RS232Bridge5beginEv'), ('bridge.rs232.merged', 'packaged application', 'meshcore.capability.rs232.merged.v1')): manifest['capabilities'].append(capability) manifest['verification'].append(dict(capability=capability, source=source, present=True, evidence=evidence)) path = record['files'][0] with zipfile.ZipFile(path) as archive: application = archive.read('application.bin') self.assertLessEqual(len(application), 256) uf2 = (struct.pack('<8I', 0x0A324655, 0x9E5D5157, 0x2000, 0x26000, 256, 0, 1, 0xADA52840) + application.ljust(256, b'\xff') + b'\0' * 220 + struct.pack(' /tmp/missing.log\n") status.write_text("state=failed\n" + settings.replace("exit_code=0", "exit_code=1") + f"log={log}\n") partial_command = command.copy() partial_command[partial_command.index("--output") + 1] = str(directory / "partial") result = subprocess.run(partial_command, capture_output=True, text=True) self.assertNotEqual(result.returncode, 0) self.assertFalse((directory / "partial").exists()) result = subprocess.run(partial_command + ["--allow-partial"], capture_output=True, text=True) self.assertEqual(result.returncode, 0, result.stderr) plan = json.loads((directory / "partial/release-plan.json").read_text()) self.assertEqual(plan["matrix"]["exit_code"], 1) self.assertEqual(plan["matrix"]["failures"][0]["target"], "missing_repeater") assets = directory / "partial/companion" self.assertIn("Partial matrix", (assets / "BUILD-NOTES.txt").read_text()) self.assertIn("missing_repeater", (assets / "BUILD-FAILURES.md").read_text()) self.assertEqual(plan["matrix"]["portable_profile_exclusions"][0]["target"], "large_repeater") self.assertIn("large_repeater", (assets / "PORTABLE-PROFILE-EXCLUSIONS.md").read_text()) self.assertIn("Portable image limits", (assets / "BUILD-NOTES.txt").read_text()) self.assertEqual(len(list(assets.iterdir())), plan["groups"][0]["asset_count"]) checksums = (assets / "SHA256SUMS.txt").read_text() self.assertIn("BUILD-FAILURES.json", checksums) self.assertIn("PORTABLE-PROFILE-EXCLUSIONS.json", checksums) for line in checksums.splitlines(): digest, name = line.split(" ", 1) self.assertEqual(hashlib.sha256((assets / name).read_bytes()).hexdigest(), digest) # Opting in to holes never authorizes publishing bad firmware. (inputs / (stem + ".bin")).write_bytes(b"corrupted") partial_command[partial_command.index("--output") + 1] = str(directory / "bad") result = subprocess.run(partial_command + ["--allow-partial"], capture_output=True, text=True) self.assertNotEqual(result.returncode, 0) self.assertFalse((directory / "bad").exists()) if __name__ == "__main__": unittest.main()