109 lines
7.4 KiB
Python
109 lines
7.4 KiB
Python
import importlib.util, io, json, os, tarfile, tempfile, unittest, threading, time
|
|
from pathlib import Path
|
|
|
|
spec=importlib.util.spec_from_file_location('delivery',Path(__file__).with_name('pipeline.py'))
|
|
delivery=importlib.util.module_from_spec(spec);spec.loader.exec_module(delivery)
|
|
|
|
class PreservationTests(unittest.TestCase):
|
|
def test_archive_extracts_only_source_bytes(self):
|
|
with tempfile.TemporaryDirectory() as d:
|
|
buf=io.BytesIO()
|
|
with tarfile.open(fileobj=buf,mode='w') as archive:
|
|
item=tarfile.TarInfo('app/source.php');item.size=4;archive.addfile(item,io.BytesIO(b'code'))
|
|
delivery.archive_source(buf.getvalue(),Path(d))
|
|
self.assertEqual((Path(d)/'app/source.php').read_bytes(),b'code')
|
|
def test_archive_refuses_private_data_and_escaping_links(self):
|
|
for name,kind in [('.env','file'),('storage/app/private/secret.pdf','file'),('../outside','file'),('public/escape','link')]:
|
|
with self.subTest(name=name),tempfile.TemporaryDirectory() as d:
|
|
buf=io.BytesIO()
|
|
with tarfile.open(fileobj=buf,mode='w') as archive:
|
|
item=tarfile.TarInfo(name)
|
|
if kind=='link':item.type=tarfile.SYMTYPE;item.linkname='/etc/passwd'
|
|
else:item.size=1
|
|
archive.addfile(item,None if kind=='link' else io.BytesIO(b'x'))
|
|
with self.assertRaises(RuntimeError):delivery.archive_source(buf.getvalue(),Path(d))
|
|
|
|
def test_source_edit_and_unlisted_file_stop_promotion(self):
|
|
with tempfile.TemporaryDirectory() as d:
|
|
root=Path(d);(root/'app').mkdir();(root/'app/code.php').write_text('before')
|
|
original=delivery.tree_hashes(root);(root/'app/code.php').write_text('programmer change')
|
|
with self.assertRaises(RuntimeError):delivery.refuse_drift(root,original)
|
|
(root/'app/code.php').write_text('before');(root/'app/new.php').write_text('extra')
|
|
with self.assertRaises(RuntimeError):delivery.refuse_drift(root,original)
|
|
|
|
def test_runtime_data_not_part_of_source_drift(self):
|
|
with tempfile.TemporaryDirectory() as d:
|
|
root=Path(d);(root/'app').mkdir();(root/'app/code.php').write_text('same')
|
|
original=delivery.tree_hashes(root)
|
|
(root/'storage/app/private').mkdir(parents=True);(root/'storage/app/private/new.pdf').write_bytes(b'later data')
|
|
(root/'.env').write_text('APP_KEY=not-distributed')
|
|
delivery.refuse_drift(root,original)
|
|
|
|
def test_schema_change_stops_before_database_or_build_commands(self):
|
|
with tempfile.TemporaryDirectory() as d:
|
|
root=Path(d);contract=root/'schema.json';contract.write_text('{}');release=root/'release';release.mkdir()
|
|
archive=io.BytesIO()
|
|
with tarfile.open(fileobj=archive,mode='w') as tar:
|
|
item=tarfile.TarInfo('database/migrations/new.php');item.size=4;tar.addfile(item,io.BytesIO(b'code'))
|
|
config={'state_root':d,'release_root':d,'current_link':str(root/'current'),'legacy_root':d,'schema_contract':str(contract)}
|
|
runner=delivery.Delivery(config);calls=[]
|
|
def source_only(argv,**kwargs):calls.append(argv);return archive.getvalue()
|
|
runner.run=source_only
|
|
with self.assertRaisesRegex(RuntimeError,'Schema source changed'):runner.build('a'*40,release)
|
|
self.assertEqual(len(calls),1);self.assertIn('archive',calls[0])
|
|
|
|
@unittest.skipIf(os.name=='nt','Atomic release promotion is exercised on the Linux destination clone')
|
|
def test_failed_health_restores_previous_release_and_later_shared_data(self):
|
|
with tempfile.TemporaryDirectory() as d:
|
|
root=Path(d);old=root/'old';new=root/'new';old.mkdir();new.mkdir()
|
|
shared=root/'private';shared.mkdir();record=shared/'later.txt';record.write_text('new data')
|
|
link=root/'current';link.symlink_to(old,target_is_directory=True)
|
|
def failed():raise RuntimeError('observed HTTP failure')
|
|
with self.assertRaises(RuntimeError):delivery.promote(link,new,failed)
|
|
self.assertEqual(link.resolve(),old);self.assertEqual(record.read_text(),'new data')
|
|
|
|
def test_qa_refuses_unapproved_port_or_database(self):
|
|
with tempfile.TemporaryDirectory() as d:
|
|
root=Path(d);flags=root/'flags.json';flags.write_text('{}')
|
|
config={'state_root':d,'release_root':d,'current_link':str(root/'current'),'legacy_root':d,
|
|
'ci_environment':str(flags),'qa_port':5432,'test_database':'teste_investidor'}
|
|
with self.assertRaises(RuntimeError):delivery.Delivery(config).ci_env()
|
|
|
|
@unittest.skipIf(os.name=='nt','Linux symlinks and flock')
|
|
def test_explicit_rollback_preserves_later_private_records(self):
|
|
with tempfile.TemporaryDirectory() as d:
|
|
root=Path(d);old=root/'old';new=root/'new';old.mkdir();new.mkdir()
|
|
(old/'code').write_text('old');(new/'code').write_text('new')
|
|
shared=root/'storage';shared.mkdir();(shared/'later').write_text('keep')
|
|
link=root/'current';link.symlink_to(new,target_is_directory=True)
|
|
installed={'release':str(new),'commit':'b'*40,'hashes':delivery.tree_hashes(new)}
|
|
previous={'release':str(old),'commit':'a'*40,'hashes':delivery.tree_hashes(old)}
|
|
delivery.write_json(root/'installed.json',installed);delivery.write_json(root/'previous.json',previous)
|
|
config={'state_root':d,'release_root':d,'current_link':str(link),'legacy_root':d,'php':'php'}
|
|
runner=delivery.Delivery(config);runner.run=lambda *a,**k:b'';runner.health=lambda target:self.assertEqual(target,'a'*40)
|
|
self.assertEqual(runner.rollback()['status'],'rolled_back')
|
|
self.assertEqual(link.resolve(),old);self.assertEqual((shared/'later').read_text(),'keep')
|
|
|
|
@unittest.skipIf(os.name=='nt','Linux flock and systemd target')
|
|
def test_concurrent_runner_and_new_event_do_not_lose_or_overlap_jobs(self):
|
|
with tempfile.TemporaryDirectory() as d:
|
|
root=Path(d);(root/'requests').mkdir();head=root/'head';head.write_text('a'*40)
|
|
delivery.write_json(root/'requests'/('a'*40+'.json'),{'commit':'a'*40})
|
|
entered=threading.Event();resume=threading.Event();calls=[];active=[0,0];guard=threading.Lock()
|
|
config={'state_root':d,'release_root':d,'current_link':str(root/'current'),'legacy_root':d}
|
|
class Fake(delivery.Delivery):
|
|
def latest(self):return head.read_text()
|
|
def deliver(self,target):
|
|
with guard:active[0]+=1;active[1]=max(active[1],active[0])
|
|
if target=='a'*40:entered.set();resume.wait(3)
|
|
calls.append(target)
|
|
with guard:active[0]-=1
|
|
return {'status':'installed','commit':target}
|
|
first=threading.Thread(target=Fake(config).process);first.start();self.assertTrue(entered.wait(3))
|
|
head.write_text('b'*40);delivery.write_json(root/'requests'/('b'*40+'.json'),{'commit':'b'*40})
|
|
second=threading.Thread(target=Fake(config).process);second.start();resume.set();first.join(5);second.join(5)
|
|
self.assertFalse(first.is_alive() or second.is_alive());self.assertEqual(active[1],1)
|
|
self.assertEqual(calls,['a'*40,'b'*40]);self.assertEqual(list((root/'requests').glob('*.json')),[])
|
|
|
|
if __name__=='__main__':unittest.main()
|