124 lines
4.4 KiB
Python
124 lines
4.4 KiB
Python
#!/usr/bin/env python3
|
|
|
|
from __future__ import annotations
|
|
|
|
import importlib.util
|
|
import io
|
|
import socket
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
import warnings
|
|
from pathlib import Path
|
|
from contextlib import redirect_stdout
|
|
from unittest.mock import patch
|
|
|
|
|
|
SERVICES_PATH = Path(__file__).with_name("services.py")
|
|
SPEC = importlib.util.spec_from_file_location("aiw_services", SERVICES_PATH)
|
|
services = importlib.util.module_from_spec(SPEC)
|
|
assert SPEC.loader is not None
|
|
sys.modules[SPEC.name] = services
|
|
SPEC.loader.exec_module(services)
|
|
|
|
|
|
def sample_manifest() -> dict:
|
|
return {
|
|
"services": {
|
|
"alpha": {
|
|
"enabled": True,
|
|
"kind": "process",
|
|
"command": ["python3", "-c", "import time; time.sleep(60)"],
|
|
"groups": ["core"],
|
|
},
|
|
"beta": {
|
|
"enabled": True,
|
|
"kind": "process",
|
|
"command": ["python3", "-c", "import time; time.sleep(60)"],
|
|
"groups": ["capture"],
|
|
"depends_on": ["alpha"],
|
|
},
|
|
"disabled": {
|
|
"enabled": False,
|
|
"kind": "process",
|
|
"command": ["python3", "-c", "import time; time.sleep(60)"],
|
|
"groups": ["core"],
|
|
},
|
|
}
|
|
}
|
|
|
|
|
|
class ServiceManagerTests(unittest.TestCase):
|
|
def test_select_services_excludes_disabled_by_default(self) -> None:
|
|
selected = services.select_services(sample_manifest(), names=[], group=None)
|
|
|
|
self.assertEqual([item.name for item in selected], ["alpha", "beta"])
|
|
|
|
def test_select_services_can_include_disabled_for_status(self) -> None:
|
|
selected = services.select_services(sample_manifest(), names=[], group=None, include_disabled=True)
|
|
|
|
self.assertEqual([item.name for item in selected], ["alpha", "beta", "disabled"])
|
|
|
|
def test_select_services_filters_by_group(self) -> None:
|
|
selected = services.select_services(sample_manifest(), names=[], group="capture")
|
|
|
|
self.assertEqual([item.name for item in selected], ["beta"])
|
|
|
|
def test_health_ok_tcp_reports_open_port(self) -> None:
|
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server:
|
|
server.bind(("127.0.0.1", 0))
|
|
server.listen(1)
|
|
port = server.getsockname()[1]
|
|
|
|
ok, detail = services.health_ok({"health": {"type": "tcp", "host": "127.0.0.1", "port": port}})
|
|
|
|
self.assertTrue(ok)
|
|
self.assertIn(f"127.0.0.1:{port}", detail)
|
|
|
|
def test_read_pid_ignores_invalid_pid_file(self) -> None:
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
pid_dir = Path(tmp) / "pids"
|
|
target = pid_dir / "fidelity"
|
|
target.mkdir(parents=True)
|
|
(target / "alpha.pid").write_text("not-a-pid\n", encoding="utf-8")
|
|
|
|
with patch.object(services, "PID_DIR", pid_dir):
|
|
self.assertIsNone(services.read_pid("fidelity", "alpha"))
|
|
|
|
def test_start_and_stop_managed_process(self) -> None:
|
|
manifest = {
|
|
"services": {
|
|
"sleeper": {
|
|
"enabled": True,
|
|
"kind": "process",
|
|
"command": [sys.executable, "-c", "import time; time.sleep(60)"],
|
|
"restart": "never",
|
|
}
|
|
}
|
|
}
|
|
ref = services.ServiceRef("sleeper", manifest["services"]["sleeper"])
|
|
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
root = Path(tmp)
|
|
with patch.object(services, "PID_DIR", root / "pids"), \
|
|
patch.object(services, "LOG_DIR", root / "logs"), \
|
|
patch.object(services, "STATE_DIR", root / "state"):
|
|
started: set[str] = set()
|
|
services.ensure_runtime()
|
|
with warnings.catch_warnings():
|
|
warnings.simplefilter("ignore", ResourceWarning)
|
|
with redirect_stdout(io.StringIO()):
|
|
services.start_service("test", ref, manifest, started)
|
|
|
|
pid = services.read_pid("test", "sleeper")
|
|
self.assertTrue(services.is_running(pid))
|
|
self.assertIn("sleeper", started)
|
|
|
|
with redirect_stdout(io.StringIO()):
|
|
services.stop_service("test", ref)
|
|
self.assertFalse(services.is_running(pid))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|