fix(history): model sparse publication without relaxing live freshness
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
"""Installer ordering/recovery tests; subprocesses, privileges and DB backup are simulated."""
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import types
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
SCRIPT=Path(__file__).resolve().parents[1]/'commissioning/deploy_history_timing.py'
|
||||
spec=importlib.util.spec_from_file_location('history_deploy_tests',SCRIPT)
|
||||
d=importlib.util.module_from_spec(spec);spec.loader.exec_module(d)
|
||||
|
||||
|
||||
class TimingDeploymentTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.tmp=tempfile.TemporaryDirectory();self.root=Path(self.tmp.name)
|
||||
self.package=self.root/'commissioning/history-timing-source';self.package.mkdir(parents=True)
|
||||
files={}
|
||||
for index,name in enumerate(d.TARGETS):
|
||||
original=(b'old:'+name.encode()) if index%2==0 else None
|
||||
target=self.root/name;target.parent.mkdir(parents=True,exist_ok=True)
|
||||
if original is not None:target.write_bytes(original)
|
||||
candidate=self.package/'source'/name;candidate.parent.mkdir(parents=True,exist_ok=True);candidate.write_bytes(b'new:'+name.encode())
|
||||
files[name]={'before':hashlib.sha256(original).hexdigest() if original else None,'after':d.digest(candidate)}
|
||||
(self.package/'dataset.json').write_text('{}')
|
||||
(self.root/'commissioning/deploy_application.py').write_text('synthetic helper')
|
||||
self.manifest={'scope':'historical_publication_only','installationId':'plant','files':files,
|
||||
'existingDeploymentHelperSha256':d.digest(self.root/'commissioning/deploy_application.py'),
|
||||
'datasetSha256':d.digest(self.package/'dataset.json'),'installerSha256':d.digest(SCRIPT)}
|
||||
(self.package/'RELEASE.json').write_text(json.dumps(self.manifest))
|
||||
self.context=patch.multiple(d,ROOT=self.root,PACKAGE=self.package);self.context.start()
|
||||
self.calls=[]
|
||||
def tearDown(self):self.context.stop();self.tmp.cleanup()
|
||||
def fake_run(self,args,**kwargs):
|
||||
self.calls.append(args)
|
||||
if 'inspect' in args:out='sha256:test-old-image'
|
||||
elif 'ps' in args:out='test-container'
|
||||
elif 'exec' in args:out=json.dumps({'settingsUnchanged':True,'liveEnabled':False})
|
||||
else:out=''
|
||||
return subprocess.CompletedProcess(args,0,out,'')
|
||||
def execute(self,fail_at=None):
|
||||
def run(args,**kwargs):
|
||||
out=self.fake_run(args,**kwargs)
|
||||
if fail_at and fail_at in args:raise subprocess.CalledProcessError(1,args)
|
||||
return out
|
||||
def atomic(path,data,*args,**kwargs):path.write_bytes(data)
|
||||
def backup(source,dest):self.calls.append(['test-backup']);dest.write_text('synthetic backup')
|
||||
helpers=types.SimpleNamespace(atomic=atomic,backup_database=backup)
|
||||
with patch.object(d.os,'geteuid',return_value=0),patch.object(d.os,'chown'),patch.object(d.subprocess,'run',side_effect=run),patch.dict(sys.modules,{'deploy_application':helpers}):
|
||||
d.run('plant',True)
|
||||
def report(self):return json.loads(next(self.root.glob('history-timing-releases/*/REPORT.json')).read_text())
|
||||
def test_default_checks_only(self):
|
||||
d.run('plant');self.assertEqual(self.calls,[])
|
||||
self.assertFalse((self.root/'history-timing-releases').exists())
|
||||
def test_order_tests_backup_then_only_v4_replacement(self):
|
||||
self.execute()
|
||||
test=next(i for i,c in enumerate(self.calls) if '/app/run_tests.py' in c)
|
||||
backup=next(i for i,c in enumerate(self.calls) if c==['test-backup'])
|
||||
up=next(i for i,c in enumerate(self.calls) if 'up' in c)
|
||||
self.assertLess(test,backup);self.assertLess(backup,up)
|
||||
self.assertTrue(all('license-portal' not in c and 'forecast-engine' not in c for c in self.calls))
|
||||
self.assertEqual(self.report()['status'],'historical_timing_installed_no_actuation')
|
||||
def test_failed_tests_restore_host_source_without_restart(self):
|
||||
with self.assertRaises(subprocess.CalledProcessError):self.execute('/app/run_tests.py')
|
||||
self.assertFalse(any('up' in c for c in self.calls))
|
||||
for name,v in self.manifest['files'].items():self.assertEqual(d.digest(self.root/name),v['before'])
|
||||
def test_failed_registration_rolls_image_back(self):
|
||||
with self.assertRaises(subprocess.CalledProcessError):self.execute('exec')
|
||||
self.assertIn('previous_image_restored',self.report()['rollback'])
|
||||
self.assertTrue(any('--pull' in c and 'never' in c for c in self.calls))
|
||||
def test_source_drift_refused_before_actions(self):
|
||||
(self.root/d.TARGETS[0]).write_text('parallel change')
|
||||
with self.assertRaises(ValueError):d.verify()
|
||||
self.assertEqual(self.calls,[])
|
||||
def test_source_already_installed_idempotent(self):
|
||||
for name in d.TARGETS:(self.root/name).write_bytes((self.package/'source'/name).read_bytes())
|
||||
self.assertEqual(d.verify()['scope'],'historical_publication_only')
|
||||
self.execute()
|
||||
self.assertEqual(self.report()['status'],'historical_timing_installed_no_actuation')
|
||||
def test_no_production_selection_or_permission_in_bootstrap(self):
|
||||
self.assertNotIn("'/settings'",d.BOOTSTRAP)
|
||||
self.assertNotIn('/trial/arm',d.BOOTSTRAP)
|
||||
self.assertIn("before['settings']==after['settings']",d.BOOTSTRAP)
|
||||
self.assertIn("historyTimingVersion",d.BOOTSTRAP)
|
||||
def test_target_image_contains_installer_test_dependency(self):
|
||||
dockerfile=(SCRIPT.parents[1]/'Dockerfile').read_text()
|
||||
self.assertIn('COPY commissioning/deploy_history_timing.py ./commissioning/deploy_history_timing.py',dockerfile)
|
||||
|
||||
|
||||
if __name__=='__main__':unittest.main()
|
||||
Reference in New Issue
Block a user