Files
grm/scripts/tests/test_prune_runner_images.py
T
emil cae4e2a860
Post-merge / detect-type (push) Successful in 1m3s
Post-merge / release (push) Successful in 1m8s
Post-merge / validate-commit-msg (push) Successful in 1m9s
Post-merge / vikunja (push) Successful in 1m13s
Post-merge / badges (push) Successful in 1m19s
Post-merge / sync-wiki (push) Successful in 1m44s
Post-merge / publish (push) Successful in 1m3s
Post-merge / configure-repo (push) Successful in 1m18s
GRM-107: refactor: use devx Makefile aliases, bump devx>=0.23.0
2026-06-27 20:24:29 +00:00

145 lines
5.2 KiB
Python

"""Tests for prune_runner_images.py."""
from __future__ import annotations
from unittest.mock import MagicMock, patch
from scripts.prune_runner_images import (
filter_runner_images,
list_docker_images,
main,
remove_images,
)
class TestListDockerImages:
"""Tests for list_docker_images()."""
@patch("scripts.prune_runner_images.subprocess.run")
def test_returns_images_from_docker(self, mock_run: MagicMock) -> None:
mock_run.return_value = MagicMock(
stdout="repo1:tag1\nrepo2:tag2\n",
returncode=0,
)
result = list_docker_images()
assert result == ["repo1:tag1", "repo2:tag2"]
@patch("scripts.prune_runner_images.subprocess.run")
def test_filters_none_entries(self, mock_run: MagicMock) -> None:
mock_run.return_value = MagicMock(
stdout="repo:tag\n<none>:<none>\nother:v1\n",
returncode=0,
)
result = list_docker_images()
assert result == ["repo:tag", "other:v1"]
@patch("scripts.prune_runner_images.subprocess.run")
def test_empty_output(self, mock_run: MagicMock) -> None:
mock_run.return_value = MagicMock(stdout="", returncode=0)
result = list_docker_images()
assert result == []
@patch("scripts.prune_runner_images.subprocess.run")
def test_strips_whitespace(self, mock_run: MagicMock) -> None:
mock_run.return_value = MagicMock(
stdout=" repo:tag \n\n other:v2 \n",
returncode=0,
)
result = list_docker_images()
assert result == ["repo:tag", "other:v2"]
class TestFilterRunnerImages:
"""Tests for filter_runner_images()."""
def test_matches_runner_images(self) -> None:
images = [
"git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-base:latest",
"git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-quality:latest",
"git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-full:latest",
]
result = filter_runner_images(images)
assert len(result) == 3
def test_excludes_non_runner_images(self) -> None:
images = [
"docker.io/library/python:3.12",
"docker.io/library/nginx:latest",
"git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-base:latest",
]
result = filter_runner_images(images)
assert len(result) == 1
assert "ci-base" in result[0]
def test_empty_list(self) -> None:
assert filter_runner_images([]) == []
def test_no_matches(self) -> None:
images = ["python:3.12", "nginx:latest"]
assert filter_runner_images(images) == []
class TestRemoveImages:
"""Tests for remove_images()."""
@patch("scripts.prune_runner_images.subprocess.run")
def test_removes_images(self, mock_run: MagicMock) -> None:
mock_run.return_value = MagicMock(returncode=0, stderr="")
images = ["repo/ci-base:latest", "repo/ci-quality:latest"]
removed = remove_images(images)
assert removed == images
assert mock_run.call_count == 2
@patch("scripts.prune_runner_images.subprocess.run")
def test_dry_run_does_not_call_docker(self, mock_run: MagicMock) -> None:
images = ["repo/ci-base:latest"]
removed = remove_images(images, dry_run=True)
assert removed == images
mock_run.assert_not_called()
@patch("scripts.prune_runner_images.subprocess.run")
def test_failed_removal_not_in_result(self, mock_run: MagicMock) -> None:
mock_run.return_value = MagicMock(returncode=1, stderr="image in use")
images = ["repo/ci-base:latest"]
removed = remove_images(images)
assert removed == []
@patch("scripts.prune_runner_images.subprocess.run")
def test_empty_list(self, mock_run: MagicMock) -> None:
removed = remove_images([])
assert removed == []
mock_run.assert_not_called()
class TestMain:
"""Tests for main()."""
@patch("scripts.prune_runner_images.list_docker_images")
@patch("scripts.prune_runner_images.remove_images")
def test_no_images(self, mock_remove: MagicMock, mock_list: MagicMock) -> None:
mock_list.return_value = []
assert main([]) == 0
mock_remove.assert_not_called()
@patch("scripts.prune_runner_images.list_docker_images")
@patch("scripts.prune_runner_images.remove_images")
def test_with_images(self, mock_remove: MagicMock, mock_list: MagicMock) -> None:
mock_list.return_value = [
"repo/runner-images/ci-base:latest",
"python:3.12",
]
mock_remove.return_value = ["repo/runner-images/ci-base:latest"]
assert main([]) == 0
mock_remove.assert_called_once()
@patch("scripts.prune_runner_images.list_docker_images")
@patch("scripts.prune_runner_images.remove_images")
def test_dry_run_flag(self, mock_remove: MagicMock, mock_list: MagicMock) -> None:
mock_list.return_value = ["repo/runner-images/ci-base:latest"]
mock_remove.return_value = ["repo/runner-images/ci-base:latest"]
assert main(["--dry-run"]) == 0
mock_remove.assert_called_once_with(
["repo/runner-images/ci-base:latest"],
dry_run=True,
)