"""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:\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, )