mirror of
https://github.com/agentscope-ai/ReMe.git
synced 2026-10-13 04:01:35 +00:00
Some checks failed
CI / Python packages / Build and verify distributions (push) Waiting to run
CI / Python quality / GitHub Actions (push) Waiting to run
CI / Python quality / Pre-commit (push) Waiting to run
CI / Python tests / Unit Tests - py3.11 (push) Waiting to run
CI / Python tests / Unit Tests - py3.12 (push) Waiting to run
CI / Python tests / Unit Tests - py3.13 (push) Waiting to run
CI / Python tests / Unit Tests - py3.14 (push) Waiting to run
CI / Windows / CLI smoke - py3.11 (push) Waiting to run
Deploy / Documentation / Build documentation (push) Waiting to run
Deploy / Documentation / deploy (push) Blocked by required conditions
CI and Release / Docker / Build and test / amd64 (push) Waiting to run
CI and Release / Docker / Build and test / arm64 (push) Waiting to run
CI and Release / Docker / Publish multi-platform tags (push) Blocked by required conditions
Security / CodeQL / Analyze javascript-typescript (push) Waiting to run
Security / CodeQL / Analyze python (push) Waiting to run
CI / ReMe Studio / Studio checks (push) Has been cancelled
* feat(docker): add container deployment and multi-platform release workflow * fix(llm): initialize providers on model access and preserve runtime injection * test(llm): keep rejected model updates independent of credentials
247 lines
8.8 KiB
Python
247 lines
8.8 KiB
Python
"""Tests for the psutil-based service discovery helpers in ``service_utils``.
|
|
|
|
Covers the cross-platform replacements for the old ``lsof`` / ``pgrep``
|
|
shell-outs:
|
|
|
|
* ``_pid_on_port`` — real integration (open a listening socket, find its
|
|
PID) plus a mock test proving inaccessible processes are skipped, not
|
|
crashed on.
|
|
* ``_scan_reme_procs`` — mock ``psutil.process_iter`` to assert cmdline
|
|
parsing, defaulting, filtering, and AccessDenied tolerance.
|
|
"""
|
|
|
|
# pylint: disable=protected-access,missing-function-docstring,unused-argument
|
|
|
|
import os
|
|
import json
|
|
import socket
|
|
from types import SimpleNamespace
|
|
|
|
import psutil
|
|
import pytest
|
|
|
|
from reme.constants import normalize_connect_host
|
|
from reme.utils import service_utils as su
|
|
|
|
# ----------------------------------------------------------------------
|
|
# Fakes for mocking psutil.process_iter
|
|
# ----------------------------------------------------------------------
|
|
|
|
|
|
def _conn(port: int, status=psutil.CONN_LISTEN, has_laddr: bool = True):
|
|
return SimpleNamespace(status=status, laddr=SimpleNamespace(port=port) if has_laddr else None)
|
|
|
|
|
|
class _FakeProc:
|
|
"""Stand-in for a psutil.Process yielded by process_iter."""
|
|
|
|
def __init__(self, pid: int, *, cmdline=None, conns=None, conn_exc=None):
|
|
self.info = {"pid": pid, "cmdline": cmdline if cmdline is not None else []}
|
|
self._conns = conns or []
|
|
self._conn_exc = conn_exc
|
|
|
|
def net_connections(self, kind="tcp"):
|
|
if self._conn_exc is not None:
|
|
raise self._conn_exc
|
|
return self._conns
|
|
|
|
|
|
class _RaisingInfoProc:
|
|
"""Process whose `.info` access raises (simulates AccessDenied in iteration)."""
|
|
|
|
def __init__(self, exc):
|
|
self._exc = exc
|
|
|
|
@property
|
|
def info(self):
|
|
raise self._exc
|
|
|
|
|
|
def _patch_iter(monkeypatch, procs):
|
|
monkeypatch.setattr(su.psutil, "process_iter", lambda attrs=None: iter(procs))
|
|
|
|
|
|
# ----------------------------------------------------------------------
|
|
# _pid_on_port
|
|
# ----------------------------------------------------------------------
|
|
|
|
|
|
def test_pid_on_port_finds_own_listening_socket():
|
|
"""Real integration: a listening socket is attributed to this process's PID."""
|
|
srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
srv.bind(("127.0.0.1", 0))
|
|
srv.listen(1)
|
|
port = srv.getsockname()[1]
|
|
try:
|
|
assert su._pid_on_port(port) == os.getpid()
|
|
finally:
|
|
srv.close()
|
|
|
|
|
|
def test_pid_on_port_none_when_nobody_listening():
|
|
"""A port with no listener resolves to None."""
|
|
# Grab then immediately release an ephemeral port to get a number that is
|
|
# (almost certainly) unbound right now.
|
|
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
s.bind(("127.0.0.1", 0))
|
|
free_port = s.getsockname()[1]
|
|
s.close()
|
|
assert su._pid_on_port(free_port) is None
|
|
|
|
|
|
def test_pid_on_port_skips_inaccessible_processes(monkeypatch):
|
|
"""AccessDenied on one process must not abort the scan of the rest."""
|
|
procs = [
|
|
_FakeProc(111, conn_exc=psutil.AccessDenied(pid=111)),
|
|
_FakeProc(222, conn_exc=psutil.NoSuchProcess(pid=222)),
|
|
_FakeProc(333, conns=[_conn(9999)]),
|
|
]
|
|
_patch_iter(monkeypatch, procs)
|
|
assert su._pid_on_port(9999) == 333
|
|
|
|
|
|
def test_pid_on_port_ignores_non_listen_and_mismatched(monkeypatch):
|
|
"""Only LISTEN sockets on the exact port match."""
|
|
procs = [
|
|
_FakeProc(1, conns=[_conn(9999, status=psutil.CONN_ESTABLISHED)]), # right port, wrong state
|
|
_FakeProc(2, conns=[_conn(8888)]), # listening, wrong port
|
|
_FakeProc(3, conns=[_conn(9999, has_laddr=False)]), # no laddr
|
|
]
|
|
_patch_iter(monkeypatch, procs)
|
|
assert su._pid_on_port(9999) is None
|
|
|
|
|
|
# ----------------------------------------------------------------------
|
|
# _scan_reme_procs
|
|
# ----------------------------------------------------------------------
|
|
|
|
|
|
def test_scan_reme_procs_parses_host_and_port(monkeypatch):
|
|
procs = [
|
|
_FakeProc(
|
|
123,
|
|
cmdline=["python", "-m", "reme.reme", "start", "service.host=0.0.0.0", "service.port=8123"],
|
|
),
|
|
]
|
|
_patch_iter(monkeypatch, procs)
|
|
assert su._scan_reme_procs() == [(123, "0.0.0.0", 8123)]
|
|
|
|
|
|
def test_scan_reme_procs_accepts_prefixed_cli_arguments(monkeypatch):
|
|
procs = [
|
|
_FakeProc(
|
|
123,
|
|
cmdline=["reme", "start", "--service.host=0.0.0.0", "-service.port=8123"],
|
|
),
|
|
]
|
|
_patch_iter(monkeypatch, procs)
|
|
assert su._scan_reme_procs() == [(123, "0.0.0.0", 8123)]
|
|
|
|
|
|
def test_scan_reme_procs_defaults_when_args_absent(monkeypatch):
|
|
procs = [_FakeProc(7, cmdline=["reme", "start"])]
|
|
_patch_iter(monkeypatch, procs)
|
|
assert su._scan_reme_procs() == [(7, su.REME_DEFAULT_HOST, su.REME_DEFAULT_PORT)]
|
|
|
|
|
|
def test_scan_reme_procs_filters_unrelated(monkeypatch):
|
|
procs = [
|
|
_FakeProc(1, cmdline=["reme", "find_reme"]), # no 'start' token
|
|
_FakeProc(2, cmdline=["python", "-m", "http.server", "start"]), # no reme token
|
|
_FakeProc(3, cmdline=[]), # empty cmdline
|
|
]
|
|
_patch_iter(monkeypatch, procs)
|
|
assert not su._scan_reme_procs()
|
|
|
|
|
|
def test_scan_reme_procs_ignores_non_digit_port(monkeypatch):
|
|
"""A malformed service.port= falls back to the default port, not a crash."""
|
|
procs = [_FakeProc(9, cmdline=["reme", "start", "service.port=notaport"])]
|
|
_patch_iter(monkeypatch, procs)
|
|
assert su._scan_reme_procs() == [(9, su.REME_DEFAULT_HOST, su.REME_DEFAULT_PORT)]
|
|
|
|
|
|
def test_scan_reme_procs_skips_access_denied(monkeypatch):
|
|
"""A process that denies cmdline access is skipped, others still scanned."""
|
|
procs = [
|
|
_RaisingInfoProc(psutil.AccessDenied(pid=99)),
|
|
_FakeProc(5, cmdline=["reme", "start"]),
|
|
]
|
|
_patch_iter(monkeypatch, procs)
|
|
assert su._scan_reme_procs() == [(5, su.REME_DEFAULT_HOST, su.REME_DEFAULT_PORT)]
|
|
|
|
|
|
def test_connect_host_converts_wildcard_bind_address():
|
|
assert normalize_connect_host("0.0.0.0") == "127.0.0.1"
|
|
assert normalize_connect_host("192.0.2.10") == "192.0.2.10"
|
|
|
|
|
|
def test_running_app_config_preserves_plugins(monkeypatch):
|
|
"""Process replay exposes the full config while the compatibility helper returns service only."""
|
|
monkeypatch.setattr(su, "_reme_start_argv", lambda: [["config=example"]])
|
|
monkeypatch.setattr("reme.config.parse_args", lambda *_args: ("start", {"config": "example"}))
|
|
monkeypatch.setattr(
|
|
"reme.config.resolve_app_config",
|
|
lambda **_kwargs: {
|
|
"plugins": ["example"],
|
|
"service": {"backend": "plugin-client", "port": 9911},
|
|
},
|
|
)
|
|
|
|
assert su.running_app_config() == {
|
|
"plugins": ["example"],
|
|
"service": {"backend": "plugin-client", "port": 9911},
|
|
}
|
|
assert su.running_service_config() == {"backend": "plugin-client", "port": 9911}
|
|
|
|
|
|
def test_discovery_ignores_container_init_and_replays_real_cli(monkeypatch):
|
|
"""Tini's original argv omits environment overrides applied by the entrypoint."""
|
|
procs = [
|
|
_FakeProc(1, cmdline=["/usr/bin/tini", "--", "python", "/usr/local/lib/reme_container.py", "start"]),
|
|
_FakeProc(2, cmdline=["/opt/venv/bin/python", "/opt/venv/bin/reme", "start", 'service={"port":8124}']),
|
|
]
|
|
_patch_iter(monkeypatch, procs)
|
|
assert su._reme_start_argv() == [['service={"port":8124}']]
|
|
assert su.running_service_config()["port"] == 8124
|
|
assert su._scan_reme_procs() == [(2, su.REME_DEFAULT_HOST, 8124)]
|
|
|
|
|
|
def test_discovery_resolves_mounted_config_port(tmp_path, monkeypatch):
|
|
"""find_reme follows ports from JSON configs as well as dot-notation overrides."""
|
|
config = tmp_path / "reme.json"
|
|
config.write_text(json.dumps({"service": {"backend": "http", "host": "0.0.0.0", "port": 8125}}))
|
|
_patch_iter(monkeypatch, [_FakeProc(2, cmdline=["reme", "start", f"config={config}"])])
|
|
assert su._scan_reme_procs() == [(2, "0.0.0.0", 8125)]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"command",
|
|
[
|
|
["reme", "--start"],
|
|
["python3.11", "-u", "-m", "reme.reme", "-start"],
|
|
["/opt/venv/bin/python", "/opt/venv/bin/reme", "start"],
|
|
["C:\\Python\\python.exe", "C:\\Python\\Scripts\\reme.exe", "start"],
|
|
],
|
|
)
|
|
def test_discovery_accepts_cli_launchers_and_action_prefixes(command, monkeypatch):
|
|
_patch_iter(monkeypatch, [_FakeProc(7, cmdline=command)])
|
|
assert su._reme_start_argv() == [[]]
|
|
assert su._scan_reme_procs() == [(7, su.REME_DEFAULT_HOST, su.REME_DEFAULT_PORT)]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"command",
|
|
[
|
|
["tini", "--", "reme", "start"],
|
|
["dumb-init", "reme", "start"],
|
|
["python", "/usr/local/lib/reme_container.py", "start"],
|
|
["python", "-c", "reme.py", "start"],
|
|
["other-reme-app", "start"],
|
|
],
|
|
)
|
|
def test_discovery_filters_supervisors_and_unrelated_commands(command, monkeypatch):
|
|
_patch_iter(monkeypatch, [_FakeProc(7, cmdline=command)])
|
|
assert not su._reme_start_argv()
|
|
assert not su._scan_reme_procs()
|