Files
computer-playbook/tests/unit/python/cli/administration/deploy/ci/test_selections.py
T
kevinveenbirkenbachandClaude Opus 5 6d191555c3 feat(ci-trigger): retrigger by role, and by what never got a verdict
Two switches and an alias for the case f4c5a7cc36 was not written for: a run
where most of the ranking is red and much of it never ran.

--roles-only collapses the failed selections to their role ids. f4c5a7cc36
replaced exactly that form with pinned tokens, and its reason holds: the
rotation may not draw the combination that broke. It holds for a handful of red
rows. At thirty roles the question stops being which combination broke and
becomes whether the role comes up at all, and a token per combination spends
the priority budget proving the same role red twice. The default is unchanged;
this is the trade, named, behind a flag.

--include-unrun puts the rows the source run holds no job for on the priority
line, with their axes. resume_offset covers a run that walked a contiguous
prefix; it stops at the first gap, and a run whose priority line consumed the
budget before the head of the ranking leaves a hole at row one. Then the offset
is empty, the retrigger starts over, and the rows nobody judged queue behind
the ones already settled.

With --strict the two agree on what settles a row. Only a success or a failure
does. A cancelled row, or one still running when the run was read, was never
given the chance to be either, so it comes back as itself rather than as a role
name that would let the rotation draw some other combination instead. On the
run this was written against that moves 24 rows: 123 started, 99 judged.

Membership is decided on role#variant. Mode, onion state and distro are
assigned by the sweep rotation, so comparing full tokens between a source run
and a freshly computed ranking reports almost every row as unrun -- measured,
266 of 269, against 177 once the identity is right. Each ranking row is a
distinct role#variant, which is what makes it the identity to compare on.

i8ciall carries the combination, next to i8cifail which reproduces the exact
red rows and i8cifo which takes only the hard failures.

resume_offset and the new code now build their tokens through one helper
rather than two copies of the same five lines.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-30 16:14:25 +02:00

194 lines
7.5 KiB
Python

from __future__ import annotations
import unittest
from typing import ClassVar
from cli.administration.deploy.ci import selections
from tests.utils.ci.job_names import deploy_job_name
from utils.github.variant import selection
from utils.github.variant.pools import DISTROS, FILESYSTEMS
def _job(name: str, conclusion: str | None, status: str = "completed") -> dict:
return {"name": name, "status": status, "conclusion": conclusion}
def _row(app: str, variant: str, mode: str, tor: bool = False) -> dict:
return {
"apps": app,
"variant": variant,
"mode": mode,
"tor": "true" if tor else "false",
"distro": DISTROS[0],
"filesystem": FILESYSTEMS[0],
"priority": "false",
}
_AXES = f"%{DISTROS[0]}"
class TestResumeOffset(unittest.TestCase):
"""Where a retrigger picks the regular line up again."""
_REGULAR: ClassVar[list[dict]] = [
_row("web-app-a", "0", "compose"),
_row("web-app-b", "1", "swarm", tor=True),
_row("web-app-c", "0", "host"),
]
def test_a_run_that_deployed_nothing_starts_at_the_head(self) -> None:
self.assertEqual(selections.resume_offset(self._REGULAR, set()), "")
def test_the_offset_is_the_last_row_of_the_leading_run(self) -> None:
deployed = {
"web-app-a#0@compose+clearnet" + _AXES,
"web-app-b#1@swarm+tor" + _AXES,
}
self.assertEqual(
selections.resume_offset(self._REGULAR, deployed), "web-app-b#1"
)
def test_the_offset_carries_no_axes_so_it_survives_a_new_sweep(self) -> None:
"""Every axis rotates with the sweep number and a retrigger gets a
fresh one, so an axis-pinned offset would resolve only in the sweep it
was computed at and abort every chunk's discovery in the others."""
deployed = {"web-app-a#0@compose+clearnet" + _AXES}
offset = selections.resume_offset(self._REGULAR, deployed)
self.assertNotIn("@", offset)
self.assertNotIn("+", offset)
self.assertNotIn("%", offset)
self.assertNotIn("/", offset)
def test_a_gap_stops_the_scan_rather_than_skipping_past_it(self) -> None:
deployed = {
"web-app-a#0@compose+clearnet" + _AXES,
"web-app-c#0@host+clearnet" + _AXES,
}
self.assertEqual(
selections.resume_offset(self._REGULAR, deployed), "web-app-a#0"
)
def test_a_run_that_covered_everything_resumes_at_the_last_row(self) -> None:
deployed = {
"web-app-a#0@compose+clearnet" + _AXES,
"web-app-b#1@swarm+tor" + _AXES,
"web-app-c#0@host+clearnet" + _AXES,
}
self.assertEqual(
selections.resume_offset(self._REGULAR, deployed), "web-app-c#0"
)
def test_the_verdict_does_not_matter_only_that_the_row_ran(self) -> None:
jobs = [
_job(deploy_job_name("swarm", "web-app-b", "1", tor=True), "failure"),
_job(deploy_job_name("docker", "web-app-a", "0"), "success"),
]
self.assertEqual(
selections.deployed_selections(jobs),
{"web-app-a#0@compose+clearnet" + _AXES, "web-app-b#1@swarm+tor" + _AXES},
)
class TestUnrunSelections(unittest.TestCase):
"""The rows the source run never reached."""
_REGULAR: ClassVar[list[dict]] = [
_row("web-app-a", "0", "compose"),
_row("web-app-b", "1", "swarm", tor=True),
_row("web-app-c", "0", "host"),
]
def test_a_row_with_no_job_comes_back_with_its_axes(self) -> None:
deployed = {"web-app-a#0@compose+clearnet" + _AXES}
self.assertEqual(
selections.unrun_selections(self._REGULAR, deployed),
["web-app-b#1@swarm+tor" + _AXES, "web-app-c#0@host+clearnet" + _AXES],
)
def test_a_row_deployed_under_other_axes_counts_as_run(self) -> None:
"""Mode, tor and distro rotate; comparing them calls every row unrun."""
deployed = {f"web-app-b#1@compose+clearnet%{DISTROS[1]}"}
self.assertNotIn(
"web-app-b#1@swarm+tor" + _AXES,
selections.unrun_selections(self._REGULAR, deployed),
)
def test_run_and_unrun_partition_the_ranking(self) -> None:
deployed = {"web-app-a#0@compose+clearnet" + _AXES}
unrun = selections.unrun_selections(self._REGULAR, deployed)
self.assertEqual(len(unrun) + 1, len(self._REGULAR))
def test_a_run_that_deployed_nothing_owes_every_row(self) -> None:
self.assertEqual(
len(selections.unrun_selections(self._REGULAR, set())), len(self._REGULAR)
)
class TestSettledSelections(unittest.TestCase):
"""Only a green or a red row retires a combination."""
def test_a_success_and_a_failure_both_settle(self) -> None:
jobs = [
_job(deploy_job_name("docker", "web-app-a", "0"), "success"),
_job(deploy_job_name("swarm", "web-app-b", "1", tor=True), "failure"),
]
self.assertEqual(
selections.settled_selections(jobs),
{"web-app-a#0@compose+clearnet" + _AXES, "web-app-b#1@swarm+tor" + _AXES},
)
def test_a_cancelled_row_settles_nothing(self) -> None:
jobs = [_job(deploy_job_name("docker", "web-app-a", "0"), "cancelled")]
self.assertEqual(selections.settled_selections(jobs), set())
self.assertEqual(len(selections.deployed_selections(jobs)), 1)
def test_a_still_running_row_settles_nothing(self) -> None:
jobs = [_job(deploy_job_name("docker", "web-app-a", "0"), None, "in_progress")]
self.assertEqual(selections.settled_selections(jobs), set())
def test_an_aborted_row_comes_back_as_unrun(self) -> None:
"""It was never judged, so the combination is still owed an attempt."""
regular = [_row("web-app-a", "0", "compose")]
jobs = [_job(deploy_job_name("docker", "web-app-a", "0"), "cancelled")]
self.assertEqual(
selections.unrun_selections(regular, selections.settled_selections(jobs)),
["web-app-a#0@compose+clearnet" + _AXES],
)
self.assertEqual(
selections.unrun_selections(regular, selections.deployed_selections(jobs)),
[],
)
class TestCollapseToRoles(unittest.TestCase):
"""Trading the exact red combination for covering the role."""
def test_the_combinations_of_one_role_become_one_entry(self) -> None:
collapsed = selections.collapse_to_roles(
[
"web-app-a#0@compose+clearnet" + _AXES,
"web-app-a#1@swarm+tor" + _AXES,
"web-app-b#0@swarm+clearnet" + _AXES,
]
)
self.assertEqual(collapsed, ["web-app-a", "web-app-b"])
def test_nothing_stays_pinned(self) -> None:
"""A leftover axis would pin the rotation to a combination nobody chose."""
for token in selections.collapse_to_roles(
["web-app-a#2@swarm+tor" + _AXES, "web-app-b#0@compose+clearnet" + _AXES]
):
with self.subTest(token=token):
self.assertFalse(selection.parse(token).pinned)
def test_no_role_is_dropped(self) -> None:
tokens = [
f"web-app-{name}#0@compose+clearnet{_AXES}" for name in ("a", "b", "c")
]
self.assertEqual(len(selections.collapse_to_roles(tokens)), len(tokens))
def test_an_already_bare_role_survives_unchanged(self) -> None:
"""An untriggered priority entry may already carry no axes."""
self.assertEqual(selections.collapse_to_roles(["web-app-a"]), ["web-app-a"])