同步移动端工程、设备控制改进与发布资料

This commit is contained in:
2026-09-26 19:21:12 +08:00
parent 912ba432cf
commit 98eadc5c8b
137 changed files with 9274 additions and 85 deletions
@@ -1,7 +1,14 @@
import hashlib
from pathlib import Path
import subprocess
import pytest
from app.system import kernel_recovery as kr
from app.ota.manager import OtaManager
from app.ota.state import write_json, write_last_result
from app.ota.versioning import SoftwareVersion
from scripts.ota_worker import UpdateFailed, verify_kernel_capability_retained
from scripts.axp313a_kernel_health import (
HealthError,
check_cpufreq,
@@ -11,6 +18,159 @@ from scripts.axp313a_kernel_health import (
from scripts.render_dual_kernel_boot import BootScriptError, render
def make_recovery(tmp_path: Path, monkeypatch) -> kr.KernelRecovery:
host = tmp_path / "host"
data = tmp_path / "data"
boot = host / "boot"
boot.mkdir(parents=True)
files = {
"Image-matrix-axp313a1": b"candidate-image",
"sun50i-h616-walnutpi-1b-matrix-axp313a1.dtb": b"candidate-sd-dtb",
"sun50i-h616-walnutpi-1b-emmc-matrix-axp313a1.dtb": b"candidate-emmc-dtb",
}
monkeypatch.setattr(kr, "BOOT_HASHES", {name: hashlib.sha256(value).hexdigest() for name, value in files.items()})
for name, value in files.items():
(boot / name).write_bytes(value)
for name in ("Image", "sun50i-h616-walnutpi-1b.dtb", "sun50i-h616-walnutpi-1b-emmc.dtb",
"boot.cmd.matrix-original", "boot.scr.matrix-original", "matrix-original.SHA256SUMS"):
(boot / name).write_bytes(b"original")
(boot / "boot.cmd").write_text(
"matrix-kernel-good matrix-kernel-good matrix_kernel_candidate=1",
encoding="utf-8",
)
(boot / "boot.scr").write_bytes(b"matrix_kernel_candidate=1 Image-matrix-axp313a1")
health = host / "etc/systemd/system/matrix-axp313a-health.service"
health.parent.mkdir(parents=True)
health.write_text("ConditionKernelCommandLine=matrix_kernel_candidate=1", encoding="utf-8")
script = host / "opt/matrix-screen-controller-system/axp313a_kernel_health.py"
script.parent.mkdir(parents=True)
script.write_text("health", encoding="utf-8")
enabled = host / "etc/systemd/system/multi-user.target.wants/matrix-axp313a-health.service"
enabled.parent.mkdir(parents=True)
enabled.write_text("enabled", encoding="utf-8")
modules = host / "lib/modules" / kr.CANDIDATE_RELEASE
(modules / "kernel").mkdir(parents=True)
(modules / "modules.dep").write_text("index", encoding="ascii")
module = modules / "kernel/example.ko"
module.write_bytes(b"module")
digest = hashlib.sha256(b"kernel/example.ko\0" + hashlib.sha256(b"module").digest()).hexdigest()
monkeypatch.setattr(kr, "MODULE_COUNT", 1)
monkeypatch.setattr(kr, "MODULE_DIGEST", digest)
release = host / "proc/sys/kernel/osrelease"
release.parent.mkdir(parents=True)
release.write_text("6.1.31", encoding="ascii")
boot_id = host / "proc/sys/kernel/random/boot_id"
boot_id.parent.mkdir(parents=True)
boot_id.write_text("old-boot", encoding="ascii")
return kr.KernelRecovery(data, host_root=host)
def test_candidate_validation_and_single_recovery_attempt(tmp_path, monkeypatch):
recovery = make_recovery(tmp_path, monkeypatch)
assert recovery.verify_candidate() is None
assert recovery.prepare_attempt("1.2.0:time") is True
assert recovery.prepare_attempt("1.2.0:time") is False
monkeypatch.setattr(kr.subprocess, "run", lambda *args, **kwargs: subprocess.CompletedProcess(args, 1))
recovery.arm_and_reboot(reboot=False)
assert recovery.status()["state"] == "pending"
with pytest.raises(RuntimeError, match="no unused"):
recovery.arm_and_reboot(reboot=False)
recovery.host("/proc/sys/kernel/random/boot_id").write_text("new-boot", encoding="ascii")
recovery.host("/proc/sys/kernel/osrelease").write_text(kr.CANDIDATE_RELEASE, encoding="ascii")
policy = recovery.host("/sys/devices/system/cpu/cpufreq/policy0")
policy.mkdir(parents=True)
(policy / "scaling_governor").write_text("schedutil", encoding="ascii")
(policy / "scaling_available_governors").write_text("schedutil performance", encoding="ascii")
recovery.finalize_after_boot(wait_seconds=1)
assert recovery.status()["state"] == "succeeded"
def test_recovery_failure_and_damaged_candidate_do_not_retry(tmp_path, monkeypatch):
recovery = make_recovery(tmp_path, monkeypatch)
recovery.host("/boot/Image-matrix-axp313a1").write_bytes(b"damaged")
assert recovery.prepare_attempt("1.2.0:time") is False
assert recovery.status()["state"] == "requires_package"
assert recovery.prepare_attempt("1.2.1:later") is False
other = make_recovery(tmp_path / "other", monkeypatch)
assert other.prepare_attempt("1.2.0:time") is True
monkeypatch.setattr(kr.subprocess, "run", lambda *args, **kwargs: subprocess.CompletedProcess(args, 1))
other.arm_and_reboot(reboot=False)
other.host("/proc/sys/kernel/random/boot_id").write_text("new-boot", encoding="ascii")
other.finalize_after_boot(wait_seconds=1)
assert other.status()["state"] == "failed"
assert other.prepare_attempt("1.2.1:later") is False
def test_scheduled_recovery_that_never_runs_becomes_persistent_failure(tmp_path, monkeypatch):
recovery = make_recovery(tmp_path, monkeypatch)
assert recovery.prepare_attempt("1.2.0:time") is True
monkeypatch.setattr(kr.time, "time", lambda: recovery.read_record()["recorded_at"] + 181)
assert recovery.status()["state"] == "failed"
assert recovery.read_record()["state"] == "failed"
assert recovery.prepare_attempt("1.2.1:later") is False
def test_delayed_recovery_task_does_not_reboot(tmp_path, monkeypatch):
recovery = make_recovery(tmp_path, monkeypatch)
assert recovery.prepare_attempt("1.2.0:time") is True
monkeypatch.setattr(kr.time, "time", lambda: recovery.read_record()["recorded_at"] + 181)
monkeypatch.setattr(kr.subprocess, "run", lambda *args, **kwargs: pytest.fail("late task ran a command"))
recovery.arm_and_reboot()
assert recovery.read_record()["state"] == "failed"
def test_ota_rejects_new_cpufreq_loss_and_keeps_preexisting_degradation():
before = {"release": kr.CANDIDATE_RELEASE, "available": True, "marker": True,
"policies": {"policy0": {"current": "schedutil", "performance": True}}}
after = {**before, "policies": {}, "available": False}
off = {"requested": False, "available": True, "effective": False,
"current_governors": {"policy0": "schedutil"}}
with pytest.raises(UpdateFailed, match="lost the validated"):
verify_kernel_capability_retained(before, after, off, off)
verify_kernel_capability_retained({**before, "available": False}, after, off, off)
with pytest.raises(UpdateFailed, match="governors"):
verify_kernel_capability_retained(before, before, off, {**off, "current_governors": {"policy0": "performance"}})
def test_legacy_worker_completion_schedules_lightweight_recovery(tmp_path, monkeypatch):
class Display:
def start_ota_indicator(self, _path):
pass
def stop_ota_indicator(self):
pass
class Recovery:
def __init__(self):
self.attempts = []
def status(self):
return {"state": "ready", "reason": ""}
def prepare_attempt(self, result_id):
self.attempts.append(result_id)
return len(self.attempts) == 1
recovery = Recovery()
manager = OtaManager(
code_root=tmp_path / "code", data_root=tmp_path / "data", runtime_root=tmp_path / "run",
software_version=SoftwareVersion.parse("1.2.0"), display=Display(),
)
monkeypatch.setattr(manager, "_production_recovery", lambda: recovery)
calls = []
monkeypatch.setattr("app.ota.manager.subprocess.run", lambda command, **kwargs: calls.append(command))
write_json(manager.status_path, {"schema_version": 1, "active": True, "job": {"id": "old-worker"}})
assert manager.resume_or_start_monitor(manager.schedule_kernel_recovery_after_ota)
write_last_result(manager.data_root, {"status": "success", "target_version": "1.2.0",
"installed_at": "2026-09-26T08:00:00Z"})
write_json(manager.status_path, {"schema_version": 1, "active": False, "job": {"id": "old-worker"}})
manager._monitor_thread.join(timeout=3)
assert recovery.attempts == ["1.2.0:2026-09-26T08:00:00Z"]
assert len(calls) == 1
assert "systemd-run" in calls[0]
BOOT_SOURCE = '''# DO NOT EDIT THIS FILE
setenv docker_optimizations "on"
setenv bootargs "root=/dev/mmcblk1p2"