Files
matrix-screen-controller/各种归档/20260907_OTA1.1.0软件安装节点验证/ota110_integration.py
T

140 lines
6.7 KiB
Python

from pathlib import Path
import importlib.util, json, os, shutil, subprocess, sys, tarfile, zipfile
ROOT = Path(__file__).resolve().parent
SEED = ROOT/'seed'
SEED.mkdir(exist_ok=True)
with zipfile.ZipFile(ROOT/'candidate.ota') as z:
with z.open('payload.tar.gz') as p, tarfile.open(fileobj=p, mode='r|gz') as t:
for m in t:
assert m.isfile() and not m.name.startswith('/') and '..' not in Path(m.name).parts
out=SEED/m.name;out.parent.mkdir(parents=True,exist_ok=True)
out.write_bytes(t.extractfile(m).read());out.chmod(m.mode)
sys.path.insert(0,str(SEED/'software'))
def module(name,path):
spec=importlib.util.spec_from_file_location(name,path)
m=importlib.util.module_from_spec(spec);sys.modules[name]=m;spec.loader.exec_module(m);return m
oldpackage=module('app.ota.legacy_package',ROOT/'baseline/package.py')
worker=module('legacy_worker',ROOT/'baseline/ota_worker.py')
worker.inspect_package=oldpackage.inspect_package
worker.extract_payload=oldpackage.extract_payload
from scripts import ota_components as c
def reset():
for p in [Path('/opt'),Path('/var/lib'),Path('/run'),Path('/usr/local/bin'),Path('/etc/systemd/system')]:
for entry in p.iterdir():
if entry.is_dir() and not entry.is_symlink(): shutil.rmtree(entry)
else: entry.unlink()
c.DATA.mkdir()
c.TARGET.mkdir()
(c.TARGET/'VERSION').write_text('1.0.3',encoding='utf-8')
c.RELEASES.mkdir()
(c.DATA/'keep.txt').write_bytes(b'user resource preserved')
for name in ('app-unit','ota-unit'):
c.FILES[name].write_bytes(('old '+name).encode())
if '--resume' not in sys.argv:
reset()
else:
assert '413 passed' in (ROOT/'validation.log').read_text(encoding='utf-8',errors='replace')
request={'schema_version':1,'job_id':'baseline103','target_version':'1.1.0','current_version':'1.0.3',
'package_path':'/opt/candidate.ota','status_path':str(c.RUNTIME/'ota-status.json'),
'accepted_at':'2026-09-07T00:00:00Z'}
c.RUNTIME.mkdir(parents=True,exist_ok=True)
(c.RUNTIME/'ota-request.json').write_text(json.dumps(request),encoding='utf-8')
shutil.copy2(ROOT/'candidate.ota',request['package_path'])
tx=worker.Transaction(request)
tx.start_visual=lambda: None
tx.stop_visual=lambda: None
actual_run=worker.run
def run(command,**kwargs):
if any('dedicated_host.py' in p for p in command):
print('Dedicated host policy: separately checked outside namespace',flush=True)
return
actual_run(command,**kwargs)
worker.run=run
if '--resume' in sys.argv:
actual_run([str(tx.release/'.venv/bin/python'),'-m','scripts.prepare_data_root','--data-root',str(tx.data_candidate),'--runtime-root',str(tx.work_root/'migration-runtime')],cwd=tx.release,env={**os.environ,'PYTHONPATH':str(tx.release)})
else:
tx.prepare()
assert c.FILES['frpc'].is_file()
assert (c.DATA/c.JOURNAL/'state.json').is_file()
tx.switch()
# Hardware/HTTP health is intentionally simulated in this isolated filesystem.
# The real worker ordering, venv, compile, pytest, migration and installation run.
tx.succeed()
c.finish()
assert (c.TARGET/'VERSION').read_text().strip()=='1.1.0'
assert (c.DATA/'keep.txt').read_bytes()==b'user resource preserved'
assert not (c.DATA/c.JOURNAL).exists()
c.check_installed(c.TARGET,'1.1.0')
print('PASS: real 1.0.3 worker -> 1.1.0, actual offline venv/tests/native compile/frpc; simulated service health',flush=True)
# Exercise idempotence and repair with the actual ARM64 shell installer.
bundle=SEED/'software/system-dependencies/frpc'
env=os.environ.copy();env['FRPC_BUNDLE']=str(bundle);env['FRPC_RUN_USER']=c.account()
installer=['/bin/sh',str(c.TARGET/'scripts/install_frpc_system.sh')]
state_path=Path('/run/fake-systemctl.json')
state_path.write_text(json.dumps({c.FRP:{'active':True,'enabled':True}}))
before=c.FILES['frpc'].stat().st_mtime_ns
subprocess.run(installer,env=env,check=True)
assert c.FILES['frpc'].stat().st_mtime_ns==before
c.FILES['frpc'].write_bytes(b'corrupt')
subprocess.run(installer,env=env,check=True)
c.check_installed(c.TARGET,'1.1.0')
c.FILES['frpc-dropin'].unlink()
subprocess.run(installer,env=env,check=True)
c.check_installed(c.TARGET,'1.1.0')
assert json.loads(state_path.read_text())[c.FRP]=={'active':True,'enabled':True}
print('PASS: existing binary skipped; damaged binary and missing drop-in repaired; enabled/active preserved',flush=True)
# Test independent rollback after the legacy worker switches program and data.
for stage in ('installed','switched','data-gap','service-failure','health-failure'):
reset()
source=c.RELEASES/'1.1.0-baseline103'
shutil.copytree(SEED/'software',source)
candidate=c.DATA.with_name('matrix-screen-controller.ota.baseline103')
shutil.copytree(c.DATA,candidate)
c.RUNTIME.mkdir(parents=True,exist_ok=True)
(c.RUNTIME/'ota-request.json').write_text(json.dumps(request),encoding='utf-8')
c.begin(source,candidate)
assert c.FILES['frpc'].exists()
if stage in ('service-failure','health-failure'):
failed_tx=worker.Transaction(request)
failed_tx.work_root.mkdir(parents=True)
failed_tx.start_visual=lambda: None
failed_tx.stop_visual=lambda: None
if stage=='service-failure':
state_path.write_text(json.dumps({'start_fail_once':c.SERVICE}))
try:
failed_tx.switch()
raise RuntimeError('injected health failure')
except Exception as error:
failed_tx.rollback(error)
if stage in ('switched','data-gap'):
c.TARGET.rename(c.RELEASES/'1.0.3-pre-ota-baseline103')
c.TARGET.symlink_to(source,target_is_directory=True)
c.DATA.rename(c.DATA.with_name('matrix-screen-controller.rollback.baseline103'))
if stage=='switched': candidate.rename(c.DATA)
c.finish(boot=True)
assert (c.TARGET/'VERSION').read_text().strip()=='1.0.3'
assert (c.DATA/'keep.txt').read_bytes()==b'user resource preserved'
assert not c.FILES['frpc'].exists()
assert not (c.DATA/c.JOURNAL).exists()
print('PASS: component/application interruption recovery '+stage,flush=True)
# v2 patch must fail in the real old parser, even without its own path policy.
from app.ota.package import build_package
from app.ota.versioning import SoftwareVersion
mini=ROOT/'patch-source';mini.mkdir(exist_ok=True)
(mini/'VERSION').write_text('1.1.1',encoding='utf-8')
patch=ROOT/'patch.ota';patch.unlink(missing_ok=True)
build_package(mini,SEED/'wheelhouse',patch,version=SoftwareVersion.parse('1.1.1'),release_notes='gate test')
try:
oldpackage.inspect_package(patch,current_version=SoftwareVersion.parse('1.0.3'))
except oldpackage.OtaPackageError:
print('PASS: real 1.0.3 parser rejects v2 patch before installation',flush=True)
else: raise AssertionError('legacy updater accepted patch')