4b6817381b
Benchmark image — build + push to ECR (any adapter) / build + push (push) Waiting to run
CI / quality (ubuntu-latest) (push) Waiting to run
CI / test (tools-runtime) (push) Waiting to run
CI / test (e2e-general) (push) Waiting to run
CI / test (cli-runtime) (push) Waiting to run
CI / test (e2e-provider-and-openclaw) (push) Waiting to run
CI / test (integrations-and-misc) (push) Waiting to run
CI / coverage-report (push) Blocked by required conditions
CI / test-kubernetes (push) Waiting to run
CI / should-run-thorough (push) Waiting to run
CI / test-thorough (cloudwatch-demo) (push) Blocked by required conditions
CI / test-thorough (flink-ecs) (push) Blocked by required conditions
CI / test-thorough (upstream-lambda) (push) Blocked by required conditions
CI / test-thorough (prefect-ecs-fargate) (push) Blocked by required conditions
CodeQL / Analyze (python) (push) Waiting to run
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Blocked by required conditions
Release / publish-release (push) Blocked by required conditions
Release / publish-main-release (push) Blocked by required conditions
Release / prepare (push) Waiting to run
Release / verify (push) Blocked by required conditions
Release / build-python-dist (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Blocked by required conditions
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Waiting to run
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Waiting to run
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Waiting to run
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
158 lines
5.1 KiB
Python
158 lines
5.1 KiB
Python
"""Tests for the per-PID resource probe (issue #1489)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import pathlib
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
from collections.abc import Iterator
|
|
from datetime import UTC, datetime
|
|
from types import SimpleNamespace
|
|
from unittest.mock import patch
|
|
|
|
import psutil
|
|
import pytest
|
|
|
|
from tools.system.fleet_monitoring.probe import (
|
|
ProcessSnapshot,
|
|
probe,
|
|
process_has_open_codex_rollout,
|
|
)
|
|
|
|
_REPO_ROOT = pathlib.Path(__file__).resolve().parents[2]
|
|
_PROBE_MODULE = _REPO_ROOT / "tools" / "system" / "fleet_monitoring" / "probe.py"
|
|
_SOURCE_ROOTS = (
|
|
"cli",
|
|
"config",
|
|
"core",
|
|
"deployment",
|
|
"integrations",
|
|
"platform",
|
|
"services",
|
|
"tools",
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def busy_process() -> Iterator[int]:
|
|
"""Spawn a subprocess burning CPU in a tight loop; yields its PID.
|
|
The warmup sleep ensures the process has accumulated enough CPU time for
|
|
psutil.cpu_percent(interval>0) to measure a non-zero delta.
|
|
"""
|
|
proc = subprocess.Popen(
|
|
[sys.executable, "-c", "while True: pass"],
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL,
|
|
)
|
|
|
|
time.sleep(0.1)
|
|
|
|
yield proc.pid
|
|
|
|
proc.terminate()
|
|
try:
|
|
proc.wait(timeout=5)
|
|
except subprocess.TimeoutExpired:
|
|
proc.kill()
|
|
proc.wait()
|
|
|
|
|
|
def test_probe_returns_snapshot_for_self() -> None:
|
|
"""Probing the current Python process must return a populated snapshot."""
|
|
snap = probe(os.getpid(), cpu_interval=0.0)
|
|
|
|
assert snap is not None
|
|
assert isinstance(snap, ProcessSnapshot)
|
|
assert snap.pid == os.getpid()
|
|
assert snap.rss_mb > 0.0, "interpreter + pytest should occupy non-zero RSS"
|
|
assert snap.status, "status should be a non-empty string (e.g. 'running')"
|
|
assert isinstance(snap.started_at, datetime)
|
|
assert snap.started_at.tzinfo == UTC, "started_at must be tz-aware UTC"
|
|
if sys.platform != "win32":
|
|
assert snap.num_fds is not None and snap.num_fds > 0, (
|
|
"POSIX systems should always report a positive FD count"
|
|
)
|
|
|
|
|
|
def test_probe_returns_none_for_missing_pid() -> None:
|
|
"""Probing a PID that does not exist returns None, never raises."""
|
|
# 2**31 - 1 is far above any realistic allocated PID on Linux/macOS
|
|
# (kernel.pid_max is typically 32768 or 4194304).
|
|
assert probe(2**31 - 1, cpu_interval=0.0) is None
|
|
|
|
|
|
def test_probe_returns_none_for_access_denied_process() -> None:
|
|
"""``cpu_percent()`` and ``memory_info()`` raise ``psutil.AccessDenied``
|
|
for processes owned by another user on macOS and on Linux setups
|
|
with restricted ``/proc``. The wiring layer treats both that and a
|
|
truly missing PID as "no snapshot this tick" — the function must
|
|
never let ``AccessDenied`` escape.
|
|
"""
|
|
with patch.object(
|
|
psutil.Process,
|
|
"memory_info",
|
|
side_effect=psutil.AccessDenied(pid=os.getpid()),
|
|
):
|
|
assert probe(os.getpid(), cpu_interval=0.0) is None
|
|
|
|
|
|
def test_process_has_open_codex_rollout_matches_rollout_jsonl() -> None:
|
|
with patch.object(
|
|
psutil.Process,
|
|
"open_files",
|
|
return_value=[
|
|
SimpleNamespace(path="/tmp/unrelated.log"),
|
|
SimpleNamespace(path="/Users/me/.codex/rollout-2026-05-17.jsonl"),
|
|
],
|
|
):
|
|
assert process_has_open_codex_rollout(os.getpid()) is True
|
|
|
|
|
|
def test_process_has_open_codex_rollout_returns_false_when_inaccessible() -> None:
|
|
with patch.object(
|
|
psutil.Process,
|
|
"open_files",
|
|
side_effect=psutil.AccessDenied(pid=os.getpid()),
|
|
):
|
|
assert process_has_open_codex_rollout(os.getpid()) is False
|
|
|
|
|
|
def test_psutil_is_not_imported_outside_probe_module() -> None:
|
|
"""Acceptance criterion #3: ``psutil`` must stay confined to
|
|
``tools/system/fleet_monitoring/probe.py`` so the dependency surface is explicit. A
|
|
static scan over runtime package trees catches future regressions
|
|
deterministically — runtime import-graph checks would be flaky
|
|
against lazy-import patterns the codebase already uses elsewhere.
|
|
"""
|
|
leaks: list[str] = []
|
|
py_files: list[pathlib.Path] = []
|
|
for root_name in _SOURCE_ROOTS:
|
|
py_files.extend(sorted((_REPO_ROOT / root_name).rglob("*.py")))
|
|
for py_file in py_files:
|
|
if py_file == _PROBE_MODULE:
|
|
continue
|
|
text = py_file.read_text(encoding="utf-8")
|
|
for needle in ("import psutil", "from psutil"):
|
|
if needle in text:
|
|
leaks.append(f"{py_file.relative_to(_REPO_ROOT)} contains {needle!r}")
|
|
break
|
|
|
|
assert not leaks, (
|
|
"psutil leaked into modules other than tools/system/fleet_monitoring/probe.py:\n "
|
|
+ "\n ".join(leaks)
|
|
)
|
|
|
|
|
|
def test_probe_returns_nonzero_cpu_for_busy_process(busy_process: int) -> None:
|
|
"""Regression: cpu_percent(interval>0) returned 0.0 when called inside proc.oneshot()
|
|
because both internal readings hit the same cache (#1950). Probing a busy subprocess
|
|
with a positive interval must yield a non-zero value now that the call lives
|
|
outside the oneshot block.
|
|
"""
|
|
snap = probe(busy_process, cpu_interval=0.1)
|
|
|
|
assert snap is not None
|
|
assert snap.cpu_percent > 0.0
|