Files
simba-rmic-2026/scripts/test-package-firmware.py
T
sloray 054379dc64
构建客户端 / 检查(类型 + 单元测试) (push) Canceled after 0s
构建客户端 / Linux x64(deb + AppImage) (push) Canceled after 0s
构建客户端 / Windows x64(NSIS 安装包) (push) Canceled after 0s
构建客户端 / macOS 通用(.app + .dmg) (push) Canceled after 0s
fix(packaging): 修复 Windows 安装与固件打包并完善客户端文案
- fix(installer): 修复低完整性产物无法写入临时目录的问题,并在 CI 打包前后检查完整性标签
- fix(firmware): 从本次 Cargo ELF 打包并同步配套镜像,修正短目录说明与 LLVM 搜索顺序
- fix(installer): 修复中文完成页和页眉遮挡,侧栏使用等比缩放的原始 Logo
- style(client): 应用图标增加圆角,统一“裁判系统客户端”命名
- fix(config): 安装提示改用可选本地热点配置,排除 Wi-Fi 凭据
- docs: 润色项目说明并统一裁判系统术语
- test(packaging): 补充 Windows 完整性和固件打包回归,覆盖外部输出目录、旧镜像与重复生成

验证:Windows NSIS 构建及原生页面预览;通用与本地安装配置编译;固件 release 构建、重复打包和客户端离线校验;6 项固件打包回归与 Windows 权限回归;git diff --check。未执行实机烧录。
2026-10-21 09:25:17 +08:00

122 lines
5.8 KiB
Python

#!/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()