"""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()