#!/usr/bin/env python3 """固件打包回归:外部 Cargo 目录、旧镜像隔离、配套文件及 ZIP 完整性。""" import hashlib import importlib.util import json import os from pathlib import Path import subprocess import sys import tempfile import unittest from unittest.mock import patch import zipfile REPO = Path(__file__).resolve().parent.parent spec = importlib.util.spec_from_file_location("package_firmware", REPO / "scripts/package_firmware.py") package = importlib.util.module_from_spec(spec) spec.loader.exec_module(package) class PackageFirmwareTests(unittest.TestCase): def setUp(self): self.temporary = tempfile.TemporaryDirectory(prefix="rmic-package-") self.addCleanup(self.temporary.cleanup) self.root = Path(self.temporary.name) self.release = self.root / "external-target/xtensa-esp32s3-espidf/release" self.release.mkdir(parents=True) self.elf = self.release / "esp32" self.elf.write_bytes(b"current-elf") (self.release / "bootloader.bin").write_bytes(b"current-bootloader") (self.release / "partition-table.bin").write_bytes(b"\xff" * 0xC00) self.output = self.root / "new-firmware" def test_cargo_metadata_respects_external_target_directory(self): with patch.dict(os.environ, {"CARGO_TARGET_DIR": str(self.root / "external-target")}): self.assertEqual(package.resolve_elf(REPO, "").resolve(), self.elf.resolve()) def test_cargo_config_target_directory(self): # Run real metadata in an isolated crate, without compiling dependencies. crate = self.root / "crate" (crate / ".cargo").mkdir(parents=True) (crate / "src").mkdir() (crate / "src/main.rs").write_text("fn main() {}\n") (crate / "Cargo.toml").write_text('[package]\nname="fixture"\nversion="0.1.0"\n') (crate / ".cargo/config.toml").write_text( '[build]\ntarget-dir=' + json.dumps(str(self.root / "external-target")) + "\n" ) with patch.dict(os.environ): os.environ.pop("CARGO_TARGET_DIR", None) self.assertEqual(package.resolve_elf(crate, "").resolve(), self.elf.resolve()) def test_missing_elf_does_not_reuse_old_app(self): self.output.mkdir() app = self.output / "esp32s3-app.bin" app.write_bytes(b"old-app") with self.assertRaisesRegex(SystemExit, "ELF 不存在"): package.export_app(REPO, self.output, self.release / "missing", False) self.assertEqual(app.read_bytes(), b"old-app") def test_missing_companion_stops_before_export(self): (self.release / "bootloader.bin").unlink() with patch.object(package.subprocess, "run") as run: with self.assertRaisesRegex(SystemExit, "配套镜像"): package.export_app(REPO, self.output, self.elf, False) run.assert_not_called() def test_export_failure_does_not_create_archive(self): with patch.object(package.shutil, "which", return_value=sys.executable): with patch.object(package.subprocess, "run", return_value=subprocess.CompletedProcess([], 1)): with self.assertRaisesRegex(SystemExit, "save-image 失败"): package.export_app(REPO, self.output, self.elf, False) self.assertEqual(list(self.root.rglob("*.zip")), []) def test_package_replaces_newer_old_images_and_is_reproducible(self): self.output.mkdir() old_app = self.output / "esp32s3-app.bin" old_app.write_bytes(b"old-app") os.utime(old_app, (self.elf.stat().st_mtime + 100, self.elf.stat().st_mtime + 100)) (self.output / "BUILD-INFO.json").write_text( '{"verification":{"hardware_flashed_and_tested":true},"old_test_results":true}' ) original_run = subprocess.run def run(command, *args, **kwargs): if "save-image" in command: self.assertEqual(Path(command[-2]).resolve(), self.elf.resolve()) Path(command[-1]).write_bytes(b"app-from-current-elf") return subprocess.CompletedProcess(command, 0) return original_run(command, *args, **kwargs) arguments = ["package_firmware.py", "--elf", str(self.elf), "--firmware", str(self.output)] with patch.object(sys, "argv", arguments), patch.object(package.subprocess, "run", side_effect=run): with patch.object(package.shutil, "which", return_value=sys.executable): self.assertEqual(package.main(), 0) archive = next(self.output.glob("*.zip")) first = archive.read_bytes() self.assertEqual(package.main(), 0) self.assertEqual(archive.read_bytes(), first) with zipfile.ZipFile(archive) as bundle: self.assertIsNone(bundle.testzip()) self.assertEqual(len(bundle.namelist()), 5) self.assertEqual(bundle.read("esp32s3-app.bin"), b"app-from-current-elf") self.assertEqual(bundle.read("bootloader.bin"), b"current-bootloader") self.assertEqual(bundle.read("partition-table.bin"), b"\xff" * 0xC00) manifest = json.loads(bundle.read("bundle.json")) for filename, key in (("esp32s3-app.bin", "appSha256"), ("partition-table.bin", "partitionTableSha256"), ("bootloader.bin", "bootloaderSha256")): self.assertEqual(hashlib.sha256(bundle.read(filename)).hexdigest(), manifest[key]) metadata = json.loads(bundle.read("BUILD-INFO.json")) self.assertFalse(metadata["verification"]["hardware_flashed_and_tested"]) self.assertNotIn("old_test_results", metadata) digest = hashlib.sha256(first).hexdigest() self.assertEqual(archive.with_name(archive.name + ".sha256").read_text().split()[0], digest) if __name__ == "__main__": unittest.main()