Extract PostgreSQL, Podman runtime and systemd lifecycle adapters while preserving existing lifecycle and scoring behavior. Update RPM layout and add guarded operator-run lab harnesses. Record successful manual stop/start, promotion, demotion and rewind-only rejoin with pinned installed hashes. The unfenced lab does not validate automatic failover, Pacemaker-managed transitions or divergent-data rewind. Validation: 103 offline tests, original-function differential checks, checksum manifest and whitespace checks passed.
66 lines
3.3 KiB
Python
66 lines
3.3 KiB
Python
#!/usr/bin/env python3
|
|
"""Stop/start harness tests with mocked commands and temporary evidence only."""
|
|
import contextlib
|
|
import importlib.util
|
|
import io
|
|
import os
|
|
from pathlib import Path
|
|
import tempfile
|
|
from types import SimpleNamespace
|
|
import unittest
|
|
from unittest.mock import patch
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
spec = importlib.util.spec_from_file_location('cycle', ROOT / 'scripts/test-elitedesk-cycle.py')
|
|
cycle = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(cycle)
|
|
|
|
|
|
class Cycle(unittest.TestCase):
|
|
def scenario(self, failure=None):
|
|
with tempfile.TemporaryDirectory() as directory, contextlib.ExitStack() as stack:
|
|
state = {'running': True, 'started': 'old'}
|
|
calls = []
|
|
def command(args, **kwargs):
|
|
action = args[-1]; calls.append(action)
|
|
self.assertIn(action, ['validate-all', 'monitor', 'stop', 'start', 'status'])
|
|
if action == failure:
|
|
return SimpleNamespace(returncode=1, stdout='', stderr='injected failure')
|
|
if action == 'stop': state['running'] = False
|
|
if action == 'start': state.update(running=True, started='new')
|
|
rc = 7 if action == 'monitor' and not state['running'] else 0
|
|
return SimpleNamespace(returncode=rc, stdout='', stderr='')
|
|
stack.enter_context(patch.object(cycle.os, 'geteuid', return_value=0))
|
|
stack.enter_context(patch.object(cycle.socket, 'gethostname', return_value='DAVID-ELITEDESK'))
|
|
stack.enter_context(patch.object(cycle.lab, 'BACKUP_ROOT', Path(directory)))
|
|
stack.enter_context(patch.object(cycle.lab, 'cluster', return_value={}))
|
|
stack.enter_context(patch.object(cycle.lab, 'observe', side_effect=lambda:
|
|
dict(system_id='same', started=state['started'])))
|
|
stack.enter_context(patch.object(cycle, 'verify_payload'))
|
|
stack.enter_context(patch.object(cycle, 'owner_command', side_effect=lambda *args:
|
|
'inactive' if args[0] == 'systemctl' else ''))
|
|
stack.enter_context(patch.object(cycle, 'score', side_effect=lambda:
|
|
'100' if state['running'] else '-INFINITY'))
|
|
stack.enter_context(patch.object(cycle.subprocess, 'run', side_effect=command))
|
|
old_mask = os.umask(0o022)
|
|
try:
|
|
with contextlib.redirect_stdout(io.StringIO()):
|
|
if failure:
|
|
with self.assertRaises(RuntimeError): cycle.main()
|
|
self.assertEqual(calls[-1], failure)
|
|
self.assertEqual(calls.count(failure), 1)
|
|
else:
|
|
cycle.main()
|
|
self.assertEqual(calls, ['validate-all','monitor','stop','monitor','start','monitor','status'])
|
|
finally:
|
|
os.umask(old_mask)
|
|
self.assertEqual(len(list(Path(directory).glob('*/results.json'))), 1)
|
|
|
|
def test_successful_cycle(self): self.scenario()
|
|
def test_stop_failure_halts_without_start(self): self.scenario('stop')
|
|
def test_start_failure_halts_without_retry(self): self.scenario('start')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main(verbosity=2)
|