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> * ci: run the pytest suite via mise and task Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * fix: make test cleanup survive repeated SIGINT A group Ctrl-C reaches pytest a second time via task and uv while the session fixture tears down, aborting the volume cleanup. Ignore SIGINT and SIGTERM during cleanup, remove volumes in finally, and tolerate already-removed objects. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * fix: pin Python 3.13 for uv without mise Add .python-version (read by mise and uv) and cap requires-python at <3.14 so bare uv uses 3.13 or fails instead of picking 3.14. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * fix: clean up test containers on SIGTERM Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * test: port compat tests to pytest Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * test: assert the success message first in the corruption test Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * test: assert the empty-tag snapshot carries no tags Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * test: remove anonymous volumes with short-lived test containers Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * test: port feature-gated tests to pytest Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * test: port sabotage self-test to task test:sabotage Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * fix: create parent directories in put_file Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * test: move structure.yml to tests and remove cst volume leak container-structure-test left one anonymous volume per test container. tests/structure.py runs it with --save and removes the new containers with their volumes afterwards. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * test: remove the Bash suite Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * ci: run only the Python suite in the test job Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * docs: describe the Python test suite Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * test: isolate scenario steps, share helpers, use restore_latest, HEAD dir probe Scenario fixtures record each step; a failing step fails only the tests that assert on it. running() and raise_interrupt() live in helpers.py. put_file probes parent directories with a HEAD request instead of a get_archive stream. stream_run uses restore_latest and asserts its result. Docstrings no longer refer to the removed Bash suite. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * test: sabotage check rejects errors and malformed reports Only <failure> of the expected tests counts as red; errors in them and a malformed JUnit report exit 2 (invalid) instead of passing or exiting 1. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * test: structure wrapper removes only its own containers Restrict cleanup to containers of the image under test without the rbd-test-run label and ignore NotFound. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * build: run all test suites and aggregate the exit code task test no longer stops at the first failing suite. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * fix: sabotage check treats failed scenario steps as INVALID StepFailed is no AssertionError any more; sabotage.py counts a JUnit failure named StepFailed as error, so an expected-red test only counts when its own assertion fails. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * fix: hard timeout for container-structure-test CST_TIMEOUT (default 300 s) kills a hanging run; existing cleanup removes the containers; exit code 124 with a clear message. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * fix: shorten sabotage message for lint Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * fix: handle JUnit failures without message in sabotage check Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
78 lines
2.7 KiB
Python
78 lines
2.7 KiB
Python
"""Run container-structure-test without leaking Docker volumes.
|
|
|
|
container-structure-test starts one container per test and never removes it. The image
|
|
declares `VOLUME /data`, so every container drags an anonymous volume along that outlives
|
|
`docker rm`. With `--save` the containers are kept, and this wrapper removes the ones of
|
|
the image under test that the run created, together with their volumes (`remove(v=True)`);
|
|
containers of other images or of the compat suite (a parallel run, a compose stack) are left
|
|
alone.
|
|
Run with `task test:structure`.
|
|
|
|
A run longer than CST_TIMEOUT seconds (default 300) is killed: container-structure-test has
|
|
no timeout of its own and can block forever waiting for a container.
|
|
|
|
Exit code: the one of container-structure-test; 124 on timeout.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import contextlib
|
|
import os
|
|
import signal
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import docker
|
|
from helpers import raise_interrupt
|
|
|
|
CONFIG = Path(__file__).with_name("structure.yml")
|
|
TIMEOUT_EXIT = 124
|
|
|
|
|
|
def _containers_of(client: docker.DockerClient, image_id: str) -> list:
|
|
"""Containers of the image that container-structure-test could have created.
|
|
|
|
Containers of the compat suite carry the `rbd-test-run` label; their owner removes them.
|
|
"""
|
|
found = client.containers.list(all=True, filters={"ancestor": image_id})
|
|
return [c for c in found if "rbd-test-run" not in c.labels]
|
|
|
|
|
|
def main() -> int:
|
|
image = os.environ.get("IMAGE", "restic-backup-docker:test")
|
|
signal.signal(signal.SIGTERM, raise_interrupt)
|
|
client = docker.from_env()
|
|
image_id = client.images.get(image).id
|
|
before = {c.id for c in _containers_of(client, image_id)}
|
|
try:
|
|
timeout = float(os.environ.get("CST_TIMEOUT", "300"))
|
|
try:
|
|
return subprocess.run(
|
|
[
|
|
"container-structure-test",
|
|
"test",
|
|
"--save",
|
|
"--image",
|
|
image,
|
|
"--config",
|
|
os.environ.get("CST_CONFIG", str(CONFIG)),
|
|
],
|
|
check=False,
|
|
timeout=timeout,
|
|
).returncode
|
|
except subprocess.TimeoutExpired:
|
|
print(f"container-structure-test timed out after {timeout:g} s", file=sys.stderr)
|
|
return TIMEOUT_EXIT
|
|
finally:
|
|
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
|
for container in _containers_of(client, image_id):
|
|
if container.id not in before:
|
|
with contextlib.suppress(docker.errors.NotFound):
|
|
container.remove(v=True, force=True)
|
|
signal.signal(signal.SIGINT, signal.default_int_handler)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|