"""Shared fixtures for docker-image integration tests. Tests in this directory build the image with the current ``Dockerfile`` and exercise it via ``docker run``. They skip when Docker is unavailable (e.g. on developer laptops without a daemon). Override the image with ``HERMES_TEST_IMAGE`` env var to point at a pre-built image (faster local iteration); otherwise the ``built_image`` fixture builds the repo's Dockerfile once per session. Docker tests need longer timeouts than the suite default (30s), so every test under this directory is granted a 180s default via ``pytest.mark.timeout`` applied at collection time. """ from __future__ import annotations import os import shutil import subprocess from collections.abc import Iterator import pytest IMAGE_TAG = os.environ.get("HERMES_TEST_IMAGE", "hermes-agent-harness:latest") def _docker_available() -> bool: """Return True iff a docker CLI is on PATH and the daemon answers.""" if shutil.which("docker") is None: return False try: r = subprocess.run( ["docker", "info"], capture_output=True, timeout=5, ) return r.returncode == 0 except (subprocess.TimeoutExpired, OSError): return False def pytest_collection_modifyitems(config, items): # noqa: D401 - pytest hook """Apply docker-suite policy: timeout bump + skip on missing docker.""" docker_ok = _docker_available() skip_docker = pytest.mark.skip( reason="Docker not available or daemon not running", ) extend_timeout = pytest.mark.timeout(180) for item in items: if "tests/docker/" not in str(item.fspath).replace(os.sep, "/"): continue item.add_marker(extend_timeout) if not docker_ok: item.add_marker(skip_docker) @pytest.fixture(scope="session") def built_image() -> str: """Build the image once per test session. Override with ``HERMES_TEST_IMAGE`` env var to point at a pre-built image (faster local iteration). """ if os.environ.get("HERMES_TEST_IMAGE"): return IMAGE_TAG repo_root = os.path.abspath( os.path.join(os.path.dirname(__file__), "..", ".."), ) result = subprocess.run( ["docker", "build", "-t", IMAGE_TAG, repo_root], capture_output=True, text=True, timeout=1200, ) assert result.returncode == 0, ( f"docker build failed:\n{result.stderr[-2000:]}" ) return IMAGE_TAG @pytest.fixture def container_name(request) -> Iterator[str]: """Generate a unique container name and ensure cleanup on test exit.""" safe = request.node.name.replace("[", "_").replace("]", "_") name = f"hermes-test-{safe}" yield name subprocess.run( ["docker", "rm", "-f", name], capture_output=True, timeout=10, )