#!/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)