GRM-26: fix: CI pipeline for rootless Docker runners
This commit was merged in pull request #5.
This commit is contained in:
+37
-23
@@ -2,42 +2,56 @@ name: CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [master]
|
||||
push:
|
||||
branches: [master]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
quality:
|
||||
runs-on: docker
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python3 -m venv .venv
|
||||
.venv/bin/pip install -e ".[dev]"
|
||||
.venv/bin/ansible-galaxy collection install -r ansible/requirements.yml
|
||||
- name: Set up environment
|
||||
run: make setup
|
||||
- name: Lint all
|
||||
run: make lint-all
|
||||
|
||||
unit-tests:
|
||||
runs-on: docker
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python3 -m venv .venv
|
||||
.venv/bin/pip install -e ".[dev]"
|
||||
. .venv/bin/activate
|
||||
make lint-all
|
||||
- name: Unit tests with 100% coverage
|
||||
run: make pytest-cov
|
||||
run: |
|
||||
. .venv/bin/activate
|
||||
make pytest-cov
|
||||
- name: Check unit test speed
|
||||
run: |
|
||||
. .venv/bin/activate
|
||||
python3 scripts/check_test_speed.py --max-seconds 10
|
||||
|
||||
molecule-tests:
|
||||
needs: quality
|
||||
runs-on: docker
|
||||
strategy:
|
||||
matrix:
|
||||
runner-index: [0, 1, 2]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install dependencies
|
||||
- name: Set up environment
|
||||
run: make setup
|
||||
- name: Discover assigned scenarios
|
||||
run: |
|
||||
python3 -m venv .venv
|
||||
.venv/bin/pip install -e ".[dev]"
|
||||
.venv/bin/ansible-galaxy collection install -r ansible/requirements.yml
|
||||
- name: Molecule tests (all 7 scenarios)
|
||||
run: make molecule
|
||||
. .venv/bin/activate
|
||||
SCENARIOS=$(python3 scripts/distribute_molecule.py --runner-index ${{ matrix.runner-index }} --max-runners 3)
|
||||
echo "Assigned scenarios: $SCENARIOS"
|
||||
echo "SCENARIOS=$SCENARIOS" >> $GITHUB_ENV
|
||||
- name: Run molecule tests
|
||||
run: |
|
||||
. .venv/bin/activate
|
||||
export DOCKER_HOST="unix:///run/user/$(id -u)/docker.sock"
|
||||
export ANSIBLE_INJECT_INVOCATION=1
|
||||
cd ansible/roles/gitea-runner
|
||||
for s in $SCENARIOS; do
|
||||
if [ "$s" = "default" ]; then
|
||||
ANSIBLE_ALLOW_BROKEN_CONDITIONALS=true molecule test
|
||||
else
|
||||
ANSIBLE_ALLOW_BROKEN_CONDITIONALS=true molecule test -s "$s"
|
||||
fi
|
||||
done
|
||||
|
||||
@@ -32,7 +32,7 @@ install-hooks:
|
||||
@echo "Git hooks installed."
|
||||
|
||||
checkmake:
|
||||
@which checkmake >/dev/null 2>&1 || (which go >/dev/null 2>&1 && go install github.com/mrtazz/checkmake/cmd/checkmake@latest) || (echo "Warning: checkmake not installed. Install Go and run: go install github.com/mrtazz/checkmake/cmd/checkmake@latest" && exit 0)
|
||||
@python3 scripts/install_checkmake.py
|
||||
|
||||
install:
|
||||
@if [ -z "$(HOST)" ]; then echo "HOST is required. Example: make install HOST=192.168.1.10"; exit 1; fi
|
||||
|
||||
@@ -11,18 +11,30 @@
|
||||
|
||||
- name: Stop gitea-runner systemd instance
|
||||
ansible.builtin.systemd:
|
||||
name: "gitea-runner@{{ runner_name }}"
|
||||
name: "gitea-runner-{{ runner_mode | default('docker') }}@{{ runner_name }}"
|
||||
state: stopped
|
||||
daemon_reload: true
|
||||
when: systemd_available.stat.exists
|
||||
|
||||
- name: Disable gitea-runner systemd instance
|
||||
ansible.builtin.systemd:
|
||||
name: "gitea-runner@{{ runner_name }}"
|
||||
name: "gitea-runner-{{ runner_mode | default('docker') }}@{{ runner_name }}"
|
||||
enabled: false
|
||||
daemon_reload: true
|
||||
when: systemd_available.stat.exists
|
||||
|
||||
- name: Stop Docker container directly
|
||||
ansible.builtin.command: "docker stop -t 30 gitea-runner-{{ runner_name }}"
|
||||
changed_when: true
|
||||
failed_when: false
|
||||
when: runner_mode | default('docker') == 'docker'
|
||||
|
||||
- name: Remove Docker container directly
|
||||
ansible.builtin.command: "docker rm -f gitea-runner-{{ runner_name }}"
|
||||
changed_when: true
|
||||
failed_when: false
|
||||
when: runner_mode | default('docker') == 'docker'
|
||||
|
||||
- name: Include deregistration
|
||||
ansible.builtin.include_role:
|
||||
name: gitea-runner
|
||||
@@ -31,7 +43,7 @@
|
||||
|
||||
- name: Remove systemd unit file (last instance)
|
||||
ansible.builtin.file:
|
||||
path: "/etc/systemd/system/gitea-runner@.service"
|
||||
path: "/etc/systemd/system/gitea-runner-{{ runner_mode | default('docker') }}@.service"
|
||||
state: absent
|
||||
when: remove_systemd_template | default(false)
|
||||
|
||||
|
||||
@@ -4,13 +4,13 @@ driver:
|
||||
|
||||
platforms:
|
||||
- name: ${MOLECULE_PLATFORM_NAME:-ubuntu-2204}
|
||||
image: ${MOLECULE_PLATFORM_IMAGE:-geerlingguy/docker-ubuntu2204-ansible:latest}
|
||||
image: ${MOLECULE_PLATFORM_IMAGE:-ubuntu:22.04}
|
||||
command: ${MOLECULE_PLATFORM_COMMAND:-sleep infinity}
|
||||
volumes:
|
||||
- /sys/fs/cgroup:/sys/fs/cgroup:rw
|
||||
cgroupns_mode: host
|
||||
privileged: true
|
||||
pre_build_image: true
|
||||
pre_build_image: false
|
||||
|
||||
provisioner:
|
||||
name: ansible
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
- name: Update apt cache
|
||||
ansible.builtin.apt:
|
||||
update_cache: true
|
||||
cache_valid_time: 0
|
||||
when: ansible_facts['os_family'] == 'Debian'
|
||||
|
||||
- name: Install python3-debian for deb822_repository module
|
||||
|
||||
@@ -4,13 +4,13 @@ driver:
|
||||
|
||||
platforms:
|
||||
- name: ${MOLECULE_PLATFORM_NAME:-ubuntu-2204}
|
||||
image: ${MOLECULE_PLATFORM_IMAGE:-geerlingguy/docker-ubuntu2204-ansible:latest}
|
||||
image: ${MOLECULE_PLATFORM_IMAGE:-ubuntu:22.04}
|
||||
command: ${MOLECULE_PLATFORM_COMMAND:-sleep infinity}
|
||||
volumes:
|
||||
- /sys/fs/cgroup:/sys/fs/cgroup:rw
|
||||
cgroupns_mode: host
|
||||
privileged: true
|
||||
pre_build_image: true
|
||||
pre_build_image: false
|
||||
|
||||
provisioner:
|
||||
name: ansible
|
||||
|
||||
@@ -4,13 +4,13 @@ driver:
|
||||
|
||||
platforms:
|
||||
- name: ${MOLECULE_PLATFORM_NAME:-ubuntu-2204}
|
||||
image: ${MOLECULE_PLATFORM_IMAGE:-geerlingguy/docker-ubuntu2204-ansible:latest}
|
||||
image: ${MOLECULE_PLATFORM_IMAGE:-ubuntu:22.04}
|
||||
command: ${MOLECULE_PLATFORM_COMMAND:-sleep infinity}
|
||||
volumes:
|
||||
- /sys/fs/cgroup:/sys/fs/cgroup:rw
|
||||
cgroupns_mode: host
|
||||
privileged: true
|
||||
pre_build_image: true
|
||||
pre_build_image: false
|
||||
|
||||
provisioner:
|
||||
name: ansible
|
||||
|
||||
@@ -4,13 +4,13 @@ driver:
|
||||
|
||||
platforms:
|
||||
- name: ${MOLECULE_PLATFORM_NAME:-ubuntu-2204}
|
||||
image: ${MOLECULE_PLATFORM_IMAGE:-geerlingguy/docker-ubuntu2204-ansible:latest}
|
||||
image: ${MOLECULE_PLATFORM_IMAGE:-ubuntu:22.04}
|
||||
command: ${MOLECULE_PLATFORM_COMMAND:-sleep infinity}
|
||||
volumes:
|
||||
- /sys/fs/cgroup:/sys/fs/cgroup:rw
|
||||
cgroupns_mode: host
|
||||
privileged: true
|
||||
pre_build_image: true
|
||||
pre_build_image: false
|
||||
|
||||
provisioner:
|
||||
name: ansible
|
||||
|
||||
@@ -4,13 +4,13 @@ driver:
|
||||
|
||||
platforms:
|
||||
- name: ${MOLECULE_PLATFORM_NAME:-ubuntu-2204}
|
||||
image: ${MOLECULE_PLATFORM_IMAGE:-geerlingguy/docker-ubuntu2204-ansible:latest}
|
||||
image: ${MOLECULE_PLATFORM_IMAGE:-ubuntu:22.04}
|
||||
command: ${MOLECULE_PLATFORM_COMMAND:-sleep infinity}
|
||||
volumes:
|
||||
- /sys/fs/cgroup:/sys/fs/cgroup:rw
|
||||
cgroupns_mode: host
|
||||
privileged: true
|
||||
pre_build_image: true
|
||||
pre_build_image: false
|
||||
|
||||
provisioner:
|
||||
name: ansible
|
||||
|
||||
@@ -4,13 +4,13 @@ driver:
|
||||
|
||||
platforms:
|
||||
- name: ${MOLECULE_PLATFORM_NAME:-ubuntu-2204}
|
||||
image: ${MOLECULE_PLATFORM_IMAGE:-geerlingguy/docker-ubuntu2204-ansible:latest}
|
||||
image: ${MOLECULE_PLATFORM_IMAGE:-ubuntu:22.04}
|
||||
command: ${MOLECULE_PLATFORM_COMMAND:-sleep infinity}
|
||||
volumes:
|
||||
- /sys/fs/cgroup:/sys/fs/cgroup:rw
|
||||
cgroupns_mode: host
|
||||
privileged: true
|
||||
pre_build_image: true
|
||||
pre_build_image: false
|
||||
|
||||
provisioner:
|
||||
name: ansible
|
||||
|
||||
@@ -4,13 +4,13 @@ driver:
|
||||
|
||||
platforms:
|
||||
- name: ${MOLECULE_PLATFORM_NAME:-ubuntu-2204}
|
||||
image: ${MOLECULE_PLATFORM_IMAGE:-geerlingguy/docker-ubuntu2204-ansible:latest}
|
||||
image: ${MOLECULE_PLATFORM_IMAGE:-ubuntu:22.04}
|
||||
command: ${MOLECULE_PLATFORM_COMMAND:-sleep infinity}
|
||||
volumes:
|
||||
- /sys/fs/cgroup:/sys/fs/cgroup:rw
|
||||
cgroupns_mode: host
|
||||
privileged: true
|
||||
pre_build_image: true
|
||||
pre_build_image: false
|
||||
|
||||
provisioner:
|
||||
name: ansible
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
- gnupg
|
||||
state: present
|
||||
update_cache: true
|
||||
cache_valid_time: 0
|
||||
|
||||
- name: Add Docker GPG key
|
||||
ansible.builtin.get_url:
|
||||
@@ -53,6 +54,7 @@
|
||||
- docker-compose-plugin
|
||||
state: present
|
||||
update_cache: true
|
||||
cache_valid_time: 0
|
||||
|
||||
- name: Install Docker (Arch Linux)
|
||||
when:
|
||||
|
||||
@@ -44,5 +44,12 @@
|
||||
changed_when: "'already exists' not in register_output.stdout | default('')"
|
||||
timeout: 60
|
||||
|
||||
- name: Restart gitea-runner service after registration
|
||||
ansible.builtin.systemd:
|
||||
name: "gitea-runner-docker@{{ runner_name }}"
|
||||
state: restarted
|
||||
daemon_reload: true
|
||||
when: systemd_available.stat.exists | default(false) | bool
|
||||
|
||||
- name: Include service setup
|
||||
ansible.builtin.include_tasks: service.yml
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
|
||||
- name: Verify runner container running (Docker mode)
|
||||
ansible.builtin.shell: |
|
||||
docker ps --filter "name=gitea-runner-{{ inventory_hostname }}" --format "{{ '{{.Status}}' }}"
|
||||
docker ps --filter "name=gitea-runner-{{ runner_name }}" --format "{{ '{{.Status}}' }}"
|
||||
register: docker_check
|
||||
changed_when: false
|
||||
when: runner_mode == 'docker'
|
||||
|
||||
@@ -24,3 +24,10 @@
|
||||
register: register_output
|
||||
changed_when: "'already exists' not in register_output.stdout | default('')"
|
||||
timeout: 60
|
||||
|
||||
- name: Restart gitea-runner service after registration
|
||||
ansible.builtin.systemd:
|
||||
name: "gitea-runner-binary@{{ runner_name }}"
|
||||
state: restarted
|
||||
daemon_reload: true
|
||||
when: systemd_available.stat.exists | default(false) | bool
|
||||
|
||||
@@ -3,6 +3,8 @@ log:
|
||||
|
||||
runner:
|
||||
file: "{{ gitea_runner_file }}"
|
||||
fetch_timeout: 50s
|
||||
fetch_interval: 2s
|
||||
|
||||
container:
|
||||
label: "{{ gitea_runner_container_label }}"
|
||||
|
||||
+2
-2
@@ -8,7 +8,7 @@ version = "0.1.0"
|
||||
description = "Lean CLI to manage Gitea Actions runners"
|
||||
readme = "README.md"
|
||||
license = {text = "GPL-3.0"}
|
||||
requires-python = ">=3.11"
|
||||
requires-python = ">=3.12"
|
||||
classifiers = [
|
||||
"Programming Language :: Python :: 3",
|
||||
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
|
||||
@@ -35,7 +35,7 @@ dev = [
|
||||
"bandit>=1.8.2",
|
||||
"pre-commit>=4.6.0",
|
||||
# Non-Python dev dependency: checkmake (Makefile linter)
|
||||
# Install via: go install github.com/mrtazz/checkmake/cmd/checkmake@latest
|
||||
# Install via: go install github.com/checkmake/checkmake/cmd/checkmake@latest
|
||||
]
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Distribute molecule scenarios across N parallel runners.
|
||||
|
||||
Discovers all molecule scenarios under ansible/roles/*/molecule/ and
|
||||
splits them evenly across the requested number of runners.
|
||||
|
||||
Usage:
|
||||
python3 scripts/distribute_molecule.py --runner-index 0 --max-runners 3
|
||||
# prints: default deregister
|
||||
python3 scripts/distribute_molecule.py --list
|
||||
# prints all scenarios, one per line
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import click
|
||||
|
||||
from gitea_runner_manager.i18n import _
|
||||
|
||||
DEFAULT_MAX_RUNNERS = 3
|
||||
MOLECULE_ROOT = Path("ansible/roles/gitea-runner/molecule")
|
||||
|
||||
|
||||
def discover_scenarios(root: Path | None = None) -> list[str]:
|
||||
"""Return sorted list of molecule scenario directory names."""
|
||||
if root is None:
|
||||
root = MOLECULE_ROOT
|
||||
if not root.is_dir():
|
||||
raise click.ClickException(
|
||||
_("Molecule directory not found: {path}", path=str(root))
|
||||
)
|
||||
scenarios = [
|
||||
d.name
|
||||
for d in root.iterdir()
|
||||
if d.is_dir() and not d.name.startswith("_") and d.name != "common"
|
||||
]
|
||||
return sorted(scenarios)
|
||||
|
||||
|
||||
def distribute(scenarios: list[str], max_runners: int) -> list[list[str]]:
|
||||
"""Split *scenarios* into *max_runners* balanced groups (round-robin)."""
|
||||
groups: list[list[str]] = [[] for _ in range(max_runners)]
|
||||
for i, scenario in enumerate(scenarios):
|
||||
groups[i % max_runners].append(scenario)
|
||||
return groups
|
||||
|
||||
|
||||
def scenarios_for_runner(
|
||||
scenarios: list[str], runner_index: int, max_runners: int
|
||||
) -> list[str]:
|
||||
"""Return the subset of scenarios assigned to *runner_index*."""
|
||||
groups = distribute(scenarios, max_runners)
|
||||
if runner_index < 0 or runner_index >= len(groups):
|
||||
raise click.ClickException(
|
||||
_(
|
||||
"Runner index {index} out of range (0..{max})",
|
||||
index=runner_index,
|
||||
max=max_runners - 1,
|
||||
)
|
||||
)
|
||||
return groups[runner_index]
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.option(
|
||||
"--runner-index",
|
||||
type=int,
|
||||
default=None,
|
||||
help="Zero-based runner index. If omitted, prints all groups.",
|
||||
)
|
||||
@click.option(
|
||||
"--max-runners",
|
||||
type=int,
|
||||
default=DEFAULT_MAX_RUNNERS,
|
||||
show_default=True,
|
||||
help="Total number of parallel runners.",
|
||||
)
|
||||
@click.option(
|
||||
"--list",
|
||||
"list_all",
|
||||
is_flag=True,
|
||||
help="List all discovered scenarios, one per line.",
|
||||
)
|
||||
def cli(runner_index: int | None, max_runners: int, list_all: bool) -> None:
|
||||
scenarios = discover_scenarios()
|
||||
if list_all:
|
||||
for s in scenarios:
|
||||
click.echo(s)
|
||||
return
|
||||
if runner_index is None:
|
||||
groups = distribute(scenarios, max_runners)
|
||||
for i, group in enumerate(groups):
|
||||
click.echo(f"Runner {i}: {' '.join(group) if group else '(none)'}")
|
||||
return
|
||||
assigned = scenarios_for_runner(scenarios, runner_index, max_runners)
|
||||
click.echo(" ".join(assigned))
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
cli() # pragma: no cover
|
||||
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Install checkmake if it is not already present.
|
||||
|
||||
Tries to install via Go if available, otherwise downloads the latest
|
||||
pre-built Linux binary from the official GitHub releases.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import platform
|
||||
import shutil
|
||||
import subprocess # nosec B404
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import urllib.request
|
||||
|
||||
import click
|
||||
|
||||
CHECKMAKE_VERSION = "0.3.2"
|
||||
RELEASE_URL_TEMPLATE = (
|
||||
"https://github.com/checkmake/checkmake/releases/download/"
|
||||
f"v{CHECKMAKE_VERSION}/checkmake-v{CHECKMAKE_VERSION}.linux.{{arch}}"
|
||||
)
|
||||
TARGET_PATH = Path("/usr/local/bin/checkmake")
|
||||
|
||||
|
||||
def _arch() -> str:
|
||||
"""Return the architecture string used by checkmake releases."""
|
||||
machine = platform.machine().lower()
|
||||
if machine in {"x86_64", "amd64"}:
|
||||
return "amd64"
|
||||
if machine in {"aarch64", "arm64"}:
|
||||
return "arm64"
|
||||
raise click.ClickException(
|
||||
f"Unsupported architecture: {machine}"
|
||||
)
|
||||
|
||||
|
||||
def _install_with_go() -> bool:
|
||||
"""Install checkmake using go install if Go is available."""
|
||||
go_bin = shutil.which("go")
|
||||
if go_bin is None:
|
||||
return False
|
||||
subprocess.run( # nosec B603
|
||||
[
|
||||
go_bin,
|
||||
"install",
|
||||
"github.com/checkmake/checkmake/cmd/checkmake@latest",
|
||||
],
|
||||
check=True,
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
def _download_binary() -> None:
|
||||
"""Download the prebuilt checkmake binary for the current architecture."""
|
||||
url = RELEASE_URL_TEMPLATE.format(arch=_arch())
|
||||
urllib.request.urlretrieve(url, TARGET_PATH) # nosec B310
|
||||
TARGET_PATH.chmod(0o755)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Install checkmake if not already present."""
|
||||
if shutil.which("checkmake") is not None:
|
||||
return
|
||||
|
||||
if not _install_with_go():
|
||||
_download_binary()
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
main() # pragma: no cover
|
||||
@@ -754,6 +754,20 @@ TRANSLATIONS: dict[str, dict[str, str]] = {
|
||||
"ru": "код выхода {code}",
|
||||
"zh": "退出代码 {code}",
|
||||
},
|
||||
"Molecule directory not found: {path}": {
|
||||
"en": "Molecule directory not found: {path}",
|
||||
"bg": "Директорията на molecule не е намерена: {path}",
|
||||
"de": "Molecule-Verzeichnis nicht gefunden: {path}",
|
||||
"ru": "Директория molecule не найдена: {path}",
|
||||
"zh": "未找到 molecule 目录: {path}",
|
||||
},
|
||||
"Runner index {index} out of range (0..{max})": {
|
||||
"en": "Runner index {index} out of range (0..{max})",
|
||||
"bg": "Индексът на runner {index} е извън диапазона (0..{max})",
|
||||
"de": "Runner-Index {index} außerhalb des Bereichs (0..{max})",
|
||||
"ru": "Индекс runner {index} вне диапазона (0..{max})",
|
||||
"zh": "Runner 索引 {index} 超出范围 (0..{max})",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
"""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,
|
||||
discover_scenarios,
|
||||
distribute,
|
||||
scenarios_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 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"]]
|
||||
|
||||
def test_uneven_split(self) -> None:
|
||||
scenarios = ["a", "b", "c", "d", "e"]
|
||||
groups = distribute(scenarios, 3)
|
||||
assert groups == [["a", "d"], ["b", "e"], ["c"]]
|
||||
|
||||
def test_more_runners_than_scenarios(self) -> None:
|
||||
scenarios = ["a", "b"]
|
||||
groups = distribute(scenarios, 5)
|
||||
assert groups == [["a"], ["b"], [], [], []]
|
||||
|
||||
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:
|
||||
groups = distribute([], 3)
|
||||
assert groups == [[], [], []]
|
||||
|
||||
|
||||
class TestScenariosForRunner:
|
||||
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"]
|
||||
|
||||
def test_out_of_range_raises(self) -> None:
|
||||
with pytest.raises(click.ClickException) as exc:
|
||||
scenarios_for_runner(["a"], 5, 3)
|
||||
assert "out of range" in str(exc.value)
|
||||
|
||||
def test_negative_index_raises(self) -> None:
|
||||
with pytest.raises(click.ClickException) as exc:
|
||||
scenarios_for_runner(["a"], -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_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"
|
||||
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, ["--runner-index", "1", "--max-runners", "3"])
|
||||
assert result.exit_code == 0
|
||||
assert result.output.strip() == "b"
|
||||
|
||||
|
||||
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"])
|
||||
@@ -111,12 +111,9 @@ class TestAnsibleExecutorExtractStatus:
|
||||
executor = AnsibleExecutor()
|
||||
log_file = tmp_path / "test.log"
|
||||
log_file.write_text("incomplete")
|
||||
log_file.chmod(0o000)
|
||||
try:
|
||||
with patch("gitea_runner_manager.executor.open", side_effect=OSError("read error")):
|
||||
status = executor._extract_status(log_file)
|
||||
assert status is None
|
||||
finally:
|
||||
log_file.chmod(0o644)
|
||||
|
||||
|
||||
class TestAnsibleExecutorPrepareLog:
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import platform
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from click import ClickException
|
||||
|
||||
import scripts.install_checkmake as install_checkmake
|
||||
|
||||
|
||||
class TestArch:
|
||||
def test_amd64(self) -> None:
|
||||
with patch.object(platform, "machine", return_value="x86_64"):
|
||||
assert install_checkmake._arch() == "amd64"
|
||||
|
||||
def test_arm64(self) -> None:
|
||||
with patch.object(platform, "machine", return_value="aarch64"):
|
||||
assert install_checkmake._arch() == "arm64"
|
||||
|
||||
def test_unsupported(self) -> None:
|
||||
with patch.object(platform, "machine", return_value="riscv64"):
|
||||
with pytest.raises(ClickException):
|
||||
install_checkmake._arch()
|
||||
|
||||
|
||||
class TestInstallWithGo:
|
||||
def test_no_go(self) -> None:
|
||||
with patch("shutil.which", return_value=None):
|
||||
assert install_checkmake._install_with_go() is False
|
||||
|
||||
def test_with_go(self) -> None:
|
||||
with patch("shutil.which", return_value="/usr/bin/go"):
|
||||
with patch("subprocess.run") as mock_run:
|
||||
assert install_checkmake._install_with_go() is True
|
||||
mock_run.assert_called_once_with(
|
||||
[
|
||||
"/usr/bin/go",
|
||||
"install",
|
||||
"github.com/checkmake/checkmake/cmd/checkmake@latest",
|
||||
],
|
||||
check=True,
|
||||
)
|
||||
|
||||
|
||||
class TestDownloadBinary:
|
||||
def test_download(self, tmp_path: Path) -> None:
|
||||
target = tmp_path / "checkmake"
|
||||
|
||||
def _write_file(url: str, path: str) -> tuple[str, None]:
|
||||
Path(path).write_bytes(b"binary")
|
||||
return path, None
|
||||
|
||||
with patch.object(install_checkmake, "TARGET_PATH", target):
|
||||
with patch.object(platform, "machine", return_value="x86_64"):
|
||||
with patch("urllib.request.urlretrieve", side_effect=_write_file) as mock_retrieve:
|
||||
install_checkmake._download_binary()
|
||||
mock_retrieve.assert_called_once()
|
||||
assert target.exists()
|
||||
assert target.stat().st_mode & 0o111
|
||||
|
||||
|
||||
class TestMain:
|
||||
def test_already_installed(self) -> None:
|
||||
with patch("shutil.which", return_value="/usr/bin/checkmake"):
|
||||
install_checkmake.main()
|
||||
|
||||
def test_install_with_go(self) -> None:
|
||||
with patch("shutil.which", side_effect=[None, "/usr/bin/go"]):
|
||||
with patch("subprocess.run") as mock_run:
|
||||
install_checkmake.main()
|
||||
mock_run.assert_called_once_with(
|
||||
[
|
||||
"/usr/bin/go",
|
||||
"install",
|
||||
"github.com/checkmake/checkmake/cmd/checkmake@latest",
|
||||
],
|
||||
check=True,
|
||||
)
|
||||
|
||||
def test_download_when_no_go(self, tmp_path: Path) -> None:
|
||||
target = tmp_path / "checkmake"
|
||||
|
||||
def _write_file(url: str, path: str) -> tuple[str, None]:
|
||||
Path(path).write_bytes(b"binary")
|
||||
return path, None
|
||||
|
||||
with patch.object(install_checkmake, "TARGET_PATH", target):
|
||||
with patch("shutil.which", side_effect=[None, None]):
|
||||
with patch.object(platform, "machine", return_value="x86_64"):
|
||||
with patch("urllib.request.urlretrieve", side_effect=_write_file) as mock_retrieve:
|
||||
install_checkmake.main()
|
||||
mock_retrieve.assert_called_once()
|
||||
Reference in New Issue
Block a user