Add validated OTA candidate publishing and fix 1.1.2 dependency failure
This commit is contained in:
@@ -241,6 +241,8 @@ def release_project(tmp_path, monkeypatch):
|
||||
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')
|
||||
(source / 'requirements.txt').write_text('example==1.0.0\n', encoding='utf-8')
|
||||
(source / 'requirements-dev.txt').write_text('-r requirements.txt\n', encoding='utf-8')
|
||||
deps = tmp_path / '发布更新相关/其他依赖'
|
||||
binary = deps / 'frp/v/frpc'
|
||||
binary.parent.mkdir(parents=True)
|
||||
@@ -249,7 +251,9 @@ def release_project(tmp_path, monkeypatch):
|
||||
(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')
|
||||
wheel = wheels / 'example-1.0.0-py3-none-any.whl'
|
||||
wheel.write_bytes(b'fixture wheel')
|
||||
(wheels/'SHA256SUMS').write_text(f'{hashlib.sha256(wheel.read_bytes()).hexdigest()} {wheel.name}\n', encoding='ascii')
|
||||
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')
|
||||
@@ -258,15 +262,102 @@ def release_project(tmp_path, monkeypatch):
|
||||
return exporter, source
|
||||
|
||||
|
||||
def test_export_checkpoint_then_patch_without_repeating_binary(tmp_path, monkeypatch):
|
||||
def validated_ota_report(tmp_path, candidate, source_version, target_version):
|
||||
artifact = candidate / f'matrix-screen-controller-{target_version}.ota'
|
||||
report = tmp_path / f'validated-{target_version}.json'
|
||||
report.write_text(json.dumps({
|
||||
'schema_version': 1, 'artifact_type': 'ota',
|
||||
'package_sha256': hashlib.sha256(artifact.read_bytes()).hexdigest(),
|
||||
'software_version': target_version, 'source_version': source_version,
|
||||
'restored_version': source_version, 'status': 'success',
|
||||
'validated_at': '2026-09-27T18:00:00+08:00',
|
||||
'offline_install_passed': True, 'ota_health_passed': True,
|
||||
'runtime_lifecycle_passed': True, 'user_data_preserved': True,
|
||||
'frp_state_preserved': True, 'bluetooth_state_preserved': True,
|
||||
'transaction_cleanup_passed': True, 'baseline_restored': True,
|
||||
}), encoding='utf-8')
|
||||
return report
|
||||
|
||||
|
||||
def publish_fixture_ota(tmp_path, monkeypatch, exporter, current, target, notes):
|
||||
import sys
|
||||
candidate = tmp_path / f'candidate-{target}'
|
||||
args = ['export', 'ota', '--notes', notes, '--candidate-output', str(candidate)]
|
||||
if target.endswith('.0'):
|
||||
args.extend(['--version', target])
|
||||
monkeypatch.setattr(sys, 'argv', args)
|
||||
assert exporter.main() == 0
|
||||
report = validated_ota_report(tmp_path, candidate, current, target)
|
||||
monkeypatch.setattr(sys, 'argv', ['export', 'ota', '--notes', notes,
|
||||
'--validated-candidate', str(candidate),
|
||||
'--validation-report', str(report)])
|
||||
assert exporter.main() == 0
|
||||
return candidate
|
||||
|
||||
|
||||
@pytest.mark.parametrize('failure', ['missing', 'version', 'digest'])
|
||||
def test_ota_candidate_rejects_incomplete_offline_wheels(tmp_path, monkeypatch, failure):
|
||||
import sys
|
||||
exporter, source = release_project(tmp_path, monkeypatch)
|
||||
wheels = tmp_path / '发布更新相关/其他依赖/aarch64-py311'
|
||||
if failure == 'missing':
|
||||
(source / 'requirements-dev.txt').write_text('-r requirements.txt\nparamiko==4.0.0\n', encoding='utf-8')
|
||||
expected = 'offline wheel missing'
|
||||
elif failure == 'version':
|
||||
(source / 'requirements.txt').write_text('example==2.0.0\n', encoding='utf-8')
|
||||
expected = 'offline wheel version missing'
|
||||
else:
|
||||
(wheels / 'example-1.0.0-py3-none-any.whl').write_bytes(b'changed')
|
||||
expected = 'digest differs'
|
||||
candidate = tmp_path / 'candidate-1.1.0'
|
||||
monkeypatch.setattr(sys, 'argv', ['export', 'ota', '--version', '1.1.0',
|
||||
'--candidate-output', str(candidate), '--notes', 'test'])
|
||||
with pytest.raises(ValueError, match=expected):
|
||||
exporter.main()
|
||||
assert not candidate.exists()
|
||||
assert (source / 'VERSION').read_text(encoding='utf-8').strip() == '1.0.6'
|
||||
assert json.loads((tmp_path / '发布记录.json').read_text(encoding='utf-8'))['releases'] == []
|
||||
assert not (tmp_path / '.release-export.lock').exists()
|
||||
|
||||
|
||||
@pytest.mark.parametrize('failure', ['report', 'source', 'artifact'])
|
||||
def test_ota_candidate_promotion_rejects_changed_evidence(tmp_path, monkeypatch, failure):
|
||||
import sys
|
||||
from scripts.promote_ota_release import promote
|
||||
exporter, source = release_project(tmp_path, monkeypatch)
|
||||
candidate = tmp_path / 'candidate-1.1.0'
|
||||
monkeypatch.setattr(sys, 'argv', ['export', 'ota', '--version', '1.1.0',
|
||||
'--candidate-output', str(candidate), '--notes', 'test'])
|
||||
assert exporter.main() == 0
|
||||
assert (source / 'VERSION').read_text(encoding='utf-8').strip() == '1.0.6'
|
||||
assert not (tmp_path / '发布更新相关/OTA数据包/1.1.0').exists()
|
||||
report = validated_ota_report(tmp_path, candidate, '1.0.6', '1.1.0')
|
||||
if failure == 'report':
|
||||
document = json.loads(report.read_text(encoding='utf-8'))
|
||||
document['package_sha256'] = '0' * 64
|
||||
report.write_text(json.dumps(document), encoding='utf-8')
|
||||
expected = 'matching successful OTA'
|
||||
elif failure == 'source':
|
||||
(source / 'changed.py').write_text('CHANGED = True\n', encoding='utf-8')
|
||||
expected = 'software file list differs'
|
||||
else:
|
||||
artifact = candidate / 'matrix-screen-controller-1.1.0.ota'
|
||||
artifact.write_bytes(artifact.read_bytes() + b'tampered')
|
||||
expected = 'manifest differs'
|
||||
with pytest.raises(ValueError, match=expected):
|
||||
promote(source, candidate, report, 'test')
|
||||
assert (source / 'VERSION').read_text(encoding='utf-8').strip() == '1.0.6'
|
||||
assert not (tmp_path / '发布更新相关/OTA数据包/1.1.0').exists()
|
||||
assert json.loads((tmp_path / '发布记录.json').read_text(encoding='utf-8'))['releases'] == []
|
||||
assert not (tmp_path / '.release-export.lock').exists()
|
||||
|
||||
|
||||
def test_export_checkpoint_then_patch_without_repeating_binary(tmp_path, monkeypatch):
|
||||
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
|
||||
publish_fixture_ota(tmp_path, monkeypatch, exporter, '1.0.6', '1.1.0', 'test')
|
||||
assert (source/'VERSION').read_text(encoding='utf-8').strip() == '1.1.0'
|
||||
monkeypatch.setattr(sys, 'argv', ['export', 'ota', '--notes', 'patch'])
|
||||
assert exporter.main() == 0
|
||||
publish_fixture_ota(tmp_path, monkeypatch, exporter, '1.1.0', '1.1.1', 'patch')
|
||||
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)
|
||||
@@ -282,7 +373,14 @@ def test_failed_export_does_not_consume_version(tmp_path, monkeypatch):
|
||||
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'])
|
||||
candidate = tmp_path / 'candidate-1.1.0'
|
||||
monkeypatch.setattr(sys, 'argv', ['export', 'ota', '--version', '1.1.0',
|
||||
'--candidate-output', str(candidate), '--notes', 'test'])
|
||||
assert exporter.main() == 0
|
||||
report = validated_ota_report(tmp_path, candidate, '1.0.6', '1.1.0')
|
||||
monkeypatch.setattr(sys, 'argv', ['export', 'ota', '--notes', 'test',
|
||||
'--validated-candidate', str(candidate),
|
||||
'--validation-report', str(report)])
|
||||
def fail(*args):
|
||||
raise OSError('injected history write failure')
|
||||
monkeypatch.setattr(exporter, '_atomic_json', fail)
|
||||
@@ -319,11 +417,9 @@ def test_provisioned_account_does_not_need_sudo_group(host, monkeypatch):
|
||||
|
||||
@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()
|
||||
publish_fixture_ota(tmp_path, monkeypatch, exporter, '1.0.6', '1.1.0', 'original')
|
||||
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')
|
||||
|
||||
Reference in New Issue
Block a user