Files
hermes-agent/tests/tools/test_docker_cgroup_limits.py
teknium1 5f6b1d251f test: purge low-value tests, lane py17 (495 removed)
Change-detectors, tautologies, source-reading tests, redundant duplicates,
mock-echo tests and dead/unrunnable tests. Per-test rationale in the lane
ledger (category + reason for every removal).
2026-09-23 03:15:26 -07:00

140 lines
6.2 KiB
Python

"""Tests for cgroup resource-limit gating in the docker backend.
On hosts where the cgroup v2 cpu/memory/pids controllers are not delegated
(e.g. unprivileged Proxmox LXCs), passing ``--cpus``/``--memory``/``--pids-limit``
to ``docker run`` fails every container start with OCI runtime error / exit 126.
``_cgroup_limits_available`` probes once and the resource flags are gated on it,
so the sandbox degrades gracefully instead of failing.
"""
import subprocess
import pytest
import tools.environments.docker as docker_env
@pytest.fixture(autouse=True)
def _reset_cgroup_cache():
"""The probe results are cached in module-level globals; reset per test."""
docker_env._cgroup_limits_ok = None
docker_env._storage_opt_ok = None
yield
docker_env._cgroup_limits_ok = None
docker_env._storage_opt_ok = None
def test_probe_returns_true_when_container_starts(monkeypatch):
monkeypatch.setattr(docker_env, "find_docker", lambda: "/usr/bin/docker")
captured = {}
def _run(cmd, *a, **k):
captured["cmd"] = cmd
return subprocess.CompletedProcess(cmd, 0, stdout="", stderr="")
monkeypatch.setattr(docker_env.subprocess, "run", _run)
assert docker_env._cgroup_limits_available("hermes-agent:latest") is True
# Probes all three controllers together against the real sandbox image.
assert "--cpus" in captured["cmd"]
assert "--memory" in captured["cmd"]
assert "--pids-limit" in captured["cmd"]
assert "hermes-agent:latest" in captured["cmd"]
def test_probe_result_is_cached(monkeypatch):
monkeypatch.setattr(docker_env, "find_docker", lambda: "/usr/bin/docker")
calls = []
def _run(cmd, *a, **k):
calls.append(cmd)
return subprocess.CompletedProcess(cmd, 0, stdout="", stderr="")
monkeypatch.setattr(docker_env.subprocess, "run", _run)
docker_env._cgroup_limits_available("img")
docker_env._cgroup_limits_available("img")
docker_env._cgroup_limits_available("img")
assert len(calls) == 1 # probe runs once, then cached
def test_probe_definitive_cgroup_failure_is_cached(monkeypatch):
"""A daemon rejection that names cgroups IS a host property; caching it is
the point of the probe, so subsequent spawns do not pay it again (#116162
only stops caching failures that say nothing about cgroups)."""
monkeypatch.setattr(docker_env, "find_docker", lambda: "/usr/bin/docker")
calls = []
def _run(cmd, *a, **k):
calls.append(cmd)
return subprocess.CompletedProcess(
cmd, 126, stdout="", stderr="docker: Error response from daemon: failed to "
"create task for container: OCI runtime create failed: error setting cgroup "
"config for procHooks process: permission denied")
monkeypatch.setattr(docker_env.subprocess, "run", _run)
assert docker_env._cgroup_limits_available("img") is False
assert docker_env._cgroup_limits_available("img") is False
assert len(calls) == 1
@pytest.mark.parametrize(
"info, create, cached",
[
# `docker info` failing (daemon cold-start) is transient: not "not overlay2".
((1, "", "Cannot connect to the Docker daemon"), None, None),
# The daemon rejecting --storage-opt is a host property: cached False.
((0, "overlay2\n", ""), (125, "", "Error response from daemon: --storage-opt is "
"supported only for overlay2 with pquota"), False),
# A create failure that never reached the storage check (pull error) is transient.
((0, "overlay2\n", ""), (125, "", "Unable to find image 'hello-world:latest'"), None),
],
)
def test_storage_opt_probe_caches_only_definitive_answers(monkeypatch, info, create, cached):
"""``_storage_opt_supported`` must not latch disk quota off for the process on a
failure that says nothing about pquota support (#116162)."""
monkeypatch.setattr(docker_env, "find_docker", lambda: "/usr/bin/docker")
def _run(cmd, *a, **k):
rc, out, err = info if cmd[1] == "info" else create
return subprocess.CompletedProcess(cmd, rc, stdout=out, stderr=err)
monkeypatch.setattr(docker_env.subprocess, "run", _run)
assert docker_env.DockerEnvironment._storage_opt_supported() is False
assert docker_env._storage_opt_ok is cached
def test_transient_probe_failure_recovers_on_next_spawn(monkeypatch):
"""E2e through DockerEnvironment: the probe timing out on spawn one (auto-pull
past the 60s timeout, daemon cold-start) leaves THAT container unlimited but
must not disable limits process-wide. Spawn two re-probes, succeeds, and its
docker run argv carries --cpus/--memory/--pids-limit."""
monkeypatch.setattr(docker_env, "find_docker", lambda: "/usr/bin/docker")
calls, probe_calls = [], []
def _run(cmd, *a, **k):
calls.append(cmd)
if isinstance(cmd, list) and len(cmd) > 1 and cmd[1] == "run" and "--rm" in cmd:
# the throwaway cgroup probe (`run --rm ... sleep 0`), not the real `run -d`
probe_calls.append(cmd)
if len(probe_calls) == 1:
raise subprocess.TimeoutExpired(cmd, 60)
return subprocess.CompletedProcess(cmd, 0, stdout="", stderr="")
if isinstance(cmd, list) and len(cmd) > 1 and cmd[1] == "version":
return subprocess.CompletedProcess(cmd, 0, stdout="Docker version", stderr="")
if isinstance(cmd, list) and len(cmd) > 1 and cmd[1] == "run" and "-d" in cmd:
return subprocess.CompletedProcess(cmd, 0, stdout="fake-container-id\n", stderr="")
return subprocess.CompletedProcess(cmd, 0, stdout="", stderr="")
monkeypatch.setattr(docker_env.subprocess, "run", _run)
docker_env.DockerEnvironment(image="img", cpu=1.0, memory=512, task_id="t1")
docker_env.DockerEnvironment(image="img", cpu=1.0, memory=512, task_id="t2")
run_argvs = [c for c in calls
if isinstance(c, list) and len(c) > 1 and c[1] == "run" and "-d" in c]
assert len(run_argvs) == 2
assert "--cpus" not in run_argvs[0] and "--memory" not in run_argvs[0]
assert "--cpus" in run_argvs[1] and "1.0" in run_argvs[1]
assert "--memory" in run_argvs[1] and "512m" in run_argvs[1]
assert "--pids-limit" in run_argvs[1]
assert len(probe_calls) == 2 # re-probed, not latched off the timeout