File
Blob: scripts/test_build_firmware.py
| 1 | """Reject unsupported resolved SDK settings before compiling a radio image.""" |
| 2 | import importlib.util |
| 3 | import json |
| 4 | from pathlib import Path |
| 5 | import tempfile |
| 6 | from types import SimpleNamespace |
| 7 | import unittest |
| 8 | from unittest.mock import patch |
| 9 | |
| 10 | spec = importlib.util.spec_from_file_location( |
| 11 | "build_firmware", Path(__file__).with_name("build_firmware.py") |
| 12 | ) |
| 13 | build_firmware = importlib.util.module_from_spec(spec) |
| 14 | spec.loader.exec_module(build_firmware) |
| 15 | |
| 16 | SUPPORTED_CONFIGURATION = { |
| 17 | "COMPILER_OPTIMIZATION_PERF": True, |
| 18 | "COMPILER_OPTIMIZATION_ASSERTIONS_ENABLE": True, |
| 19 | "SPIRAM_MALLOC_ALWAYSINTERNAL": 1024, |
| 20 | "MBEDTLS_HARDWARE_AES": True, |
| 21 | "MBEDTLS_HARDWARE_SHA": True, |
| 22 | "MBEDTLS_HARDWARE_MPI": True, |
| 23 | } |
| 24 | |
| 25 | |
| 26 | class BuildConfigurationTests(unittest.TestCase): |
| 27 | def test_firmware_uses_the_canonical_device_token(self): |
| 28 | with tempfile.TemporaryDirectory() as directory: |
| 29 | root = Path(directory) |
| 30 | build = root / "build" |
| 31 | (build / "config").mkdir(parents=True) |
| 32 | (build / "config/sdkconfig.json").write_text(json.dumps(SUPPORTED_CONFIGURATION)) |
| 33 | (root / ".credential.env").write_text( |
| 34 | 'WIFI_SSID="synthetic network"\nWIFI_PASSWORD="synthetic password"\n' |
| 35 | f'DEVICE_TOKEN="{"c" * 43}"\n' |
| 36 | ) |
| 37 | (root / "worker").mkdir() |
| 38 | (root / "worker/.dev.vars").write_text(f'DEVICE_TOKEN="{"s" * 43}"\n') |
| 39 | with ( |
| 40 | patch.object(build_firmware, "ROOT", root), |
| 41 | patch.object(build_firmware, "build_dir", return_value=build), |
| 42 | patch.object(build_firmware, "configured_sdk_root", return_value=root / "sdk"), |
| 43 | patch.object(build_firmware, "verify_checkout"), |
| 44 | patch.object(build_firmware, "idf_environment", return_value=("python3", {})), |
| 45 | patch.object(build_firmware.subprocess, "check_output", return_value=f"rustc ({build_firmware.RUST_RELEASE})"), |
| 46 | patch.object(build_firmware.subprocess, "run", return_value=SimpleNamespace(returncode=0)), |
| 47 | patch("sys.argv", ["build_firmware.py", "--signaling-url", "https://radio.example.com"]), |
| 48 | ): |
| 49 | with self.assertRaises(SystemExit) as result: |
| 50 | build_firmware.main() |
| 51 | self.assertEqual(result.exception.code, 0) |
| 52 | header = (build / "private/signaling_private.h").read_text() |
| 53 | self.assertIn('"' + "\\x63" * 43 + '"', header) |
| 54 | self.assertNotIn("\\x73" * 43, header) |
| 55 | |
| 56 | def check_configuration(self, config): |
| 57 | with tempfile.TemporaryDirectory() as directory: |
| 58 | build = Path(directory) |
| 59 | (build / "config").mkdir() |
| 60 | (build / "config/sdkconfig.json").write_text(json.dumps(config)) |
| 61 | build_firmware.require_supported_sdk_configuration(build) |
| 62 | |
| 63 | def test_supported_s3_configuration_is_accepted(self): |
| 64 | self.check_configuration(SUPPORTED_CONFIGURATION) |
| 65 | |
| 66 | def test_stale_debug_configuration_is_rejected(self): |
| 67 | config = {**SUPPORTED_CONFIGURATION, "COMPILER_OPTIMIZATION_PERF": False} |
| 68 | with self.assertRaisesRegex(SystemExit, "-O2"): |
| 69 | self.check_configuration(config) |
| 70 | |
| 71 | def test_disabled_assertions_are_rejected(self): |
| 72 | config = { |
| 73 | **SUPPORTED_CONFIGURATION, |
| 74 | "COMPILER_OPTIMIZATION_ASSERTIONS_ENABLE": False, |
| 75 | } |
| 76 | with self.assertRaisesRegex(SystemExit, "assertions"): |
| 77 | self.check_configuration(config) |
| 78 | |
| 79 | def test_stale_psram_threshold_is_rejected(self): |
| 80 | config = {**SUPPORTED_CONFIGURATION, "SPIRAM_MALLOC_ALWAYSINTERNAL": 16384} |
| 81 | with self.assertRaisesRegex(SystemExit, "1 KiB PSRAM"): |
| 82 | self.check_configuration(config) |
| 83 | |
| 84 | def test_disabled_hardware_crypto_is_rejected(self): |
| 85 | for setting in ( |
| 86 | "MBEDTLS_HARDWARE_AES", |
| 87 | "MBEDTLS_HARDWARE_SHA", |
| 88 | "MBEDTLS_HARDWARE_MPI", |
| 89 | ): |
| 90 | with self.subTest(setting=setting): |
| 91 | config = {**SUPPORTED_CONFIGURATION, setting: False} |
| 92 | with self.assertRaisesRegex(SystemExit, "hardware AES/SHA/MPI"): |
| 93 | self.check_configuration(config) |
| 94 | |
| 95 | def test_missing_resolved_configuration_has_an_actionable_error(self): |
| 96 | with tempfile.TemporaryDirectory() as directory: |
| 97 | with self.assertRaisesRegex(SystemExit, "did not produce a readable"): |
| 98 | build_firmware.require_supported_sdk_configuration(Path(directory)) |
| 99 | |
| 100 | def test_malformed_resolved_configuration_has_an_actionable_error(self): |
| 101 | with tempfile.TemporaryDirectory() as directory: |
| 102 | build = Path(directory) |
| 103 | (build / "config").mkdir() |
| 104 | (build / "config/sdkconfig.json").write_text("{") |
| 105 | with self.assertRaisesRegex(SystemExit, "invalid resolved"): |
| 106 | build_firmware.require_supported_sdk_configuration(build) |
| 107 | |
| 108 | def test_resolved_configuration_must_be_an_object(self): |
| 109 | with tempfile.TemporaryDirectory() as directory: |
| 110 | build = Path(directory) |
| 111 | (build / "config").mkdir() |
| 112 | (build / "config/sdkconfig.json").write_text("[]") |
| 113 | with self.assertRaisesRegex(SystemExit, "JSON object"): |
| 114 | build_firmware.require_supported_sdk_configuration(build) |
| 115 | |
| 116 | |
| 117 | if __name__ == "__main__": |
| 118 | unittest.main() |