72 lines
4.1 KiB
Python
72 lines
4.1 KiB
Python
#!/usr/bin/env python3
|
|
"""Exercise actual controller receipt/cleanup ordering with local disposable IO.
|
|
Only native admission/launch inputs are stubbed; no runtime/native calls.
|
|
"""
|
|
import contextlib
|
|
import importlib.util
|
|
import io
|
|
import json
|
|
from pathlib import Path
|
|
import shutil
|
|
import sys
|
|
import tempfile
|
|
from types import SimpleNamespace
|
|
import unittest
|
|
from unittest.mock import patch
|
|
|
|
HERE=Path(__file__).resolve().parent
|
|
|
|
|
|
class ReceiptCleanupTests(unittest.TestCase):
|
|
def exercise(self,fail_persist):
|
|
spec=importlib.util.spec_from_file_location('controller',HERE/'verify-installed.py')
|
|
m=importlib.util.module_from_spec(spec);spec.loader.exec_module(m)
|
|
with tempfile.TemporaryDirectory(prefix='nk-receipt-order-') as d:
|
|
base=Path(d);stage=base/'stage';stage.mkdir(mode=0o700)
|
|
receipt=base/'receipt.json';destination=base/'installed';destination.mkdir()
|
|
for name in ('runtime.py','verify-installed.py','process_supervision.py'):(stage/name).touch(mode=0o600)
|
|
jobs=[{'label':name,'returncode':0,'status':'completed'} for name in ('rein-aharness','glas-harness','claude')]
|
|
class FakeSupervisor:
|
|
def __init__(self,state):
|
|
state.mkdir(mode=0o700);p=state/'public-journal.json';p.write_text(json.dumps({'jobs':jobs}));p.chmod(0o600)
|
|
def close(self):return {'quiescent':True,'no_live_or_waitable_descendants':True,'jobs':jobs}
|
|
original_protected=m.protected
|
|
def protected(path,**kwargs):
|
|
actual=original_protected(path,**kwargs)
|
|
if path==stage:return SimpleNamespace(st_ino=actual.st_ino,st_dev=2049)
|
|
return actual
|
|
removed=[];real_rmtree=shutil.rmtree
|
|
def checked_remove(path):
|
|
before=json.loads(receipt.read_text())
|
|
self.assertTrue(before['durable_receipt_before_cleanup'])
|
|
self.assertTrue(before['process_supervision']['quiescent'])
|
|
self.assertEqual(before['cleanup_status'],'pending')
|
|
self.assertEqual([x['exit_code'] for x in before['launcher_status']],[0,0,0])
|
|
removed.append(True);real_rmtree(path)
|
|
output=io.StringIO()
|
|
with contextlib.ExitStack() as stack:
|
|
for name,value in {'STAGE':stage,'DESTINATION':destination,'DURABLE_RECEIPT':receipt,
|
|
'protected':protected,'identify':lambda supervisor:None,
|
|
'supervision_module':lambda:SimpleNamespace(Supervisor=FakeSupervisor),
|
|
'runtime_reader':lambda:SimpleNamespace(runtime_digest=lambda path:m.DIGEST),
|
|
'phase':lambda *args:(0,{'status':'rehearsed','steps':{j['label']:0 for j in jobs}})}.items():
|
|
stack.enter_context(patch.object(m,name,value))
|
|
stack.enter_context(patch.object(m.os,'nice',lambda value:None))
|
|
stack.enter_context(patch.object(m.shutil,'rmtree',checked_remove))
|
|
stack.enter_context(patch.object(sys,'argv',['verify-installed.py','--stage-inode',str(stage.stat().st_ino)]))
|
|
if fail_persist:stack.enter_context(patch.object(m,'persist',side_effect=OSError('fixture write failure')))
|
|
with contextlib.redirect_stdout(output):code=m.main()
|
|
result=json.loads(output.getvalue())
|
|
if fail_persist:
|
|
self.assertEqual(code,1);self.assertFalse(removed);self.assertTrue(stage.exists())
|
|
self.assertFalse(result['durable_receipt_before_cleanup'])
|
|
else:
|
|
self.assertEqual(code,0);self.assertEqual(removed,[True]);self.assertFalse(stage.exists())
|
|
checkpoint=json.loads(receipt.read_text());self.assertEqual(checkpoint['cleanup_status'],'pending')
|
|
self.assertTrue(result['owned_staging_removed']);self.assertEqual(receipt.stat().st_mode&0o777,0o600)
|
|
|
|
def test_actual_cleanup_waits_for_durable_complete_public_receipt(self):self.exercise(False)
|
|
def test_actual_cleanup_refuses_when_durable_receipt_write_fails(self):self.exercise(True)
|
|
|
|
|
|
if __name__=='__main__':unittest.main()
|