初始化奇妙小屏幕控制器项目

This commit is contained in:
2026-09-08 22:56:52 +08:00
commit 8d368de3b5
491 changed files with 67678 additions and 0 deletions
@@ -0,0 +1,299 @@
from __future__ import annotations
import hashlib
import json
from pathlib import Path
import shutil
import subprocess
import zipfile
import pytest
from app.ota.policy import check_upgrade, export_bundle, package_format, read_policy
from app.ota.versioning import SoftwareVersion as V
from app.ota.package import build_package, inspect_package, OtaPackageError
from scripts import ota_components as c
@pytest.mark.parametrize('current,target', [
('1.0.0', '1.0.3'), ('1.0.3', '1.1.0'), ('1.0.6', '1.1.0'),
('1.1.0', '1.1.3'), ('1.1.0', '1.2.0'), ('1.1.8', '1.2.0'), ('1.2.0', '1.2.9'),
])
def test_allowed_upgrade_paths(current, target):
check_upgrade(V.parse(current), V.parse(target))
@pytest.mark.parametrize('current,target,required', [
('1.0.6', '1.1.1', '1.1.0'), ('1.0.3', '1.2.0', '1.1.0'),
('1.0.0', '1.2.5', '1.1.0'), ('1.1.3', '1.2.1', '1.2.0'),
])
def test_cannot_skip_software_install(current, target, required):
with pytest.raises(ValueError, match=required):
check_upgrade(V.parse(current), V.parse(target))
def source_and_wheels(root, version):
source = root / 'source'
source.mkdir()
(source / 'VERSION').write_text(version, encoding='utf-8')
wheels = root / 'wheels'
wheels.mkdir()
(wheels / 'SHA256SUMS').write_text('test', encoding='utf-8')
return source, wheels
def test_bridge_v1_and_patch_v2_with_identical_manifest_shape(tmp_path):
for version in ('1.1.0', '1.1.3'):
root = tmp_path / version
root.mkdir()
source, wheels = source_and_wheels(root, version)
package = root / 'release.ota'
build_package(source, wheels, package, version=V.parse(version), release_notes='test')
with zipfile.ZipFile(package) as z:
manifest = json.loads(z.read('manifest.json'))
assert manifest['format_version'] == package_format(V.parse(version))
if version == '1.1.3':
with pytest.raises(OtaPackageError, match='1.1.0'):
inspect_package(package, current_version=V.parse('1.0.6'))
inspect_package(package, current_version=V.parse('1.1.0'))
def test_patch_rejects_system_binary_payload(tmp_path):
source, wheels = source_and_wheels(tmp_path, '1.1.1')
with pytest.raises(OtaPackageError, match='普通补丁'):
build_package(source, wheels, tmp_path/'bad.ota', version=V.parse('1.1.1'),
release_notes='test', system_dependencies=wheels)
def test_dependency_change_requires_checkpoint(tmp_path):
source = tmp_path / 'source'
source.mkdir()
binary = tmp_path / 'deps/frp/v/frpc'
binary.parent.mkdir(parents=True)
binary.write_bytes(b'original')
policy = {'schema_version': 1, 'checkpoints': [{'version': '1.1.0', 'components': {
'frpc': {'version': 'v', 'bundle': 'frp/v', 'sha256': hashlib.sha256(b'original').hexdigest()}}}]}
(source/'UPGRADE_POLICY.json').write_text(json.dumps(policy), encoding='utf-8')
assert export_bundle(source, tmp_path/'deps', V.parse('1.0.6'), V.parse('1.1.0')) == binary.parent
assert export_bundle(source, tmp_path/'deps', V.parse('1.1.0'), V.parse('1.1.3')) is None
binary.write_bytes(b'replaced')
with pytest.raises(ValueError, match='依赖发生变化'):
export_bundle(source, tmp_path/'deps', V.parse('1.1.0'), V.parse('1.1.3'))
policy['checkpoints'][0]['version'] = '1.2.0'
(source/'UPGRADE_POLICY.json').write_text(json.dumps(policy), encoding='utf-8')
with pytest.raises(ValueError, match='consecutive'):
read_policy(source)
@pytest.fixture
def host(tmp_path, monkeypatch):
for name, relative in [('DATA', 'var/data'), ('TARGET', 'opt/current'), ('RELEASES', 'opt/releases'),
('RUNTIME', 'run'), ('HELPER', 'opt/recover.py'), ('RECOVERY_UNIT', 'etc/recovery.service'),
('WORK_ROOT', 'opt/work'), ('ACCOUNT_POLICY', 'etc/account'),
('RUNTIME_GUARD', 'run/systemd/service.d/guard.conf')]:
monkeypatch.setattr(c, name, tmp_path / relative)
files = {name: tmp_path / 'etc' / name for name in c.FILES}
monkeypatch.setattr(c, 'FILES', files)
c.DATA.mkdir(parents=True)
c.RELEASES.mkdir(parents=True)
for name, path in files.items():
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(('old-'+name).encode())
commands = []
def run(args, **kwargs):
commands.append(args)
return subprocess.CompletedProcess(args, 0, b'', b'')
monkeypatch.setattr(c, 'run', run)
monkeypatch.setattr(c, 'sync_directory', lambda path: None)
monkeypatch.setattr(c, 'apply_metadata', lambda *args: None)
return commands
def make_journal():
journal = c.DATA / c.JOURNAL
journal.mkdir(parents=True)
record = {'job_id': 'job123', 'version': '1.1.0', 'old_version': '1.0.6',
'accepted_at': '2026-09-07T00:00:00Z', 'previous_target': None,
'permissions': {}, 'active': True, 'enabled': True,
'files': {name: {'mode': 0o644, 'uid': 0, 'gid': 0} for name in c.FILES}}
for name, path in c.FILES.items():
(journal/name).write_bytes(path.read_bytes())
(journal/'state.json').write_text(json.dumps(record), encoding='utf-8')
return journal, record
def test_failed_install_restores_system_bytes_and_running_state(host):
journal, record = make_journal()
for name in ('frpc', 'frpc-unit', 'frpc-dropin'):
c.FILES[name].write_bytes(b'new')
c.restore_components(journal, record)
for name in ('frpc', 'frpc-unit', 'frpc-dropin'):
assert c.FILES[name].read_bytes() == ('old-'+name).encode()
assert ['systemctl', 'enable', c.FRP] in host
assert ['systemctl', 'start', c.FRP] in host
def test_power_loss_between_data_renames_recovers_original(host):
journal, record = make_journal()
(c.DATA/'user-content').write_bytes(b'unchanged')
backup = c.DATA.with_name('matrix-screen-controller.rollback.job123')
c.DATA.rename(backup)
assert c.locate_journal() == backup / c.JOURNAL
recovered = c.recover_application(backup/c.JOURNAL, record)
assert recovered == c.DATA/c.JOURNAL
assert (c.DATA/'user-content').read_bytes() == b'unchanged'
assert not backup.exists()
def test_legacy_failure_restores_ota_unit_too(host):
journal, record = make_journal()
c.FILES['ota-unit'].write_bytes(b'new-worker-unit')
c.recover_application(journal, record)
assert c.FILES['ota-unit'].read_bytes() == b'old-ota-unit'
def test_success_cleanup_keeps_installed_component_and_user_data(host):
journal, record = make_journal()
c.TARGET.mkdir(parents=True)
(c.TARGET/'VERSION').write_text('1.1.0', encoding='utf-8')
result = {'last_result': {'status': 'success', 'target_version': '1.1.0', 'installed_at': '2026-09-07T01:00:00Z'}}
(c.DATA/'ota/state.json').write_text(json.dumps(result), encoding='utf-8')
(c.DATA/'user-content').write_bytes(b'keep')
c.FILES['frpc'].write_bytes(b'new-verified')
c.finish()
assert c.FILES['frpc'].read_bytes() == b'new-verified'
assert (c.DATA/'user-content').read_bytes() == b'keep'
assert not journal.exists()
assert ['systemctl', 'stop', c.FRP] not in host
def test_old_success_result_cannot_commit_new_transaction(host):
journal, record = make_journal()
c.TARGET.mkdir(parents=True)
(c.TARGET/'VERSION').write_text('1.1.0', encoding='utf-8')
(c.DATA/'ota/state.json').write_text(json.dumps({'last_result': {
'status': 'success', 'target_version': '1.1.0', 'installed_at': '2026-09-01T00:00:00Z'}}), encoding='utf-8')
assert not c.committed(record)
def test_normal_migration_does_not_touch_components(host):
c.begin(c.TARGET, c.DATA)
assert host == []
def release_project(tmp_path, monkeypatch):
from scripts import export_release as exporter
source = tmp_path / 'software'
source.mkdir()
(source / 'VERSION').write_text('1.0.6\n', encoding='utf-8')
(source / 'FEATURE_UPDATED_AT').write_text('2026-09-07T20:00+08:00\n', encoding='utf-8')
deps = tmp_path / '发布更新相关/其他依赖'
binary = deps / 'frp/v/frpc'
binary.parent.mkdir(parents=True)
binary.write_bytes(b'frpc')
digest = hashlib.sha256(b'frpc').hexdigest()
(binary.parent/'SHA256SUMS').write_text(f'{digest} frpc\n', encoding='utf-8')
wheels = deps / 'aarch64-py311'
wheels.mkdir()
(wheels/'SHA256SUMS').write_text('fixture', encoding='utf-8')
policy = {'schema_version': 1, 'checkpoints': [{'version': '1.1.0', 'components': {
'frpc': {'version': 'v', 'bundle': 'frp/v', 'sha256': digest}}}]}
(source/'UPGRADE_POLICY.json').write_text(json.dumps(policy), encoding='utf-8')
(tmp_path/'发布记录.json').write_text('{"schema_version":1,"releases":[]}', encoding='utf-8')
monkeypatch.setattr(exporter, 'SOURCE_ROOT', source)
return exporter, source
def test_export_checkpoint_then_patch_without_repeating_binary(tmp_path, monkeypatch):
import sys
import tarfile
exporter, source = release_project(tmp_path, monkeypatch)
monkeypatch.setattr(sys, 'argv', ['export', 'ota', '--version', '1.1.0', '--notes', 'test'])
assert exporter.main() == 0
assert (source/'VERSION').read_text(encoding='utf-8').strip() == '1.1.0'
monkeypatch.setattr(sys, 'argv', ['export', 'ota', '--notes', 'patch'])
assert exporter.main() == 0
artifact = next((tmp_path/'发布更新相关/OTA数据包/1.1.1').glob('*.ota'))
with zipfile.ZipFile(artifact) as z, z.open('payload.tar.gz') as f, tarfile.open(fileobj=f, mode='r|gz') as t:
assert not any('system-dependencies' in item.name for item in t)
records = json.loads((tmp_path/'发布记录.json').read_text(encoding='utf-8'))['releases']
assert [r['package_kind'] for r in records] == ['software-install', 'application']
assert all(r['artifact_path'].startswith('发布更新相关/OTA数据包/') for r in records)
assert all((tmp_path / r['artifact_path']).is_file() for r in records)
assert (source/'FEATURE_UPDATED_AT').read_text(encoding='utf-8') == '2026-09-07T20:00+08:00\n'
def test_failed_export_does_not_consume_version(tmp_path, monkeypatch):
import sys
exporter, source = release_project(tmp_path, monkeypatch)
original = (tmp_path/'发布记录.json').read_bytes()
original_version = (source/'VERSION').read_bytes()
monkeypatch.setattr(sys, 'argv', ['export', 'ota', '--version', '1.1.0', '--notes', 'test'])
def fail(*args):
raise OSError('injected history write failure')
monkeypatch.setattr(exporter, '_atomic_json', fail)
with pytest.raises(OSError, match='injected'):
exporter.main()
assert (source/'VERSION').read_bytes() == original_version
assert (tmp_path/'发布记录.json').read_bytes() == original
assert not list((tmp_path/'发布更新相关/OTA数据包').iterdir())
assert not (tmp_path/'.release-export.lock').exists()
def test_recovery_error_keeps_durable_journal(host, monkeypatch):
journal, record = make_journal()
def fail(*args):
raise OSError('injected restore failure')
monkeypatch.setattr(c, 'restore_components', fail)
with pytest.raises(OSError, match='injected'):
c.finish()
assert (journal/'state.json').is_file()
def test_provisioned_account_does_not_need_sudo_group(host, monkeypatch):
import sys
from types import SimpleNamespace
c.FILES['frpc-dropin'].unlink()
c.ACCOUNT_POLICY.write_text('maintainer ALL=(ALL:ALL) ALL\n', encoding='utf-8')
monkeypatch.setitem(sys.modules, 'pwd', SimpleNamespace(getpwnam=lambda name: SimpleNamespace(pw_uid=1001)))
monkeypatch.setitem(sys.modules, 'grp', SimpleNamespace())
assert c.account() == 'maintainer'
c.ACCOUNT_POLICY.write_text('invalid', encoding='utf-8')
with pytest.raises(RuntimeError, match='账户配置无效'):
c.account()
@pytest.mark.parametrize('fail_commit', [False, True])
def test_same_version_repair_promotes_exact_candidate_and_preserves_old_artifact(tmp_path, monkeypatch, fail_commit):
import sys
from scripts.repair_ota_release import promote
exporter, source = release_project(tmp_path, monkeypatch)
monkeypatch.setattr(sys, 'argv', ['export', 'ota', '--version', '1.1.0', '--notes', 'original'])
exporter.main()
artifact=tmp_path/'发布更新相关/OTA数据包/1.1.0/matrix-screen-controller-1.1.0.ota'
old=artifact.read_bytes();old_history=(tmp_path/'发布记录.json').read_bytes()
(source/'fix.py').write_text('FIXED = True\n',encoding='utf-8')
candidate=tmp_path/'candidate.ota'
build_package(source,tmp_path/'发布更新相关/其他依赖/aarch64-py311',candidate,
version=V.parse('1.1.0'),release_notes='repair',
system_dependencies=tmp_path/'发布更新相关/其他依赖/frp/v')
report=tmp_path/'validation.json'
report.write_text(json.dumps({'package_sha256':hashlib.sha256(candidate.read_bytes()).hexdigest(),
'installed_version':'1.1.0','status':'success','runtime_lifecycle_passed':True,
'user_data_preserved':True,'frp_state_preserved':True,'transaction_cleanup_passed':True}),encoding='utf-8')
if fail_commit:
def fail(*args): raise OSError('injected publication failure')
monkeypatch.setattr(exporter,'_atomic_json',fail)
with pytest.raises(OSError,match='publication'):
promote(source,candidate,report,'repair')
assert artifact.read_bytes()==old
assert (tmp_path/'发布记录.json').read_bytes()==old_history
else:
promote(source,candidate,report,'repair')
assert artifact.read_bytes()==candidate.read_bytes()
history=json.loads((tmp_path/'发布记录.json').read_text(encoding='utf-8'))
assert len(history['releases'])==1
assert len(history['releases'][0]['repairs'])==1
assert (source/'VERSION').read_text(encoding='utf-8').strip()=='1.1.0'
archived=list((tmp_path/'各种归档').glob('*/原安装包/*.ota'))
assert len(archived)==1 and archived[0].read_bytes()==old