180 lines
5.8 KiB
Python
180 lines
5.8 KiB
Python
from __future__ import annotations
|
|
|
|
import importlib.util
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
POLICY_PATH = ROOT / "ops" / "gitea-deploy-broker" / "runner_admission_policy.py"
|
|
PROFILES_PATH = ROOT / "ops" / "gitea-deploy-broker" / "runner_profiles.py"
|
|
|
|
|
|
def _load(name: str, path: Path):
|
|
spec = importlib.util.spec_from_file_location(name, path)
|
|
assert spec and spec.loader
|
|
module = importlib.util.module_from_spec(spec)
|
|
sys.modules[spec.name] = module
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
@pytest.fixture
|
|
def policy():
|
|
profiles = _load("appops_runner_profiles_for_admission_test", PROFILES_PATH)
|
|
sys.modules["runner_profiles"] = profiles
|
|
return _load("appops_runner_admission_policy_test", POLICY_PATH)
|
|
|
|
|
|
def _job(job_id: str, labels: list[str], queued_at: float):
|
|
return {"id": job_id, "labels": labels, "queued_at": queued_at}
|
|
|
|
|
|
def test_oldest_compatible_job_wins_and_profile_choice_rotates(policy):
|
|
jobs = [
|
|
_job("newer-security", ["itworx-ci-security"], 200.0),
|
|
_job("older-linux", ["self-hosted", "linux-validation"], 100.0),
|
|
]
|
|
|
|
first = policy.select_admission(
|
|
jobs,
|
|
available_profiles=["backend", "frontend", "security"],
|
|
active_claims=[],
|
|
cursor=0,
|
|
)
|
|
second = policy.select_admission(
|
|
jobs,
|
|
available_profiles=["backend", "frontend", "security"],
|
|
active_claims=[],
|
|
cursor=first.next_cursor,
|
|
)
|
|
|
|
assert (first.job_id, first.profile_id) == ("older-linux", "backend")
|
|
assert (second.job_id, second.profile_id) == ("older-linux", "frontend")
|
|
|
|
|
|
def test_enabled_legacy_slots_count_as_claims_and_can_consume_all_capacity(policy):
|
|
decision = policy.select_admission(
|
|
[_job("checks", ["itworx-ci-checks"], 1.0)],
|
|
available_profiles=["checks"],
|
|
# Two old broad Tower registrations are charged as two backend-envelope slots.
|
|
active_claims=["backend", "backend"],
|
|
)
|
|
|
|
assert decision.profile_id is None
|
|
assert decision.reason == "runner_admission_claim_limit"
|
|
|
|
|
|
def test_duplicate_legacy_claims_are_charged_against_dind_and_family_budgets(policy):
|
|
dind_limited = policy.validate_claims(["frontend", "backend"])
|
|
family_excess = policy.validate_claims(["build", "backend"])
|
|
|
|
assert dind_limited.ok
|
|
assert family_excess.reason == "runner_admission_dind_budget_exceeded"
|
|
|
|
|
|
def test_two_heavy_profiles_cannot_be_admitted_together(policy):
|
|
result = policy.validate_claims(["browser", "build"])
|
|
|
|
assert not result.ok
|
|
assert result.reason == "runner_admission_heavy_claim_limit"
|
|
|
|
|
|
def test_unmapped_enabled_registration_fails_closed(policy):
|
|
result = policy.validate_claims(["unknown-legacy-runner"])
|
|
|
|
assert not result.ok
|
|
assert result.reason == "runner_admission_profile_unmapped"
|
|
|
|
|
|
def test_candidate_is_reserved_before_it_can_be_selected(policy):
|
|
result = policy.select_admission(
|
|
[_job("build", ["itworx-ci-build"], 1.0)],
|
|
available_profiles=["build"],
|
|
active_claims=["browser"],
|
|
)
|
|
|
|
assert result.profile_id is None
|
|
assert result.reason == "runner_admission_heavy_claim_limit"
|
|
|
|
|
|
def test_unknown_required_job_label_does_not_fall_back_to_a_broad_profile(policy):
|
|
result = policy.select_admission(
|
|
[_job("mystery", ["linux-validation", "unraid-deploy"], 1.0)],
|
|
available_profiles=["backend", "frontend"],
|
|
active_claims=[],
|
|
)
|
|
|
|
assert result.profile_id is None
|
|
assert result.reason == "runner_admission_no_compatible_queue"
|
|
|
|
|
|
def test_legacy_docker_alias_keeps_heavy_reservation_and_budget_gates(policy):
|
|
selected = policy.select_admission(
|
|
[_job("docker", ["docker-validation"], 1.0)],
|
|
available_profiles=["build"], active_claims=[],
|
|
)
|
|
assert selected.profile_id == "build"
|
|
blocked = policy.select_admission(
|
|
[_job("docker", ["docker-validation"], 1.0)],
|
|
available_profiles=["build"], active_claims=["browser"],
|
|
)
|
|
assert blocked.profile_id is None
|
|
assert blocked.reason == "runner_admission_heavy_claim_limit"
|
|
|
|
|
|
def test_malformed_queue_row_fails_closed(policy):
|
|
with pytest.raises(policy.RunnerAdmissionPolicyError):
|
|
policy.select_admission(
|
|
[{"id": "bad", "labels": ["itworx-ci-checks"], "queued_at": "now"}],
|
|
available_profiles=["checks"],
|
|
active_claims=[],
|
|
)
|
|
|
|
|
|
def test_lease_yields_to_the_oldest_eligible_other_profile_without_stealing_same_profile_work(
|
|
policy,
|
|
):
|
|
queue = [
|
|
_job("older-build", ["itworx-ci-build"], 100.0),
|
|
_job("newer-backend", ["linux-validation"], 200.0),
|
|
]
|
|
decision = policy.select_lease_yield(
|
|
queue,
|
|
leased_profile="backend",
|
|
available_profiles=["checks", "security", "browser", "build"],
|
|
active_claims=["backend", "frontend"],
|
|
cursor=0,
|
|
)
|
|
|
|
assert (decision.profile_id, decision.job_id) == ("build", "older-build")
|
|
|
|
same_profile_is_oldest = policy.select_lease_yield(
|
|
[
|
|
_job("older-backend", ["linux-validation"], 100.0),
|
|
_job("newer-build", ["itworx-ci-build"], 200.0),
|
|
],
|
|
leased_profile="backend",
|
|
available_profiles=["checks", "security", "browser", "build"],
|
|
active_claims=["backend", "frontend"],
|
|
cursor=0,
|
|
)
|
|
|
|
assert same_profile_is_oldest.profile_id is None
|
|
assert same_profile_is_oldest.reason == "runner_admission_lease_retained"
|
|
|
|
|
|
def test_lease_does_not_yield_when_current_claims_can_admit_queued_work(policy):
|
|
decision = policy.select_lease_yield(
|
|
[_job("checks", ["itworx-ci-checks"], 100.0)],
|
|
leased_profile="backend",
|
|
available_profiles=["checks"],
|
|
active_claims=["backend"],
|
|
cursor=0,
|
|
)
|
|
|
|
assert decision.profile_id is None
|
|
assert decision.reason == "runner_admission_lease_not_needed"
|