Files
truf-server/tests/test_validated_high_lifecycle_fixes.py
T
2026-09-30 20:30:56 +03:00

371 lines
12 KiB
Python

from concurrent.futures import Future
import os
from pathlib import Path
import shutil
import subprocess
import sys
import tempfile
import unittest
from unittest import mock
ROOT = Path(__file__).resolve().parents[1]
APP_DIR = ROOT / 'app'
sys.path.insert(0, str(APP_DIR))
import lifecycle_authority
import postgres_runtime
import supervisor
import supervisor_instance
def canonical(path):
return os.path.normcase(os.path.realpath(os.path.abspath(os.fspath(path))))
class ImmediateExecutor:
def submit(self, function):
future = Future()
try:
future.set_result(function())
except BaseException as exc:
future.set_exception(exc)
return future
class ControlledStartExecutor:
def __init__(self):
self.submissions = []
def submit(self, function):
future = Future()
self.submissions.append((function.__name__, future))
if function.__name__ == 'stop':
future.set_result(function())
return future
class LifecycleBackend:
def __init__(self, probes=(), starts=()):
self.probes = list(probes)
self.starts = list(starts)
self.start_calls = 0
self.stop_calls = 0
self.closed = False
def probe(self):
return self.probes.pop(0)
def start(self):
self.start_calls += 1
return self.starts.pop(0)
def stop(self):
self.stop_calls += 1
return postgres_runtime.StopResult(True, True, 'stopped')
def close(self):
self.closed = True
class PostgresLifecycleTests(unittest.TestCase):
def test_close_of_unowned_ready_controller_never_stops_cluster(self):
backend = LifecycleBackend(probes=[
postgres_runtime.ProbeResult(postgres_runtime.ProbeKind.READY, 'preexisting ready', 'epoch'),
])
controller = postgres_runtime.PostgresController(
backend,
executor=ImmediateExecutor(),
stable_ready_interval_sec=0,
)
controller.tick(0)
controller.tick(0)
self.assertEqual(controller.state, postgres_runtime.PostgresState.READY)
self.assertFalse(controller._owned_start)
self.assertTrue(controller.close(timeout_sec=0.1))
self.assertEqual(backend.stop_calls, 0)
self.assertTrue(backend.closed)
self.assertTrue(controller.terminal)
self.assertTrue(controller.authority_release_safe)
self.assertIn('did not start', controller.detail)
def test_close_of_inflight_accepted_start_still_compensates(self):
backend = LifecycleBackend()
executor = ControlledStartExecutor()
controller = postgres_runtime.PostgresController(backend, executor=executor)
controller.tick(0)
executor.submissions[0][1].set_result(
postgres_runtime.ProbeResult(postgres_runtime.ProbeKind.STOPPED, 'offline')
)
controller.tick(0)
self.assertTrue(controller.has_inflight_start)
executor.submissions[1][1].set_result(
postgres_runtime.StartResult(True, 'accepted before close')
)
self.assertTrue(controller.close(timeout_sec=0.2))
self.assertEqual([name for name, _ in executor.submissions], ['probe', 'start', 'stop'])
self.assertEqual(backend.stop_calls, 1)
self.assertTrue(backend.closed)
def test_owned_ready_waits_for_child_then_stops_exactly_once(self):
backend = LifecycleBackend(
probes=[
postgres_runtime.ProbeResult(postgres_runtime.ProbeKind.STOPPED, 'offline'),
postgres_runtime.ProbeResult(postgres_runtime.ProbeKind.READY, 'owned ready', 'epoch'),
],
starts=[postgres_runtime.StartResult(True, 'accepted')],
)
controller = postgres_runtime.PostgresController(
backend,
executor=ImmediateExecutor(),
stable_ready_interval_sec=0,
)
for _ in range(4):
controller.tick(0)
self.assertEqual(controller.state, postgres_runtime.PostgresState.READY)
self.assertTrue(controller._owned_start)
self.assertFalse(controller.has_inflight_start)
source = mock.Mock()
source.source = 'child'
source.stop.side_effect = [False, True]
source.is_running.side_effect = [True, False]
context = {
'lifecycle_phase': lifecycle_authority.PHASE_ACTIVE,
'activation_state': lifecycle_authority.PHASE_ACTIVE,
'start_gate_open': True,
'postgres_controller': controller,
'supervisor_config': {},
}
with mock.patch.object(controller, 'close', wraps=controller.close) as close:
self.assertFalse(supervisor.coordinated_shutdown([source], context))
close.assert_not_called()
self.assertEqual(backend.stop_calls, 0)
self.assertEqual(context['lifecycle_phase'], lifecycle_authority.PHASE_FAILED_HOLD)
self.assertTrue(supervisor.coordinated_shutdown([source], context))
close.assert_called_once()
self.assertEqual(backend.stop_calls, 1)
class RetainedProcess:
def __init__(self, command):
self.command = list(command)
self.closed = False
def command_line(self):
return list(self.command)
def close(self):
self.closed = True
class SupervisorCommandVerificationTests(unittest.TestCase):
def setUp(self):
self.supervisor_path = canonical(APP_DIR / 'supervisor.py')
self.config_path = canonical(APP_DIR / 'config.yaml')
self.bootstrap_path = canonical(APP_DIR / 'runtime_bootstrap.py')
def command(self, lifecycle_mode='foreground'):
command = [
sys.executable,
'-I',
'-S',
'-B',
self.bootstrap_path,
'supervisor',
'--',
f'--runtime-bootstrap-entrypoint={self.supervisor_path}',
f'--config={self.config_path}',
'--with-postgres',
]
if lifecycle_mode == 'background':
command.append('--background-child')
return command
def lifecycle_metadata(self):
return {
'pid': 123,
'process_creation_time': 'fixture',
'executable': canonical(sys.executable),
'supervisor_path': self.supervisor_path,
'config_path': self.config_path,
}
def instance_metadata(self, lifecycle_mode='foreground'):
return {
**self.lifecycle_metadata(),
'lifecycle_mode': lifecycle_mode,
'code_manifest': {},
'code_manifest_sha256': '1' * 64,
'supervisor_sha256': '2' * 64,
'config_sha256': '3' * 64,
}
def verify_instance(self, command, lifecycle_mode='foreground', shutdown=False):
retained = RetainedProcess(command)
metadata = self.instance_metadata(lifecycle_mode)
def digest(path):
return '2' * 64 if canonical(path) == self.supervisor_path else '3' * 64
with mock.patch.object(
supervisor_instance,
'validate_instance_metadata',
return_value=metadata,
), mock.patch.object(
supervisor_instance,
'verify_code_manifest',
), mock.patch.object(
supervisor_instance,
'sha256_file',
side_effect=digest,
), mock.patch.object(
supervisor_instance,
'verify_retained_process',
return_value=retained,
):
result = supervisor_instance.verify_instance_process(
metadata,
allow_config_drift=shutdown,
allow_code_drift=shutdown,
)
return result
def assert_lifecycle_rejected(self, command, pattern):
retained = RetainedProcess(command)
with mock.patch.object(
lifecycle_authority,
'verify_retained_process',
return_value=retained,
):
with self.assertRaisesRegex(lifecycle_authority.LifecycleAuthorityError, pattern):
lifecycle_authority._verify_supervisor_process(self.lifecycle_metadata())
self.assertTrue(retained.closed)
def assert_instance_rejected(self, command, pattern):
retained = RetainedProcess(command)
metadata = self.instance_metadata()
def digest(path):
return '2' * 64 if canonical(path) == self.supervisor_path else '3' * 64
with mock.patch.object(
supervisor_instance,
'validate_instance_metadata',
return_value=metadata,
), mock.patch.object(
supervisor_instance,
'verify_code_manifest',
), mock.patch.object(
supervisor_instance,
'sha256_file',
side_effect=digest,
), mock.patch.object(
supervisor_instance,
'verify_retained_process',
return_value=retained,
):
with self.assertRaisesRegex(supervisor_instance.InstanceMetadataError, pattern):
supervisor_instance.verify_instance_process(metadata)
self.assertTrue(retained.closed)
def test_equals_form_foreground_child_and_shutdown_verification(self):
command = self.command()
retained = RetainedProcess(command)
with mock.patch.object(
lifecycle_authority,
'verify_retained_process',
return_value=retained,
):
lifecycle_authority._verify_supervisor_process(self.lifecycle_metadata())
self.assertTrue(retained.closed)
foreground = self.verify_instance(command)
self.assertFalse(foreground.closed)
foreground.close()
background = self.verify_instance(
self.command('background'),
lifecycle_mode='background',
shutdown=True,
)
self.assertFalse(background.closed)
background.close()
def test_canonical_direct_script_argument_remains_allowed(self):
command = [sys.executable, self.supervisor_path, f'--config={self.config_path}']
retained = RetainedProcess(command)
with mock.patch.object(
lifecycle_authority,
'verify_retained_process',
return_value=retained,
):
lifecycle_authority._verify_supervisor_process(self.lifecycle_metadata())
self.assertTrue(retained.closed)
instance = self.verify_instance(command)
instance.close()
def test_conflicting_or_duplicate_bindings_are_rejected(self):
cases = (
(
[*self.command(), '--runtime-bootstrap-entrypoint', self.supervisor_path],
'exactly one bound supervisor script',
),
(
[*self.command(), '--config', canonical(APP_DIR / 'other.yaml')],
'exactly one bound config argument',
),
)
for command, pattern in cases:
with self.subTest(pattern=pattern):
self.assert_lifecycle_rejected(command, pattern)
self.assert_instance_rejected(command, pattern)
class DirectReadOnlyBytecodeTests(unittest.TestCase):
def test_background_status_in_clean_copy_creates_no_application_bytecode(self):
with tempfile.TemporaryDirectory() as temp_dir:
app_dir = Path(temp_dir) / 'app'
shutil.copytree(
APP_DIR,
app_dir,
ignore=shutil.ignore_patterns('__pycache__', '*.pyc'),
)
environment = os.environ.copy()
environment.pop('PYTHONDONTWRITEBYTECODE', None)
environment.pop('PYTHONPYCACHEPREFIX', None)
completed = subprocess.run(
[
sys.executable,
str(app_dir / 'supervisor.py'),
'--config',
str(Path(temp_dir) / 'missing-config.yaml'),
'--background-status',
],
cwd=temp_dir,
env=environment,
stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
timeout=30,
check=False,
)
self.assertNotEqual(completed.returncode, 0)
self.assertEqual(list(app_dir.rglob('*.pyc')), [])
self.assertEqual(
[path for path in app_dir.rglob('__pycache__') if path.is_dir()],
[],
)
if __name__ == '__main__':
unittest.main()