mirror of
https://github.com/openhop-dev/openhop_repeater.git
synced 2026-10-23 13:32:50 +00:00
169 lines
5.6 KiB
Python
169 lines
5.6 KiB
Python
"""Synthetic socket/lifecycle regressions; no subprocesses or network services."""
|
|
|
|
import threading
|
|
import uuid
|
|
from concurrent.futures import ThreadPoolExecutor
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
|
|
from repeater.plugins.ipc import PluginIPCClient, PluginIPCError, PluginIPCServer
|
|
from repeater.plugins.manager import PluginManager, PluginManagerError
|
|
from repeater.plugins.storage import PluginStorage
|
|
|
|
|
|
class SlowManager(PluginManager):
|
|
def __init__(self, root):
|
|
super().__init__(PluginStorage(root))
|
|
self.entered = threading.Event()
|
|
self.release = threading.Event()
|
|
self.consumed = None
|
|
|
|
def install(self, wheel_path):
|
|
self.entered.set()
|
|
assert self.release.wait(3)
|
|
self.consumed = Path(wheel_path).read_bytes()
|
|
return {"id": "demo", "version": "1"}
|
|
|
|
def status(self, plugin_id):
|
|
return {"id": plugin_id, "state": "STOPPED"}
|
|
|
|
|
|
@pytest.fixture
|
|
def service(tmp_path):
|
|
manager = SlowManager(tmp_path / "plugins")
|
|
server = PluginIPCServer(tmp_path / "s", manager)
|
|
server.start()
|
|
try:
|
|
yield manager, server
|
|
finally:
|
|
manager.release.set()
|
|
server.stop()
|
|
|
|
|
|
def test_install_does_not_block_status(service, tmp_path):
|
|
manager, server = service
|
|
wheel = tmp_path / "demo.whl"
|
|
wheel.write_bytes(b"wheel")
|
|
with ThreadPoolExecutor(1) as pool:
|
|
future = pool.submit(PluginIPCClient(server.socket_path).install, str(wheel))
|
|
try:
|
|
assert manager.entered.wait(1)
|
|
assert PluginIPCClient(server.socket_path, timeout=0.2).status("other")["id"] == "other"
|
|
finally:
|
|
manager.release.set()
|
|
assert future.result()["id"] == "demo"
|
|
|
|
|
|
def test_install_has_separate_completion_budget(service, tmp_path):
|
|
manager, server = service
|
|
wheel = tmp_path / "demo.whl"
|
|
wheel.write_bytes(b"wheel")
|
|
client = PluginIPCClient(server.socket_path, timeout=0.05)
|
|
client.operation_timeout = 1
|
|
timer = threading.Timer(0.15, manager.release.set)
|
|
timer.start()
|
|
try:
|
|
assert client.install(str(wheel))["id"] == "demo"
|
|
finally:
|
|
timer.join()
|
|
|
|
|
|
def test_timeout_is_unknown_and_manager_owns_upload(service, tmp_path):
|
|
manager, server = service
|
|
wheel = tmp_path / "demo.whl"
|
|
wheel.write_bytes(b"wheel")
|
|
client = PluginIPCClient(server.socket_path, timeout=0.1)
|
|
client.operation_timeout = 0.1
|
|
with pytest.raises(PluginIPCError) as error:
|
|
client.install(str(wheel))
|
|
assert error.value.code == 504
|
|
assert error.value.outcome == "unknown"
|
|
assert error.value.upload_safe is True
|
|
wheel.unlink()
|
|
manager.release.set()
|
|
# A completion barrier is used rather than sleeping before checking consumption.
|
|
server.stop()
|
|
assert manager.consumed == b"wheel"
|
|
assert not list((manager.storage.root / ".ipc-staging").glob("*"))
|
|
|
|
|
|
def test_slow_capacity_rejects_without_blocking_control(service, tmp_path):
|
|
manager, server = service
|
|
wheel = tmp_path / "demo.whl"
|
|
wheel.write_bytes(b"wheel")
|
|
# Capacity is deliberately one slow operation, with no waiting operation queue.
|
|
with ThreadPoolExecutor(1) as pool:
|
|
future = pool.submit(PluginIPCClient(server.socket_path).install, str(wheel))
|
|
try:
|
|
assert manager.entered.wait(1)
|
|
with pytest.raises(PluginIPCError) as error:
|
|
PluginIPCClient(server.socket_path, timeout=0.2).install(str(wheel))
|
|
assert error.value.code == 503
|
|
assert PluginIPCClient(server.socket_path, timeout=0.2).status("other")["id"] == "other"
|
|
finally:
|
|
manager.release.set()
|
|
future.result()
|
|
|
|
|
|
def test_truncated_completion_is_unknown(tmp_path):
|
|
import socket
|
|
|
|
from repeater.plugins.ipc import _read_line
|
|
|
|
path = Path("/tmp") / f"legacy-{uuid.uuid4().hex}.sock"
|
|
listener = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
listener.bind(str(path))
|
|
listener.listen(1)
|
|
|
|
def legacy():
|
|
with listener.accept()[0] as conn:
|
|
_read_line(conn)
|
|
conn.sendall(b'{"ok":true')
|
|
|
|
worker = threading.Thread(target=legacy)
|
|
worker.start()
|
|
try:
|
|
with pytest.raises(PluginIPCError) as error:
|
|
PluginIPCClient(path).install("unused.whl")
|
|
assert error.value.code == 504
|
|
assert error.value.outcome == "unknown"
|
|
assert error.value.upload_safe is False
|
|
finally:
|
|
worker.join(1)
|
|
listener.close()
|
|
|
|
|
|
def test_manager_install_does_not_hold_global_control_lock(tmp_path, monkeypatch):
|
|
entered, release = threading.Event(), threading.Event()
|
|
storage = PluginStorage(tmp_path)
|
|
storage.write_state("other", {"enabled": True, "version": "1"})
|
|
|
|
def install(path, manifest):
|
|
entered.set()
|
|
assert release.wait(3)
|
|
return {"id": "demo", "version": "1"}
|
|
|
|
runtime = SimpleNamespace(
|
|
install_wheel=install,
|
|
stop=lambda *a, **k: None,
|
|
status_dict=lambda pid: {"id": pid},
|
|
_runtime_state={},
|
|
)
|
|
manager = PluginManager(storage, runtime)
|
|
monkeypatch.setattr(
|
|
"repeater.plugins.manager.load_manifest_from_wheel", lambda p: SimpleNamespace(id="demo")
|
|
)
|
|
with ThreadPoolExecutor(2) as pool:
|
|
installing = pool.submit(manager.install, tmp_path / "demo.whl")
|
|
try:
|
|
assert entered.wait(1)
|
|
assert pool.submit(manager.disable, "other").result(timeout=0.2)["id"] == "other"
|
|
with pytest.raises(PluginManagerError) as error:
|
|
manager.disable("demo")
|
|
assert error.value.code == 409
|
|
finally:
|
|
release.set()
|
|
installing.result()
|