- 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。未执行实机烧录。
122 lines
5.8 KiB
Python
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()
|