mirror of
https://github.com/lobaro/restic-backup-docker.git
synced 2026-11-13 15:45:22 +00:00
test: scaffold pytest suite with mise, uv and Task
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
31f06b5d9b
commit
b94795ee8d
8 files changed
+511
No files matched your search
@@ -0,0 +1,107 @@
|
||||
"""Shared fixtures and feature gating for the container test suite."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import uuid
|
||||
from collections.abc import Iterator
|
||||
from pathlib import Path
|
||||
|
||||
import docker
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
IMAGE = os.environ.get("IMAGE", "restic-backup-docker:test")
|
||||
REQUIRED_FEATURES_FILE = Path(
|
||||
os.environ.get("REQUIRED_FEATURES_FILE", Path(__file__).with_name("required-features.txt"))
|
||||
)
|
||||
RUN_ID = uuid.uuid4().hex[:8]
|
||||
|
||||
# Shell probe run once in a throwaway container; one line per detected feature.
|
||||
FEATURE_PROBE = """
|
||||
grep -q RESTIC_STREAM /bin/backup && echo stream
|
||||
command -v docker >/dev/null && echo docker-cli
|
||||
command -v bash >/dev/null && echo bash
|
||||
command -v msmtp >/dev/null && echo msmtp
|
||||
true
|
||||
"""
|
||||
KNOWN_FEATURES = ("stream", "docker-cli", "bash", "msmtp")
|
||||
|
||||
|
||||
def required_features() -> set[str]:
|
||||
if not REQUIRED_FEATURES_FILE.is_file():
|
||||
return set()
|
||||
lines = (line.strip() for line in REQUIRED_FEATURES_FILE.read_text().splitlines())
|
||||
return {line for line in lines if line and not line.startswith("#")}
|
||||
|
||||
|
||||
def pytest_addoption(parser: pytest.Parser) -> None:
|
||||
parser.addoption("--build", action="store_true", help="build the image before testing")
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def docker_client() -> Iterator[docker.DockerClient]:
|
||||
try:
|
||||
client = docker.from_env()
|
||||
client.ping()
|
||||
except docker.errors.DockerException as exc:
|
||||
pytest.exit(f"docker is not available: {exc}", returncode=2)
|
||||
yield client
|
||||
client.close()
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def image(request: pytest.FixtureRequest, docker_client: docker.DockerClient) -> str:
|
||||
"""Tag of the image under test; built on demand with --build."""
|
||||
if request.config.getoption("--build"):
|
||||
subprocess.run(["docker", "build", "--quiet", "-t", IMAGE, str(ROOT)], check=True)
|
||||
try:
|
||||
docker_client.images.get(IMAGE)
|
||||
except docker.errors.ImageNotFound:
|
||||
pytest.exit(f"image {IMAGE} not found, run `task build` or pass --build", returncode=2)
|
||||
return IMAGE
|
||||
|
||||
|
||||
@pytest.fixture(scope="session", autouse=True)
|
||||
def _cleanup_leftovers(docker_client: docker.DockerClient) -> Iterator[None]:
|
||||
"""Remove every container/volume this run created, even after failures."""
|
||||
yield
|
||||
for container in docker_client.containers.list(
|
||||
all=True, filters={"label": f"rbd-test-run={RUN_ID}"}
|
||||
):
|
||||
container.remove(force=True, v=True)
|
||||
for volume in docker_client.volumes.list(filters={"label": f"rbd-test-run={RUN_ID}"}):
|
||||
volume.remove(force=True)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def detected_features(docker_client: docker.DockerClient, image: str) -> set[str]:
|
||||
out = docker_client.containers.run(
|
||||
image, entrypoint=["sh", "-c", FEATURE_PROBE], remove=True, labels={"rbd-test-run": RUN_ID}
|
||||
)
|
||||
return set(out.decode().split())
|
||||
|
||||
|
||||
def pytest_collection_modifyitems(items: list[pytest.Item]) -> None:
|
||||
for item in items:
|
||||
for marker in item.iter_markers("feature"):
|
||||
item.fixturenames.append("_feature_gate")
|
||||
if marker.args[0] not in KNOWN_FEATURES:
|
||||
raise pytest.UsageError(f"unknown feature gate: {marker.args[0]}")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def _feature_gate(request: pytest.FixtureRequest, detected_features: set[str]) -> None:
|
||||
"""Skip a feature test when the image lacks it; fail when it is required."""
|
||||
for marker in request.node.iter_markers("feature"):
|
||||
name = marker.args[0]
|
||||
if name in detected_features:
|
||||
continue
|
||||
if name in required_features():
|
||||
pytest.fail(
|
||||
f"feature '{name}' listed in {REQUIRED_FEATURES_FILE.name} "
|
||||
"but not present in the image",
|
||||
pytrace=False,
|
||||
)
|
||||
pytest.skip(f"feature '{name}' not in image yet")
|
||||
@@ -0,0 +1,5 @@
|
||||
# Optional features that MUST be present in the built image.
|
||||
#
|
||||
# One gate name per line. Known gates: stream, docker-cli, bash, msmtp.
|
||||
# A listed feature that is absent turns its skip into a failure.
|
||||
# Empty on purpose: master currently ships none of the optional features.
|
||||
@@ -0,0 +1,12 @@
|
||||
"""Scaffold checks: prove the gating logic itself. Real tests follow in later commits."""
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def test_image_is_available(image):
|
||||
assert image
|
||||
|
||||
|
||||
@pytest.mark.feature("bash")
|
||||
def test_feature_gate_probe(detected_features):
|
||||
assert "bash" in detected_features
|
||||
Reference in new issue
Block a user