Prepare 1.17.1.5 USA Cascade release and verify update capabilities

This commit is contained in:
mikecarper committed 2026-09-05 08:59:50 -07:00
1 parent 8995675e1c
commit a2c3761700
12 files changed
+813 -9

No files matched your search

+54
View File
@@ -0,0 +1,54 @@
#!/usr/bin/env python3
import importlib.util
import json
from pathlib import Path
import tempfile
import unittest
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)
class ReleaseQualificationTest(unittest.TestCase):
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_usb_companion_is_accepted(self):
package.validate_manifest(self.manifest("test_companion_radio_usb", ota_update_verified=False))
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 test_missing_qualification_and_mixed_versions_are_rejected(self):
with tempfile.TemporaryDirectory() as temp:
directory = Path(temp)
stem = "test_companion_radio_usb-v1.0-deadbeef"
(directory / (stem + ".bin")).write_bytes(b"firmware")
with self.assertRaisesRegex(ValueError, "without qualification"):
package.collect_artifacts(directory, "v1.0-deadbeef")
(directory / (stem + ".capabilities.json")).write_text(json.dumps(self.manifest("test_companion_radio_usb")))
with self.assertRaisesRegex(ValueError, "mixed-version"):
package.collect_artifacts(directory, "v2.0-deadbeef")
with self.assertRaisesRegex(ValueError, "pair incomplete"):
package.collect_artifacts(directory, "v1.0-deadbeef")
if __name__ == "__main__":
unittest.main()
+73 -1
View File
@@ -6,6 +6,9 @@ import subprocess
import sys
import tempfile
import unittest
import struct
import io
import zipfile
ROOT = Path(__file__).resolve().parents[1]
@@ -13,13 +16,18 @@ CHECKER = ROOT / "scripts" / "check_firmware_capabilities.py"
class FirmwareCapabilityCheckerTest(unittest.TestCase):
def run_checker(self, image_bytes, *extra_args):
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,
@@ -35,6 +43,7 @@ class FirmwareCapabilityCheckerTest(unittest.TestCase):
"--build-profile",
"auto",
*extra_args,
*artifact_args,
],
capture_output=True,
text=True,
@@ -64,6 +73,69 @@ class FirmwareCapabilityCheckerTest(unittest.TestCase):
self.assertFalse(manifest["verified"])
self.assertIn("promised", result.stderr)
def ota_artifacts(self, *, second_slot=True, image_size=48):
def entry(kind, subtype, address, size):
return struct.pack("<HBBII16sI", 0x50AA, kind, subtype,
address, size, b"test", 0)
table = entry(1, 0, 0xe000, 0x2000) + entry(0, 0x10, 0x10000, 64)
if second_slot:
table += entry(0, 0x11, 0x20000, 64)
return {"firmware-bin": b"x" * image_size, "partitions": table}
def test_seeder_does_not_count_as_self_update(self):
result, manifest = self.run_checker(
b"ota folder on", "--require-ota", "--expect",
"companion.usb_mota_source=ota folder on")
self.assertEqual(result.returncode, 1)
self.assertFalse(manifest["verified"])
self.assertEqual(manifest["ota_update_methods"], [])
def test_wifi_update_requires_two_slots(self):
result, manifest = self.run_checker(
b"uploader", "--require-ota", "--expect", "ota.update.wifi=uploader",
artifacts=self.ota_artifacts(second_slot=False))
self.assertEqual(result.returncode, 1)
self.assertIn("ota_1", manifest["ota_update_evidence"])
def test_wifi_update_must_fit_both_slots(self):
result, manifest = self.run_checker(
b"uploader", "--require-ota", "--expect", "ota.update.wifi=uploader",
artifacts=self.ota_artifacts(image_size=65))
self.assertEqual(result.returncode, 1)
self.assertFalse(manifest["ota_update_verified"])
def test_verified_wifi_update(self):
result, manifest = self.run_checker(
b"uploader", "--require-ota", "--expect", "ota.update.wifi=uploader",
artifacts=self.ota_artifacts())
self.assertEqual(result.returncode, 0, result.stderr)
self.assertTrue(manifest["ota_update_verified"])
self.assertEqual(manifest["ota_update_methods"], ["wifi"])
def test_bluetooth_requires_a_real_dfu_package(self):
result, manifest = self.run_checker(
b"dfu", "--platform", "NRF52_PLATFORM", "--require-ota", "--expect",
"ota.update.bluetooth=dfu")
self.assertEqual(result.returncode, 1)
archive = io.BytesIO()
with zipfile.ZipFile(archive, "w") as package:
package.writestr("manifest.json", json.dumps({"manifest": {
"application": {"bin_file": "app.bin", "dat_file": "app.dat"}}}))
package.writestr("app.bin", b"firmware")
package.writestr("app.dat", b"init packet")
result, manifest = self.run_checker(
b"dfu", "--platform", "NRF52_PLATFORM", "--require-ota", "--expect",
"ota.update.bluetooth=dfu", artifacts={"dfu-package": archive.getvalue()})
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(manifest["ota_update_methods"], ["bluetooth"])
def test_usb_companion_can_report_no_self_update(self):
result, manifest = self.run_checker(
b"ota folder on", "--expect", "companion.usb_mota_source=ota folder on")
self.assertEqual(result.returncode, 0, result.stderr)
self.assertTrue(manifest["verified"])
self.assertFalse(manifest["ota_update_verified"])
if __name__ == "__main__":
unittest.main()
+41
View File
@@ -0,0 +1,41 @@
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")/.."
source build.sh
fail() { echo "test_release_ota_policy: $*" >&2; exit 1; }
# Exercise release resolution without starting PlatformIO. This inventory
# includes a personal USB Companion and infrastructure on the same platform.
PIO_ENV_PLATFORM_BY_NAME[nrf_repeater]=NRF52_PLATFORM
PIO_ENV_PLATFORM_BY_NAME[esp_companion_radio_full]=ESP32_PLATFORM
PIO_ENV_PLATFORM_BY_NAME[rp_companion_radio_usb]=RP2040_PLATFORM
PIO_ENV_PLATFORM_BY_NAME[rp_repeater]=RP2040_PLATFORM
PIO_ENV_PLATFORM_BY_NAME[stm_sensor]=STM32_PLATFORM
resolve_bulk_command_targets() {
RESOLVED_BUILD_TARGETS=(nrf_repeater esp_companion_radio_full
rp_companion_radio_usb rp_repeater stm_sensor)
}
sort_build_targets_by_platform_and_name() { printf '%s\n' "$@"; }
validate_build_target() { return 0; }
REQUIRE_OTA_UPDATES=""
resolve_command_targets build-firmwares-logging-matrix >/dev/null
[ "$REQUIRE_OTA_UPDATES" = 1 ] || fail "option 3 did not require infrastructure OTA"
[ "${#RESOLVED_BUILD_TARGETS[@]}" = 3 ] || fail "wrong release inventory"
[[ " ${RESOLVED_BUILD_TARGETS[*]} " == *" rp_companion_radio_usb "* ]] \
|| fail "USB-updated Companion was excluded"
[ "${#OTA_EXCLUDED_TARGETS[@]}" = 2 ] || fail "missing exclusion report"
parse_cli_options build-firmwares-logging-matrix --allow-no-ota
resolve_command_targets "${PARSED_COMMAND_ARGS[@]}" >/dev/null
[ "${#RESOLVED_BUILD_TARGETS[@]}" = 5 ] || fail "development override lost targets"
parse_cli_options build-firmware rp_repeater --require-ota
if resolve_command_targets "${PARSED_COMMAND_ARGS[@]}" >/dev/null; then
fail "explicit cable-only infrastructure build passed OTA requirement"
fi
resolve_command_targets build-firmware rp_companion_radio_usb >/dev/null
[ "${#RESOLVED_BUILD_TARGETS[@]}" = 1 ] || fail "explicit USB Companion was rejected"
echo "test_release_ota_policy: OK"