diff --git a/Taskfile.yml b/Taskfile.yml index e2af618..721e238 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -32,9 +32,9 @@ tasks: - task: test:features test:structure: - desc: Run container-structure-test against the image + desc: Run container-structure-test against the image and remove its containers and volumes cmds: - - container-structure-test test --image {{.IMAGE}} --config test/structure.yml + - uv run python tests/structure.py test:compat: desc: Run the compat tests diff --git a/test/required-features.txt b/test/required-features.txt deleted file mode 100644 index 5b266d7..0000000 --- a/test/required-features.txt +++ /dev/null @@ -1,18 +0,0 @@ -# Optional features that MUST be present in the built image. -# -# One gate name per line. Known gates and the pull request that introduces -# them (see test/features.sh): -# -# stream stream backups via RESTIC_STREAM PR #109 -# docker-cli docker cli inside the container PR #99 -# bash bash for pre/post hooks PR #99 -# msmtp mail relay via msmtp PR #84 -# -# While a gate is NOT listed here its tests are reported as SKIP when the -# feature is absent, so an open pull request does not turn master red. -# -# When you merge a feature, add its gate name below. From then on the feature -# tests are mandatory and a feature that silently disappears from the image -# fails the build. -# -# Empty on purpose: master currently ships none of the optional features. diff --git a/tests/required-features.txt b/tests/required-features.txt index 01350c8..1827224 100644 --- a/tests/required-features.txt +++ b/tests/required-features.txt @@ -1,5 +1,17 @@ # 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. +# One gate name per line. Known gates and the pull request that introduces +# them (see tests/test_features.py): +# +# stream stream backups via RESTIC_STREAM PR #109 +# docker-cli docker cli inside the container PR #99 +# bash bash for pre/post hooks PR #99 +# msmtp mail relay via msmtp PR #84 +# +# While a gate is NOT listed here its tests are reported as SKIP when the +# feature is absent, so an open pull request does not turn master red. +# +# When you merge a feature, add its gate name below. From then on the feature +# tests are mandatory and a feature that silently disappears from the image +# fails the build. +# diff --git a/tests/structure.py b/tests/structure.py new file mode 100644 index 0000000..e54d96f --- /dev/null +++ b/tests/structure.py @@ -0,0 +1,55 @@ +"""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 the +run created, together with their volumes (`remove(v=True)`). Run with `task test:structure`. + +Exit code: the one of container-structure-test. +""" + +from __future__ import annotations + +import os +import signal +import subprocess +import sys +from pathlib import Path + +import docker + +CONFIG = Path(__file__).with_name("structure.yml") + + +def _raise_interrupt(signum: int, _frame: object) -> None: + raise KeyboardInterrupt(f"received {signal.Signals(signum).name}") + + +def main() -> int: + image = os.environ.get("IMAGE", "restic-backup-docker:test") + signal.signal(signal.SIGTERM, _raise_interrupt) + client = docker.from_env() + before = {c.id for c in client.containers.list(all=True)} + try: + return subprocess.run( + [ + "container-structure-test", + "test", + "--save", + "--image", + image, + "--config", + str(CONFIG), + ], + check=False, + ).returncode + finally: + signal.signal(signal.SIGINT, signal.SIG_IGN) + for container in client.containers.list(all=True): + if container.id not in before: + container.remove(v=True, force=True) + signal.signal(signal.SIGINT, signal.default_int_handler) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/test/structure.yml b/tests/structure.yml similarity index 100% rename from test/structure.yml rename to tests/structure.yml