Prepare 1.17.1.5 USA Cascade release and verify update capabilities
This commit is contained in:
1 parent
8995675e1c
commit
a2c3761700
12 files changed
+813
-9
No files matched your search
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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"
|
||||
Reference in new issue
Block a user