Files
devx/tests/unit/test_distribute_molecule.py
T

262 lines
10 KiB
Python

"""Unit tests for scripts/ci/distribute_molecule.py."""
from pathlib import Path
from unittest.mock import patch
import click
import pytest
from click.testing import CliRunner
from devx.molecule.distribute_molecule import (
MOLECULE_ROOT,
PLATFORMS,
TestPair,
build_pairs,
cli,
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 devx.molecule.distribute_molecule import cli
root = tmp_path / "molecule"
(root / "alpha").mkdir(parents=True)
(root / "beta").mkdir(parents=True)
with patch("devx.molecule.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 devx.molecule.distribute_molecule import cli
with patch("devx.molecule.distribute_molecule.discover_scenarios", return_value=["dummy"]):
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 devx.molecule.distribute_molecule import cli
root = tmp_path / "molecule"
for s in ["a", "b", "c"]:
(root / s).mkdir(parents=True)
with patch("devx.molecule.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 devx.molecule.distribute_molecule import cli
root = tmp_path / "molecule"
(root / "alpha").mkdir(parents=True)
with patch("devx.molecule.distribute_molecule.MOLECULE_ROOT", root):
runner = CliRunner()
# 1-based index: "1" maps to internal 0
result = runner.invoke(cli, ["--runner-index", "1", "--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
class TestGithubEnv:
def test_writes_env(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
gh_file = tmp_path / "env.txt"
monkeypatch.setenv("GITHUB_ENV", str(gh_file))
root = tmp_path / "molecule"
scenario = root / "alpha"
scenario.mkdir(parents=True)
(scenario / "molecule.yml").write_text("name: alpha\n")
with patch("devx.molecule.distribute_molecule.MOLECULE_ROOT", root):
runner = CliRunner()
result = runner.invoke(cli, ["--runner-index", "1", "--max-runners", "3", "--github-env"])
assert result.exit_code == 0
content = gh_file.read_text()
assert "TEST_PAIRS=" in content
assert "SKIP=false" in content
def test_skip_if_excess(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
gh_file = tmp_path / "env.txt"
monkeypatch.setenv("GITHUB_ENV", str(gh_file))
root = tmp_path / "molecule"
scenario = root / "alpha"
scenario.mkdir(parents=True)
(scenario / "molecule.yml").write_text("name: alpha\n")
with patch("devx.molecule.distribute_molecule.MOLECULE_ROOT", root):
runner = CliRunner()
result = runner.invoke(
cli, ["--runner-index", "5", "--max-runners", "3", "--github-env", "--skip-if-excess"]
)
assert result.exit_code == 0
content = gh_file.read_text()
assert "TEST_PAIRS=\n" in content
assert "SKIP=true" in content
def test_no_env_var(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delenv("GITHUB_ENV", raising=False)
root = tmp_path / "molecule"
scenario = root / "alpha"
scenario.mkdir(parents=True)
(scenario / "molecule.yml").write_text("name: alpha\n")
with patch("devx.molecule.distribute_molecule.MOLECULE_ROOT", root):
runner = CliRunner()
result = runner.invoke(cli, ["--runner-index", "1", "--max-runners", "3", "--github-env"])
assert result.exit_code != 0
class TestRunnerIndexValidation:
def test_runner_index_zero_raises(self) -> None:
"""Runner index < 1 should raise."""
with patch("devx.molecule.distribute_molecule.discover_scenarios", return_value=["dummy"]):
runner = CliRunner()
result = runner.invoke(cli, ["--runner-index", "0", "--max-runners", "3"])
assert result.exit_code != 0
assert "out of range" in result.output
def test_main_module_block() -> None:
import devx.molecule.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"])