Files
Investidor-V2/deploy/homologacao/test_pipeline.py
T

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