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>
203 lines
7.3 KiB
Python
203 lines
7.3 KiB
Python
"""Unit tests for scripts/distribute_molecule.py."""
|
|
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
import click
|
|
import pytest
|
|
|
|
from scripts.distribute_molecule import (
|
|
MOLECULE_ROOT,
|
|
PLATFORMS,
|
|
TestPair,
|
|
build_pairs,
|
|
discover_scenarios,
|
|
distribute,
|
|
pairs_for_runner,
|
|
)
|
|
|
|
|
|
class TestDiscoverScenarios:
|
|
def test_discovers_scenarios(self, tmp_path: Path) -> None:
|
|
root = tmp_path / "molecule"
|
|
(root / "default").mkdir(parents=True)
|
|
(root / "binary").mkdir(parents=True)
|
|
(root / "common").mkdir(parents=True)
|
|
(root / "_shared").mkdir(parents=True)
|
|
result = discover_scenarios(root)
|
|
assert result == ["binary", "default"]
|
|
|
|
def test_raises_when_dir_missing(self, tmp_path: Path) -> None:
|
|
with pytest.raises(click.ClickException) as exc:
|
|
discover_scenarios(tmp_path / "nonexistent")
|
|
assert "not found" in str(exc.value)
|
|
|
|
def test_default_root_constant(self) -> None:
|
|
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:
|
|
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:
|
|
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_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_empty_pairs(self) -> None:
|
|
groups = distribute([], 3)
|
|
assert groups == [[], [], []]
|
|
|
|
|
|
class TestPairsForRunner:
|
|
def test_returns_correct_subset(self) -> None:
|
|
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:
|
|
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:
|
|
pairs_for_runner(pairs, -1, 3)
|
|
assert "out of range" in str(exc.value)
|
|
|
|
|
|
class TestCli:
|
|
def test_list_flag(self, tmp_path: Path) -> None:
|
|
from click.testing import CliRunner
|
|
|
|
from scripts.distribute_molecule import cli
|
|
|
|
root = tmp_path / "molecule"
|
|
(root / "alpha").mkdir(parents=True)
|
|
(root / "beta").mkdir(parents=True)
|
|
with patch("scripts.distribute_molecule.MOLECULE_ROOT", root):
|
|
runner = CliRunner()
|
|
result = runner.invoke(cli, ["--list"])
|
|
assert result.exit_code == 0
|
|
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
|
|
|
|
from scripts.distribute_molecule import cli
|
|
|
|
root = tmp_path / "molecule"
|
|
for s in ["a", "b", "c"]:
|
|
(root / s).mkdir(parents=True)
|
|
with patch("scripts.distribute_molecule.MOLECULE_ROOT", root):
|
|
runner = CliRunner()
|
|
result = runner.invoke(cli, ["--max-runners", "3"])
|
|
assert result.exit_code == 0
|
|
assert "Runner 0:" in result.output
|
|
assert "Runner 1:" in result.output
|
|
assert "Runner 2:" in result.output
|
|
|
|
def test_runner_index_prints_assigned(self, tmp_path: Path) -> None:
|
|
from click.testing import CliRunner
|
|
|
|
from scripts.distribute_molecule import cli
|
|
|
|
root = tmp_path / "molecule"
|
|
(root / "alpha").mkdir(parents=True)
|
|
with patch("scripts.distribute_molecule.MOLECULE_ROOT", root):
|
|
runner = CliRunner()
|
|
result = runner.invoke(cli, ["--runner-index", "0", "--max-runners", "3"])
|
|
assert result.exit_code == 0
|
|
# 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:
|
|
import scripts.distribute_molecule as dm
|
|
|
|
with open(dm.__file__) as f:
|
|
source = f.read()
|
|
source = source.replace('if __name__ == "__main__":\n cli()\n', "")
|
|
namespace = dict(dm.__dict__)
|
|
exec(compile(source, dm.__file__, "exec"), namespace)
|
|
assert callable(namespace["cli"])
|