refactor: rootless Docker, fix auto-merge, molecule platform matrix
Three major improvements: 1. Rootless Docker refactor: Removes docker/binary modes, unifies to rootless Docker with per-runner system users. Each runner gets its own rootless Docker daemon, systemd user service, and isolated environment. Simplifies CLI (removes --mode option), Ansible role (single code path), and molecule scenarios (removes binary scenario). 2. Auto-merge fix: Fixes status check context mismatch in branch protection (was requiring "lint", "unit-tests", "molecule-tests" but actual contexts are "CI / quality", "CI / molecule-tests*"). Adds retry/wait logic to auto_merge.py that polls commit statuses for up to 15 minutes before attempting merge, eliminating the chicken-and-egg problem where auto-merge would fail because CI hadn't completed yet. 3. Molecule platform matrix: Adds OS platform matrix to CI — all 6 scenarios now run on all 4 supported OSes (ubuntu-2204, ubuntu-2404, debian-12, archlinux) = 24 test pairs distributed across 3 parallel runners. Updates distribute_molecule.py to distribute (scenario, platform) pairs. Updates Makefile with molecule-all target for local multi-platform testing. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent
0c6c735000
commit
55c2746569
@@ -201,6 +201,33 @@ class TestGiteaClient:
|
||||
timeout=DEFAULT_TIMEOUT,
|
||||
)
|
||||
|
||||
def test_get_commit_status(self) -> None:
|
||||
client = GiteaClient("https://git.example.com", "tok", "owner", "repo")
|
||||
client._session.request = MagicMock(
|
||||
return_value=_mock_response([{"context": "CI / quality", "status": "success"}])
|
||||
)
|
||||
|
||||
result = client.get_commit_status("abc123")
|
||||
assert result == [{"context": "CI / quality", "status": "success"}]
|
||||
client._session.request.assert_called_once_with(
|
||||
"GET",
|
||||
"https://git.example.com/repos/owner/repo/commits/abc123/statuses",
|
||||
timeout=DEFAULT_TIMEOUT,
|
||||
)
|
||||
|
||||
def test_get_pr(self) -> None:
|
||||
client = GiteaClient("https://git.example.com", "tok", "owner", "repo")
|
||||
client._session.request = MagicMock(return_value=_mock_response({"number": 7, "head": {"sha": "abc123"}}))
|
||||
|
||||
result = client.get_pr(7)
|
||||
assert result["number"] == 7
|
||||
assert result["head"]["sha"] == "abc123"
|
||||
client._session.request.assert_called_once_with(
|
||||
"GET",
|
||||
"https://git.example.com/repos/owner/repo/pulls/7",
|
||||
timeout=DEFAULT_TIMEOUT,
|
||||
)
|
||||
|
||||
def test_update_repo_settings(self) -> None:
|
||||
client = GiteaClient("https://git.example.com", "tok", "owner", "repo")
|
||||
client._session.request = MagicMock(return_value=_mock_response({"default_delete_branch_after_merge": True}))
|
||||
|
||||
@@ -9,7 +9,21 @@ from click.testing import CliRunner
|
||||
|
||||
from gitea_runner_manager.config import CONVENTIONAL_RE, TASK_ID_RE
|
||||
from gitea_runner_manager.exceptions import APIError
|
||||
from scripts.auto_merge import extract_task_id, has_ready_to_merge_label, main, validate_pr_title
|
||||
from scripts.auto_merge import (
|
||||
extract_task_id,
|
||||
has_ready_to_merge_label,
|
||||
main,
|
||||
validate_pr_title,
|
||||
wait_for_ci,
|
||||
)
|
||||
|
||||
CI_SUCCESS = "success"
|
||||
CI_PENDING = "pending"
|
||||
CI_FAILURE = "failure"
|
||||
|
||||
|
||||
def _status(context: str, status: str, updated_at: str = "2026-01-01T00:00:00Z") -> dict[str, str]:
|
||||
return {"context": context, "status": status, "updated_at": updated_at}
|
||||
|
||||
|
||||
class TestRegexes:
|
||||
@@ -70,12 +84,91 @@ class TestHasReadyToMergeLabel:
|
||||
assert has_ready_to_merge_label(client, "5") is False
|
||||
|
||||
|
||||
class TestWaitForCi:
|
||||
def test_all_pass_immediately(self) -> None:
|
||||
client = MagicMock()
|
||||
client.get_commit_status.return_value = [
|
||||
_status("CI / quality (pull_request)", CI_SUCCESS),
|
||||
_status("CI / molecule-tests (0) (pull_request)", CI_SUCCESS),
|
||||
]
|
||||
assert wait_for_ci(client, "abc123", max_wait=10) is True
|
||||
|
||||
def test_waits_then_passes(self) -> None:
|
||||
client = MagicMock()
|
||||
client.get_commit_status.side_effect = [
|
||||
[_status("CI / quality (pull_request)", CI_PENDING)],
|
||||
[_status("CI / quality (pull_request)", CI_SUCCESS)],
|
||||
]
|
||||
with patch("scripts.auto_merge.time.sleep"):
|
||||
assert wait_for_ci(client, "abc123", max_wait=10, poll_interval=5) is True
|
||||
|
||||
def test_fails_on_failed_check(self) -> None:
|
||||
client = MagicMock()
|
||||
client.get_commit_status.return_value = [
|
||||
_status("CI / quality (pull_request)", CI_SUCCESS),
|
||||
_status("CI / molecule-tests (0) (pull_request)", CI_FAILURE),
|
||||
]
|
||||
assert wait_for_ci(client, "abc123", max_wait=10) is False
|
||||
|
||||
def test_times_out(self) -> None:
|
||||
client = MagicMock()
|
||||
client.get_commit_status.return_value = [
|
||||
_status("CI / quality (pull_request)", CI_PENDING),
|
||||
]
|
||||
with patch("scripts.auto_merge.time.sleep"):
|
||||
assert wait_for_ci(client, "abc123", max_wait=5) is False
|
||||
|
||||
def test_no_statuses_waits(self) -> None:
|
||||
client = MagicMock()
|
||||
client.get_commit_status.side_effect = [
|
||||
[],
|
||||
[_status("CI / quality (pull_request)", CI_SUCCESS)],
|
||||
]
|
||||
with patch("scripts.auto_merge.time.sleep"):
|
||||
assert wait_for_ci(client, "abc123", max_wait=10, poll_interval=5) is True
|
||||
|
||||
def test_ignores_non_ci_contexts(self) -> None:
|
||||
client = MagicMock()
|
||||
client.get_commit_status.return_value = [
|
||||
_status("Auto-merge / merge (pull_request)", CI_PENDING),
|
||||
_status("CI / quality (pull_request)", CI_SUCCESS),
|
||||
]
|
||||
assert wait_for_ci(client, "abc123", max_wait=10) is True
|
||||
|
||||
def test_deduplicates_by_latest(self) -> None:
|
||||
client = MagicMock()
|
||||
client.get_commit_status.return_value = [
|
||||
_status("CI / quality (pull_request)", CI_PENDING, "2026-01-01T00:00:00Z"),
|
||||
_status("CI / quality (pull_request)", CI_SUCCESS, "2026-01-01T00:01:00Z"),
|
||||
]
|
||||
assert wait_for_ci(client, "abc123", max_wait=10) is True
|
||||
|
||||
def test_only_non_ci_contexts_waits_then_ci_appears(self) -> None:
|
||||
client = MagicMock()
|
||||
client.get_commit_status.side_effect = [
|
||||
[_status("Auto-merge / merge (pull_request)", CI_PENDING)],
|
||||
[_status("CI / quality (pull_request)", CI_SUCCESS)],
|
||||
]
|
||||
with patch("scripts.auto_merge.time.sleep"):
|
||||
assert wait_for_ci(client, "abc123", max_wait=10, poll_interval=5) is True
|
||||
|
||||
|
||||
def _mock_pr(sha: str = "abc123def456") -> dict[str, object]:
|
||||
return {"head": {"sha": sha}}
|
||||
|
||||
|
||||
def _mock_ci_passing() -> list[dict[str, str]]:
|
||||
return [_status("CI / quality (pull_request)", CI_SUCCESS)]
|
||||
|
||||
|
||||
class TestMain:
|
||||
@patch.dict("os.environ", {"REPO_TOKEN": "tok"})
|
||||
@patch("scripts.auto_merge.GiteaClient")
|
||||
def test_successful_flow_with_label_arg(self, mock_client_cls: MagicMock) -> None:
|
||||
mock_client = MagicMock()
|
||||
mock_client_cls.return_value = mock_client
|
||||
mock_client.get_pr.return_value = _mock_pr()
|
||||
mock_client.get_commit_status.return_value = _mock_ci_passing()
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
main,
|
||||
@@ -91,6 +184,8 @@ class TestMain:
|
||||
"""Label not passed via arg, but PR has ready-to-merge via API."""
|
||||
mock_client = MagicMock()
|
||||
mock_client.get_pr_labels.return_value = [{"name": "ready-to-merge"}]
|
||||
mock_client.get_pr.return_value = _mock_pr()
|
||||
mock_client.get_commit_status.return_value = _mock_ci_passing()
|
||||
mock_client_cls.return_value = mock_client
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
@@ -123,6 +218,8 @@ class TestMain:
|
||||
"""Gitea Actions doesn't populate label name, but API shows ready-to-merge."""
|
||||
mock_client = MagicMock()
|
||||
mock_client.get_pr_labels.return_value = [{"name": "ready-to-merge"}]
|
||||
mock_client.get_pr.return_value = _mock_pr()
|
||||
mock_client.get_commit_status.return_value = _mock_ci_passing()
|
||||
mock_client_cls.return_value = mock_client
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
@@ -166,6 +263,8 @@ class TestMain:
|
||||
def test_merge_pr_failure_raises_click(self, mock_client_cls: MagicMock) -> None:
|
||||
mock_client = MagicMock()
|
||||
mock_client.get_pr_labels.return_value = [{"name": "ready-to-merge"}]
|
||||
mock_client.get_pr.return_value = _mock_pr()
|
||||
mock_client.get_commit_status.return_value = _mock_ci_passing()
|
||||
mock_client.merge_pr.side_effect = APIError(http.HTTPStatus.INTERNAL_SERVER_ERROR, "server error")
|
||||
mock_client_cls.return_value = mock_client
|
||||
runner = CliRunner()
|
||||
@@ -178,9 +277,41 @@ class TestMain:
|
||||
def test_merge_pr_json_parse_failure(self, mock_client_cls: MagicMock) -> None:
|
||||
mock_client = MagicMock()
|
||||
mock_client.get_pr_labels.return_value = [{"name": "ready-to-merge"}]
|
||||
mock_client.get_pr.return_value = _mock_pr()
|
||||
mock_client.get_commit_status.return_value = _mock_ci_passing()
|
||||
mock_client.merge_pr.side_effect = APIError(http.HTTPStatus.BAD_GATEWAY, "bad gateway")
|
||||
mock_client_cls.return_value = mock_client
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(main, ["GRM-19-fix", "fix: bug", "owner/repo", "1"])
|
||||
assert result.exit_code == 1
|
||||
assert str(http.HTTPStatus.BAD_GATEWAY) in result.output
|
||||
|
||||
@patch.dict("os.environ", {"REPO_TOKEN": "tok"})
|
||||
@patch("scripts.auto_merge.GiteaClient")
|
||||
def test_ci_failure_blocks_merge(self, mock_client_cls: MagicMock) -> None:
|
||||
"""CI checks fail — merge should not be attempted."""
|
||||
mock_client = MagicMock()
|
||||
mock_client.get_pr_labels.return_value = [{"name": "ready-to-merge"}]
|
||||
mock_client.get_pr.return_value = _mock_pr()
|
||||
mock_client.get_commit_status.return_value = [
|
||||
_status("CI / quality (pull_request)", CI_FAILURE),
|
||||
]
|
||||
mock_client_cls.return_value = mock_client
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(main, ["GRM-19-fix", "fix: bug", "owner/repo", "1"])
|
||||
assert result.exit_code == 1
|
||||
assert "CI checks did not pass" in result.output
|
||||
mock_client.merge_pr.assert_not_called()
|
||||
|
||||
@patch.dict("os.environ", {"REPO_TOKEN": "tok"})
|
||||
@patch("scripts.auto_merge.GiteaClient")
|
||||
def test_no_sha_proceeds_without_wait(self, mock_client_cls: MagicMock) -> None:
|
||||
"""PR head SHA missing — should proceed without waiting."""
|
||||
mock_client = MagicMock()
|
||||
mock_client.get_pr_labels.return_value = [{"name": "ready-to-merge"}]
|
||||
mock_client.get_pr.return_value = {"head": {}}
|
||||
mock_client_cls.return_value = mock_client
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(main, ["GRM-19-fix", "fix: bug", "owner/repo", "1"])
|
||||
assert result.exit_code == 0
|
||||
assert "squash-merged" in result.output
|
||||
|
||||
+1
-56
@@ -29,7 +29,6 @@ class TestCLI:
|
||||
name=None,
|
||||
token="tok",
|
||||
gitea_url="https://git.example.com",
|
||||
mode="docker",
|
||||
admin_token="",
|
||||
integration_retries=3,
|
||||
labels=None,
|
||||
@@ -51,7 +50,6 @@ class TestCLI:
|
||||
name=None,
|
||||
token="tok",
|
||||
gitea_url="https://git.example.com",
|
||||
mode="docker",
|
||||
admin_token="",
|
||||
integration_retries=3,
|
||||
labels=None,
|
||||
@@ -91,7 +89,6 @@ class TestCLI:
|
||||
name=None,
|
||||
token="tok",
|
||||
gitea_url="https://git.example.com",
|
||||
mode="docker",
|
||||
admin_token=None,
|
||||
integration_retries=3,
|
||||
labels=None,
|
||||
@@ -141,7 +138,6 @@ class TestCLI:
|
||||
name="r1",
|
||||
token="tok",
|
||||
gitea_url="https://git.example.com",
|
||||
mode="docker",
|
||||
admin_token="",
|
||||
integration_retries=3,
|
||||
labels=None,
|
||||
@@ -163,29 +159,6 @@ class TestCLI:
|
||||
name=None,
|
||||
token="tok",
|
||||
gitea_url="https://git.example.com",
|
||||
mode="docker",
|
||||
admin_token="",
|
||||
integration_retries=3,
|
||||
labels=None,
|
||||
ask_become_pass=True,
|
||||
)
|
||||
|
||||
@patch("gitea_runner_manager.cli.RunnerManager")
|
||||
def test_install_binary_mode(self, mock_manager_class: MagicMock) -> None:
|
||||
mock_manager = MagicMock()
|
||||
mock_manager_class.return_value = mock_manager
|
||||
|
||||
runner = CliRunner(env={"GITEA_URL": "https://git.example.com", "REPO_TOKEN": "", "GITEA_RUNNER_LABELS": ""})
|
||||
result = runner.invoke(cli, ["install", "host1", "--user", "ubuntu", "--token", "tok", "--mode", "binary"])
|
||||
assert result.exit_code == 0
|
||||
mock_manager.install.assert_called_once_with(
|
||||
host="host1",
|
||||
user="ubuntu",
|
||||
key=None,
|
||||
name=None,
|
||||
token="tok",
|
||||
gitea_url="https://git.example.com",
|
||||
mode="binary",
|
||||
admin_token="",
|
||||
integration_retries=3,
|
||||
labels=None,
|
||||
@@ -230,7 +203,6 @@ class TestCLI:
|
||||
user="ubuntu",
|
||||
key="/key",
|
||||
version="v0.2.0",
|
||||
mode="docker",
|
||||
ask_become_pass=True,
|
||||
)
|
||||
|
||||
@@ -247,24 +219,6 @@ class TestCLI:
|
||||
user="ubuntu",
|
||||
key=None,
|
||||
version=None,
|
||||
mode="docker",
|
||||
ask_become_pass=True,
|
||||
)
|
||||
|
||||
@patch("gitea_runner_manager.cli.RunnerManager")
|
||||
def test_update_binary_mode(self, mock_manager_class: MagicMock) -> None:
|
||||
mock_manager = MagicMock()
|
||||
mock_manager_class.return_value = mock_manager
|
||||
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cli, ["update", "host1", "--user", "ubuntu", "--mode", "binary"])
|
||||
assert result.exit_code == 0
|
||||
mock_manager.update.assert_called_once_with(
|
||||
host="host1",
|
||||
user="ubuntu",
|
||||
key=None,
|
||||
version=None,
|
||||
mode="binary",
|
||||
ask_become_pass=True,
|
||||
)
|
||||
|
||||
@@ -294,7 +248,6 @@ class TestCLI:
|
||||
host=None,
|
||||
user=None,
|
||||
key=None,
|
||||
mode=None,
|
||||
ask_become_pass=True,
|
||||
)
|
||||
|
||||
@@ -304,14 +257,13 @@ class TestCLI:
|
||||
mock_manager_class.return_value = mock_manager
|
||||
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cli, ["start", "r1", "--host", "newhost", "--user", "newuser", "--mode", "binary"])
|
||||
result = runner.invoke(cli, ["start", "r1", "--host", "newhost", "--user", "newuser"])
|
||||
assert result.exit_code == 0
|
||||
mock_manager.start.assert_called_once_with(
|
||||
name="r1",
|
||||
host="newhost",
|
||||
user="newuser",
|
||||
key=None,
|
||||
mode="binary",
|
||||
ask_become_pass=True,
|
||||
)
|
||||
|
||||
@@ -362,7 +314,6 @@ class TestCLI:
|
||||
key=None,
|
||||
token="tok",
|
||||
gitea_url="https://git.example.com",
|
||||
mode=None,
|
||||
ask_become_pass=True,
|
||||
)
|
||||
|
||||
@@ -396,7 +347,6 @@ class TestCLI:
|
||||
key=None,
|
||||
token="tok",
|
||||
gitea_url="https://git.example.com",
|
||||
mode=None,
|
||||
ask_become_pass=True,
|
||||
)
|
||||
|
||||
@@ -426,7 +376,6 @@ class TestCLI:
|
||||
host=None,
|
||||
user=None,
|
||||
key=None,
|
||||
mode=None,
|
||||
ask_become_pass=True,
|
||||
)
|
||||
|
||||
@@ -445,7 +394,6 @@ class TestCLI:
|
||||
key=None,
|
||||
token="tok",
|
||||
gitea_url="https://git.example.com",
|
||||
mode=None,
|
||||
force=False,
|
||||
ask_become_pass=True,
|
||||
)
|
||||
@@ -465,7 +413,6 @@ class TestCLI:
|
||||
key=None,
|
||||
token="tok",
|
||||
gitea_url="https://git.example.com",
|
||||
mode=None,
|
||||
force=True,
|
||||
ask_become_pass=True,
|
||||
)
|
||||
@@ -552,7 +499,6 @@ class TestCLI:
|
||||
key=None,
|
||||
token="tok",
|
||||
gitea_url="https://git.example.com",
|
||||
mode=None,
|
||||
force=False,
|
||||
ask_become_pass=True,
|
||||
)
|
||||
@@ -578,7 +524,6 @@ class TestCLI:
|
||||
"name": "r1",
|
||||
"host": "10.0.0.1",
|
||||
"user": "ubuntu",
|
||||
"mode": "docker",
|
||||
"labels": "docker:docker://alpine:latest",
|
||||
"status": "active",
|
||||
},
|
||||
|
||||
@@ -47,7 +47,7 @@ class TestConfigConstants:
|
||||
assert BRANCH_PROTECTION_CONFIG["branch_name"] == "master"
|
||||
assert BRANCH_PROTECTION_CONFIG["enable_push"] is False
|
||||
assert BRANCH_PROTECTION_CONFIG["required_approvals"] == 1
|
||||
assert BRANCH_PROTECTION_CONFIG["status_check_contexts"] == ["lint", "unit-tests", "molecule-tests"]
|
||||
assert BRANCH_PROTECTION_CONFIG["status_check_contexts"] == ["CI / quality", "CI / molecule-tests*"]
|
||||
|
||||
def test_label_config(self) -> None:
|
||||
assert LABEL_CONFIG["name"] == "ready-to-merge"
|
||||
|
||||
@@ -8,9 +8,12 @@ import pytest
|
||||
|
||||
from scripts.distribute_molecule import (
|
||||
MOLECULE_ROOT,
|
||||
PLATFORMS,
|
||||
TestPair,
|
||||
build_pairs,
|
||||
discover_scenarios,
|
||||
distribute,
|
||||
scenarios_for_runner,
|
||||
pairs_for_runner,
|
||||
)
|
||||
|
||||
|
||||
@@ -33,47 +36,97 @@ class TestDiscoverScenarios:
|
||||
assert Path("ansible/roles/gitea-runner/molecule") == MOLECULE_ROOT
|
||||
|
||||
|
||||
class TestPairEncoding:
|
||||
def test_encode_roundtrip(self) -> None:
|
||||
pair = TestPair("default", {"name": "ubuntu-2204", "image": "ubuntu:22.04", "command": ""})
|
||||
encoded = pair.encode()
|
||||
assert encoded == "default|ubuntu-2204|ubuntu:22.04|"
|
||||
decoded = TestPair.decode(encoded)
|
||||
assert decoded.scenario == "default"
|
||||
assert decoded.platform["name"] == "ubuntu-2204"
|
||||
assert decoded.platform["image"] == "ubuntu:22.04"
|
||||
assert decoded.platform["command"] == ""
|
||||
|
||||
def test_encode_with_command(self) -> None:
|
||||
pair = TestPair(
|
||||
"default",
|
||||
{"name": "archlinux", "image": "archlinux:latest", "command": "/usr/lib/systemd/systemd"},
|
||||
)
|
||||
encoded = pair.encode()
|
||||
assert encoded == "default|archlinux|archlinux:latest|/usr/lib/systemd/systemd"
|
||||
decoded = TestPair.decode(encoded)
|
||||
assert decoded.platform["command"] == "/usr/lib/systemd/systemd"
|
||||
|
||||
|
||||
class TestBuildPairs:
|
||||
def test_cross_product(self) -> None:
|
||||
scenarios = ["a", "b"]
|
||||
platforms = [
|
||||
{"name": "p1", "image": "img1", "command": ""},
|
||||
{"name": "p2", "image": "img2", "command": ""},
|
||||
]
|
||||
pairs = build_pairs(scenarios, platforms)
|
||||
assert len(pairs) == 4
|
||||
assert pairs[0].scenario == "a"
|
||||
assert pairs[0].platform["name"] == "p1"
|
||||
assert pairs[1].scenario == "a"
|
||||
assert pairs[1].platform["name"] == "p2"
|
||||
assert pairs[2].scenario == "b"
|
||||
assert pairs[2].platform["name"] == "p1"
|
||||
assert pairs[3].scenario == "b"
|
||||
assert pairs[3].platform["name"] == "p2"
|
||||
|
||||
def test_default_platforms(self) -> None:
|
||||
pairs = build_pairs(["default"])
|
||||
assert len(pairs) == len(PLATFORMS)
|
||||
assert all(p.scenario == "default" for p in pairs)
|
||||
|
||||
|
||||
class TestDistribute:
|
||||
def test_even_split(self) -> None:
|
||||
scenarios = ["a", "b", "c", "d", "e", "f"]
|
||||
groups = distribute(scenarios, 3)
|
||||
assert groups == [["a", "d"], ["b", "e"], ["c", "f"]]
|
||||
pairs = [TestPair(f"s{i}", {"name": "p", "image": "i", "command": ""}) for i in range(6)]
|
||||
groups = distribute(pairs, 3)
|
||||
assert len(groups) == 3
|
||||
assert len(groups[0]) == 2
|
||||
assert len(groups[1]) == 2
|
||||
assert len(groups[2]) == 2
|
||||
|
||||
def test_uneven_split(self) -> None:
|
||||
scenarios = ["a", "b", "c", "d", "e"]
|
||||
groups = distribute(scenarios, 3)
|
||||
assert groups == [["a", "d"], ["b", "e"], ["c"]]
|
||||
pairs = [TestPair(f"s{i}", {"name": "p", "image": "i", "command": ""}) for i in range(5)]
|
||||
groups = distribute(pairs, 3)
|
||||
assert len(groups[0]) == 2
|
||||
assert len(groups[1]) == 2
|
||||
assert len(groups[2]) == 1
|
||||
|
||||
def test_more_runners_than_scenarios(self) -> None:
|
||||
scenarios = ["a", "b"]
|
||||
groups = distribute(scenarios, 5)
|
||||
assert groups == [["a"], ["b"], [], [], []]
|
||||
def test_more_runners_than_pairs(self) -> None:
|
||||
pairs = [TestPair("a", {"name": "p", "image": "i", "command": ""})]
|
||||
groups = distribute(pairs, 5)
|
||||
assert len(groups) == 5
|
||||
assert len(groups[0]) == 1
|
||||
assert all(len(g) == 0 for g in groups[1:])
|
||||
|
||||
def test_single_runner(self) -> None:
|
||||
scenarios = ["a", "b", "c"]
|
||||
groups = distribute(scenarios, 1)
|
||||
assert groups == [["a", "b", "c"]]
|
||||
|
||||
def test_empty_scenarios(self) -> None:
|
||||
def test_empty_pairs(self) -> None:
|
||||
groups = distribute([], 3)
|
||||
assert groups == [[], [], []]
|
||||
|
||||
|
||||
class TestScenariosForRunner:
|
||||
class TestPairsForRunner:
|
||||
def test_returns_correct_subset(self) -> None:
|
||||
scenarios = ["a", "b", "c", "d", "e", "f"]
|
||||
assert scenarios_for_runner(scenarios, 0, 3) == ["a", "d"]
|
||||
assert scenarios_for_runner(scenarios, 1, 3) == ["b", "e"]
|
||||
assert scenarios_for_runner(scenarios, 2, 3) == ["c", "f"]
|
||||
pairs = [TestPair(f"s{i}", {"name": "p", "image": "i", "command": ""}) for i in range(6)]
|
||||
assert len(pairs_for_runner(pairs, 0, 3)) == 2
|
||||
assert len(pairs_for_runner(pairs, 1, 3)) == 2
|
||||
assert len(pairs_for_runner(pairs, 2, 3)) == 2
|
||||
|
||||
def test_out_of_range_raises(self) -> None:
|
||||
pairs = [TestPair("a", {"name": "p", "image": "i", "command": ""})]
|
||||
with pytest.raises(click.ClickException) as exc:
|
||||
scenarios_for_runner(["a"], 5, 3)
|
||||
pairs_for_runner(pairs, 5, 3)
|
||||
assert "out of range" in str(exc.value)
|
||||
|
||||
def test_negative_index_raises(self) -> None:
|
||||
pairs = [TestPair("a", {"name": "p", "image": "i", "command": ""})]
|
||||
with pytest.raises(click.ClickException) as exc:
|
||||
scenarios_for_runner(["a"], -1, 3)
|
||||
pairs_for_runner(pairs, -1, 3)
|
||||
assert "out of range" in str(exc.value)
|
||||
|
||||
|
||||
@@ -93,6 +146,19 @@ class TestCli:
|
||||
assert "alpha" in result.output
|
||||
assert "beta" in result.output
|
||||
|
||||
def test_list_platforms_flag(self) -> None:
|
||||
from click.testing import CliRunner
|
||||
|
||||
from scripts.distribute_molecule import cli
|
||||
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cli, ["--list-platforms"])
|
||||
assert result.exit_code == 0
|
||||
assert "ubuntu-2204" in result.output
|
||||
assert "ubuntu-2404" in result.output
|
||||
assert "debian-12" in result.output
|
||||
assert "archlinux" in result.output
|
||||
|
||||
def test_no_runner_index_prints_all_groups(self, tmp_path: Path) -> None:
|
||||
from click.testing import CliRunner
|
||||
|
||||
@@ -115,13 +181,14 @@ class TestCli:
|
||||
from scripts.distribute_molecule import cli
|
||||
|
||||
root = tmp_path / "molecule"
|
||||
for s in ["a", "b", "c"]:
|
||||
(root / s).mkdir(parents=True)
|
||||
(root / "alpha").mkdir(parents=True)
|
||||
with patch("scripts.distribute_molecule.MOLECULE_ROOT", root):
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cli, ["--runner-index", "1", "--max-runners", "3"])
|
||||
result = runner.invoke(cli, ["--runner-index", "0", "--max-runners", "3"])
|
||||
assert result.exit_code == 0
|
||||
assert result.output.strip() == "b"
|
||||
# Output should contain encoded pairs with platform info
|
||||
assert "alpha|" in result.output
|
||||
assert "ubuntu-2204" in result.output
|
||||
|
||||
|
||||
def test_main_module_block() -> None:
|
||||
|
||||
@@ -22,7 +22,6 @@ class TestRunnerRegistry:
|
||||
host="10.0.0.1",
|
||||
user="ubuntu",
|
||||
key="/key",
|
||||
mode="docker",
|
||||
gitea_url="https://git.example.com",
|
||||
labels="docker:docker://alpine:latest",
|
||||
)
|
||||
@@ -31,7 +30,6 @@ class TestRunnerRegistry:
|
||||
assert info["host"] == "10.0.0.1"
|
||||
assert info["user"] == "ubuntu"
|
||||
assert info["key"] == "/key"
|
||||
assert info["mode"] == "docker"
|
||||
assert info["gitea_url"] == "https://git.example.com"
|
||||
assert info["labels"] == "docker:docker://alpine:latest"
|
||||
assert "created_at" in info
|
||||
|
||||
@@ -33,14 +33,12 @@ class TestRunnerManager:
|
||||
assert "registration_token=tok" in cmd_str
|
||||
assert "runner_name=192.168.1.10" in cmd_str
|
||||
assert "gitea_url=https://git.example.com" in cmd_str
|
||||
assert "runner_mode=docker" in cmd_str
|
||||
assert "Installing Gitea Runner on 192.168.1.10" in mock_executor.run.call_args.kwargs["description"]
|
||||
mock_registry.add.assert_called_once_with(
|
||||
name="192.168.1.10",
|
||||
host="192.168.1.10",
|
||||
user="ubuntu",
|
||||
key=None,
|
||||
mode="docker",
|
||||
gitea_url="https://git.example.com",
|
||||
labels=None,
|
||||
)
|
||||
@@ -60,14 +58,12 @@ class TestRunnerManager:
|
||||
assert "/key" in cmd_str
|
||||
assert "registration_token=preset" in cmd_str
|
||||
assert "runner_name=my-runner" in cmd_str
|
||||
assert "runner_mode=docker" in cmd_str
|
||||
assert "--ask-become-pass" not in cmd_str
|
||||
mock_registry.add.assert_called_once_with(
|
||||
name="my-runner",
|
||||
host="host1",
|
||||
user="root",
|
||||
key="/key",
|
||||
mode="docker",
|
||||
gitea_url="https://git.example.com",
|
||||
labels=None,
|
||||
)
|
||||
@@ -82,27 +78,6 @@ class TestRunnerManager:
|
||||
cmd = mock_executor.run.call_args.args[0]
|
||||
cmd_str = " ".join(cmd)
|
||||
assert "--ask-become-pass" in cmd_str
|
||||
assert "runner_mode=docker" in cmd_str
|
||||
|
||||
def test_install_binary_mode(self) -> None:
|
||||
mock_registry = MagicMock()
|
||||
manager = RunnerManager(registry=mock_registry)
|
||||
mock_executor = MagicMock()
|
||||
manager._executor = mock_executor
|
||||
|
||||
manager.install("host1", "root", token="tok", gitea_url="https://git.example.com", mode="binary")
|
||||
cmd = mock_executor.run.call_args.args[0]
|
||||
cmd_str = " ".join(cmd)
|
||||
assert "runner_mode=binary" in cmd_str
|
||||
mock_registry.add.assert_called_once_with(
|
||||
name="host1",
|
||||
host="host1",
|
||||
user="root",
|
||||
key=None,
|
||||
mode="binary",
|
||||
gitea_url="https://git.example.com",
|
||||
labels=None,
|
||||
)
|
||||
|
||||
def test_install_missing_gitea_url(self) -> None:
|
||||
manager = RunnerManager()
|
||||
@@ -181,7 +156,6 @@ class TestRunnerManager:
|
||||
assert "--private-key" in cmd_str
|
||||
assert "/key" in cmd_str
|
||||
assert "gitea_runner_version=v0.2.0" in cmd_str
|
||||
assert "runner_mode=docker" in cmd_str
|
||||
assert "--ask-become-pass" not in cmd_str
|
||||
assert "Updating Gitea Runner on host" in mock_executor.run.call_args.kwargs["description"]
|
||||
|
||||
@@ -194,17 +168,6 @@ class TestRunnerManager:
|
||||
cmd = mock_executor.run.call_args.args[0]
|
||||
cmd_str = " ".join(cmd)
|
||||
assert "--ask-become-pass" in cmd_str
|
||||
assert "runner_mode=docker" in cmd_str
|
||||
|
||||
def test_update_binary_mode(self) -> None:
|
||||
manager = RunnerManager()
|
||||
mock_executor = MagicMock()
|
||||
manager._executor = mock_executor
|
||||
|
||||
manager.update("host", "user", mode="binary")
|
||||
cmd = mock_executor.run.call_args.args[0]
|
||||
cmd_str = " ".join(cmd)
|
||||
assert "runner_mode=binary" in cmd_str
|
||||
|
||||
def test_update_playbook_not_found(self) -> None:
|
||||
manager = RunnerManager()
|
||||
@@ -226,15 +189,13 @@ class TestRunnerManager:
|
||||
"host": "10.0.0.1",
|
||||
"user": "ubuntu",
|
||||
"key": "/key",
|
||||
"mode": "docker",
|
||||
"gitea_url": "https://git.example.com",
|
||||
}
|
||||
manager = RunnerManager(registry=mock_registry)
|
||||
host, user, key, mode, gitea_url = manager._resolve_runner("r1")
|
||||
host, user, key, gitea_url = manager._resolve_runner("r1")
|
||||
assert host == "10.0.0.1"
|
||||
assert user == "ubuntu"
|
||||
assert key == "/key"
|
||||
assert mode == "docker"
|
||||
assert gitea_url == "https://git.example.com"
|
||||
mock_registry.get.assert_called_once_with("r1")
|
||||
|
||||
@@ -242,11 +203,10 @@ class TestRunnerManager:
|
||||
mock_registry = MagicMock()
|
||||
mock_registry.get.return_value = None
|
||||
manager = RunnerManager(registry=mock_registry)
|
||||
host, user, key, mode, gitea_url = manager._resolve_runner("r1", host="10.0.0.2", user="root")
|
||||
host, user, key, gitea_url = manager._resolve_runner("r1", host="10.0.0.2", user="root")
|
||||
assert host == "10.0.0.2"
|
||||
assert user == "root"
|
||||
assert key is None
|
||||
assert mode == "docker"
|
||||
assert gitea_url == ""
|
||||
|
||||
def test_resolve_runner_override(self) -> None:
|
||||
@@ -255,17 +215,13 @@ class TestRunnerManager:
|
||||
"host": "10.0.0.1",
|
||||
"user": "ubuntu",
|
||||
"key": "/key",
|
||||
"mode": "docker",
|
||||
"gitea_url": "https://git.example.com",
|
||||
}
|
||||
manager = RunnerManager(registry=mock_registry)
|
||||
host, user, key, mode, gitea_url = manager._resolve_runner(
|
||||
"r1", host="10.0.0.2", user="root", key="/new", mode="binary"
|
||||
)
|
||||
host, user, key, gitea_url = manager._resolve_runner("r1", host="10.0.0.2", user="root", key="/new")
|
||||
assert host == "10.0.0.2"
|
||||
assert user == "root"
|
||||
assert key == "/new"
|
||||
assert mode == "binary"
|
||||
assert gitea_url == "https://git.example.com"
|
||||
|
||||
def test_resolve_runner_not_found(self) -> None:
|
||||
@@ -277,7 +233,7 @@ class TestRunnerManager:
|
||||
|
||||
def test_start(self) -> None:
|
||||
mock_registry = MagicMock()
|
||||
mock_registry.get.return_value = {"host": "host", "user": "user", "key": None, "mode": "docker"}
|
||||
mock_registry.get.return_value = {"host": "host", "user": "user", "key": None}
|
||||
manager = RunnerManager(registry=mock_registry)
|
||||
mock_executor = MagicMock()
|
||||
manager._executor = mock_executor
|
||||
@@ -287,22 +243,20 @@ class TestRunnerManager:
|
||||
cmd_str = " ".join(cmd)
|
||||
assert "start-runner.yml" in cmd_str
|
||||
assert "runner_name=r1" in cmd_str
|
||||
assert "runner_mode=docker" in cmd_str
|
||||
assert "Starting Gitea Runner r1 on host" in mock_executor.run.call_args.kwargs["description"]
|
||||
|
||||
def test_start_with_override(self) -> None:
|
||||
mock_registry = MagicMock()
|
||||
mock_registry.get.return_value = {"host": "host", "user": "user", "key": None, "mode": "docker"}
|
||||
mock_registry.get.return_value = {"host": "host", "user": "user", "key": None}
|
||||
manager = RunnerManager(registry=mock_registry)
|
||||
mock_executor = MagicMock()
|
||||
manager._executor = mock_executor
|
||||
|
||||
manager.start("r1", host="newhost", user="newuser", mode="binary")
|
||||
manager.start("r1", host="newhost", user="newuser")
|
||||
cmd = mock_executor.run.call_args.args[0]
|
||||
cmd_str = " ".join(cmd)
|
||||
assert "newhost," in cmd_str
|
||||
assert "newuser" in cmd_str
|
||||
assert "runner_mode=binary" in cmd_str
|
||||
|
||||
def test_start_no_registry_with_explicit(self) -> None:
|
||||
mock_registry = MagicMock()
|
||||
@@ -311,14 +265,14 @@ class TestRunnerManager:
|
||||
mock_executor = MagicMock()
|
||||
manager._executor = mock_executor
|
||||
|
||||
manager.start("r1", host="host", user="user", mode="docker")
|
||||
manager.start("r1", host="host", user="user")
|
||||
cmd = mock_executor.run.call_args.args[0]
|
||||
cmd_str = " ".join(cmd)
|
||||
assert "start-runner.yml" in cmd_str
|
||||
|
||||
def test_stop(self) -> None:
|
||||
mock_registry = MagicMock()
|
||||
mock_registry.get.return_value = {"host": "host", "user": "user", "key": None, "mode": "docker"}
|
||||
mock_registry.get.return_value = {"host": "host", "user": "user", "key": None}
|
||||
manager = RunnerManager(registry=mock_registry)
|
||||
mock_executor = MagicMock()
|
||||
manager._executor = mock_executor
|
||||
@@ -332,7 +286,7 @@ class TestRunnerManager:
|
||||
|
||||
def test_enable(self) -> None:
|
||||
mock_registry = MagicMock()
|
||||
mock_registry.get.return_value = {"host": "host", "user": "user", "key": None, "mode": "docker"}
|
||||
mock_registry.get.return_value = {"host": "host", "user": "user", "key": None}
|
||||
manager = RunnerManager(registry=mock_registry)
|
||||
mock_executor = MagicMock()
|
||||
manager._executor = mock_executor
|
||||
@@ -350,7 +304,6 @@ class TestRunnerManager:
|
||||
"host": "host",
|
||||
"user": "user",
|
||||
"key": None,
|
||||
"mode": "docker",
|
||||
"gitea_url": "",
|
||||
}
|
||||
manager = RunnerManager(registry=mock_registry)
|
||||
@@ -364,7 +317,6 @@ class TestRunnerManager:
|
||||
assert "runner_name=r1" in cmd_str
|
||||
assert "registration_token=tok" in cmd_str
|
||||
assert "gitea_url=https://git.example.com" in cmd_str
|
||||
assert "runner_mode=docker" in cmd_str
|
||||
assert "Disabling Gitea Runner r1 on host" in mock_executor.run.call_args.kwargs["description"]
|
||||
|
||||
def test_disable_uses_registry_gitea_url(self) -> None:
|
||||
@@ -373,7 +325,6 @@ class TestRunnerManager:
|
||||
"host": "host",
|
||||
"user": "user",
|
||||
"key": None,
|
||||
"mode": "docker",
|
||||
"gitea_url": "https://registry.example.com",
|
||||
}
|
||||
manager = RunnerManager(registry=mock_registry)
|
||||
@@ -401,7 +352,7 @@ class TestRunnerManager:
|
||||
|
||||
def test_status(self) -> None:
|
||||
mock_registry = MagicMock()
|
||||
mock_registry.get.return_value = {"host": "host", "user": "user", "key": None, "mode": "binary"}
|
||||
mock_registry.get.return_value = {"host": "host", "user": "user", "key": None}
|
||||
manager = RunnerManager(registry=mock_registry)
|
||||
mock_executor = MagicMock()
|
||||
manager._executor = mock_executor
|
||||
@@ -411,7 +362,6 @@ class TestRunnerManager:
|
||||
cmd_str = " ".join(cmd)
|
||||
assert "status-runner.yml" in cmd_str
|
||||
assert "runner_name=r1" in cmd_str
|
||||
assert "runner_mode=binary" in cmd_str
|
||||
assert "Checking status of Gitea Runner r1 on host" in mock_executor.run.call_args.kwargs["description"]
|
||||
|
||||
def test_remove(self) -> None:
|
||||
@@ -420,7 +370,6 @@ class TestRunnerManager:
|
||||
"host": "host",
|
||||
"user": "user",
|
||||
"key": None,
|
||||
"mode": "docker",
|
||||
"gitea_url": "",
|
||||
}
|
||||
manager = RunnerManager(registry=mock_registry)
|
||||
@@ -434,7 +383,6 @@ class TestRunnerManager:
|
||||
assert "runner_name=r1" in cmd_str
|
||||
assert "registration_token=tok" in cmd_str
|
||||
assert "gitea_url=https://git.example.com" in cmd_str
|
||||
assert "runner_mode=docker" in cmd_str
|
||||
assert "Removing Gitea Runner r1 from host" in mock_executor.run.call_args.kwargs["description"]
|
||||
mock_registry.remove.assert_called_once_with("r1")
|
||||
|
||||
@@ -470,7 +418,6 @@ class TestRunnerManager:
|
||||
"host": "host",
|
||||
"user": "user",
|
||||
"key": None,
|
||||
"mode": "docker",
|
||||
"gitea_url": "",
|
||||
}
|
||||
manager = RunnerManager(registry=mock_registry)
|
||||
@@ -493,14 +440,14 @@ class TestRunnerManager:
|
||||
mock_executor.run.assert_not_called()
|
||||
mock_registry.remove.assert_called_once_with("r1")
|
||||
|
||||
def test_list_runners_docker(self) -> None:
|
||||
def test_list_runners(self) -> None:
|
||||
mock_registry = MagicMock()
|
||||
mock_registry.list.return_value = {
|
||||
"r1": {"host": "10.0.0.1", "user": "ubuntu", "key": "/key", "mode": "docker"},
|
||||
"r1": {"host": "10.0.0.1", "user": "ubuntu", "key": "/key"},
|
||||
}
|
||||
manager = RunnerManager(registry=mock_registry)
|
||||
mock_executor = MagicMock()
|
||||
mock_executor.run_ad_hoc.return_value = "running"
|
||||
mock_executor.run_ad_hoc.return_value = "active"
|
||||
manager._executor = mock_executor
|
||||
|
||||
runners = manager.list_runners()
|
||||
@@ -513,44 +460,16 @@ class TestRunnerManager:
|
||||
"ubuntu",
|
||||
"/key",
|
||||
"shell",
|
||||
(
|
||||
"docker inspect gitea-runner-r1 2>/dev/null | "
|
||||
"python3 -c \"import sys,json; print(json.load(sys.stdin)[0]['State']['Status'])\" "
|
||||
"2>/dev/null || echo unknown"
|
||||
),
|
||||
"sudo -u grm-r1 systemctl --user is-active gitea-runner 2>/dev/null",
|
||||
become=True,
|
||||
ask_become_pass=True,
|
||||
check=False,
|
||||
)
|
||||
|
||||
def test_list_runners_binary(self) -> None:
|
||||
def test_list_runners_exception(self) -> None:
|
||||
mock_registry = MagicMock()
|
||||
mock_registry.list.return_value = {
|
||||
"r1": {"host": "10.0.0.1", "user": "ubuntu", "key": "/key", "mode": "binary"},
|
||||
}
|
||||
manager = RunnerManager(registry=mock_registry)
|
||||
mock_executor = MagicMock()
|
||||
mock_executor.run_ad_hoc.return_value = "active"
|
||||
manager._executor = mock_executor
|
||||
|
||||
runners = manager.list_runners()
|
||||
assert len(runners) == 1
|
||||
assert runners[0]["status"] == "active"
|
||||
mock_executor.run_ad_hoc.assert_called_once_with(
|
||||
"10.0.0.1",
|
||||
"ubuntu",
|
||||
"/key",
|
||||
"shell",
|
||||
"systemctl is-active gitea-runner@r1 2>/dev/null",
|
||||
become=True,
|
||||
ask_become_pass=True,
|
||||
check=False,
|
||||
)
|
||||
|
||||
def test_list_runners_binary_exception(self) -> None:
|
||||
mock_registry = MagicMock()
|
||||
mock_registry.list.return_value = {
|
||||
"r1": {"host": "10.0.0.1", "user": "ubuntu", "key": None, "mode": "binary"},
|
||||
"r1": {"host": "10.0.0.1", "user": "ubuntu", "key": None},
|
||||
}
|
||||
manager = RunnerManager(registry=mock_registry)
|
||||
mock_executor = MagicMock()
|
||||
@@ -569,7 +488,7 @@ class TestRunnerManager:
|
||||
def test_list_runners_without_labels(self) -> None:
|
||||
mock_registry = MagicMock()
|
||||
mock_registry.list.return_value = {
|
||||
"r1": {"host": "10.0.0.1", "user": "ubuntu", "key": None, "mode": "docker"},
|
||||
"r1": {"host": "10.0.0.1", "user": "ubuntu", "key": None},
|
||||
}
|
||||
manager = RunnerManager(registry=mock_registry)
|
||||
mock_executor = MagicMock()
|
||||
@@ -579,38 +498,23 @@ class TestRunnerManager:
|
||||
runners = manager.list_runners()
|
||||
assert runners[0]["labels"] == ""
|
||||
|
||||
def test_list_runners_docker_exited(self) -> None:
|
||||
def test_list_runners_inactive(self) -> None:
|
||||
mock_registry = MagicMock()
|
||||
mock_registry.list.return_value = {
|
||||
"r1": {"host": "10.0.0.1", "user": "ubuntu", "key": None, "mode": "docker"},
|
||||
"r1": {"host": "10.0.0.1", "user": "ubuntu", "key": None},
|
||||
}
|
||||
manager = RunnerManager(registry=mock_registry)
|
||||
mock_executor = MagicMock()
|
||||
mock_executor.run_ad_hoc.return_value = "exited"
|
||||
mock_executor.run_ad_hoc.return_value = "inactive"
|
||||
manager._executor = mock_executor
|
||||
|
||||
runners = manager.list_runners()
|
||||
assert runners[0]["status"] == "inactive"
|
||||
|
||||
def test_list_runners_docker_fallback_to_host_container(self) -> None:
|
||||
mock_registry = MagicMock()
|
||||
mock_registry.list.return_value = {
|
||||
"r1": {"host": "10.0.0.1", "user": "ubuntu", "key": None, "mode": "docker"},
|
||||
}
|
||||
manager = RunnerManager(registry=mock_registry)
|
||||
mock_executor = MagicMock()
|
||||
# First call (name-based) returns unknown, second (host-based) returns running
|
||||
mock_executor.run_ad_hoc.side_effect = ["unknown", "running"]
|
||||
manager._executor = mock_executor
|
||||
|
||||
runners = manager.list_runners()
|
||||
assert runners[0]["status"] == "active"
|
||||
assert mock_executor.run_ad_hoc.call_count == 2
|
||||
|
||||
def test_list_runners_unknown_status(self) -> None:
|
||||
mock_registry = MagicMock()
|
||||
mock_registry.list.return_value = {
|
||||
"r1": {"host": "10.0.0.1", "user": "ubuntu", "key": None, "mode": "docker"},
|
||||
"r1": {"host": "10.0.0.1", "user": "ubuntu", "key": None},
|
||||
}
|
||||
manager = RunnerManager(registry=mock_registry)
|
||||
mock_executor = MagicMock()
|
||||
@@ -620,25 +524,10 @@ class TestRunnerManager:
|
||||
runners = manager.list_runners()
|
||||
assert runners[0]["status"] == "unknown"
|
||||
|
||||
def test_list_runners_docker_first_command_falls_back(self) -> None:
|
||||
"""When the first docker inspect raises, the loop continues to the next fallback."""
|
||||
mock_registry = MagicMock()
|
||||
mock_registry.list.return_value = {
|
||||
"r1": {"host": "10.0.0.1", "user": "ubuntu", "key": None, "mode": "docker"},
|
||||
}
|
||||
manager = RunnerManager(registry=mock_registry)
|
||||
mock_executor = MagicMock()
|
||||
mock_executor.run_ad_hoc.side_effect = [AnsibleError("ssh fail"), "running"]
|
||||
manager._executor = mock_executor
|
||||
|
||||
runners = manager.list_runners()
|
||||
assert runners[0]["status"] == "active"
|
||||
assert mock_executor.run_ad_hoc.call_count == 2
|
||||
|
||||
def test_list_runners_ad_hoc_error(self) -> None:
|
||||
mock_registry = MagicMock()
|
||||
mock_registry.list.return_value = {
|
||||
"r1": {"host": "10.0.0.1", "user": "ubuntu", "key": None, "mode": "docker"},
|
||||
"r1": {"host": "10.0.0.1", "user": "ubuntu", "key": None},
|
||||
}
|
||||
manager = RunnerManager(registry=mock_registry)
|
||||
mock_executor = MagicMock()
|
||||
|
||||
Reference in New Issue
Block a user