Files
computer-playbook/tests/unit/python/cli/meta/ci/test_validate.py
T
kevinveenbirkenbachandClaude Opus 5.5 098b0caffc fix(ci): offer multi to bond-less rows whose deploy pulls Tor in
A role without a Tor bond only took clearnet rows, and clearnet rows disable
Tor. svc-ai-litellm (variants 0 and 2) and web-app-seaweedfs (variant 0) still
pull the Tor provider in through their dependencies, so they ran on a node
with Tor about half the time under the tor axis and never under the network
axis.

row_states now gives such a row clearnet and multi. Whether the deploy pulls
the provider in is read from the services closure the discovery row already
carries, which matched the deploy's own include resolution on all 33 rows
without a bond. A row whose deploy contains no Tor provider stays clearnet
only, because NETWORK_MODE=multi without the provider fails the deploy, and a
gate pinned false still means clearnet. The matrix and the selection
validator pass the same flag, so a +multi pin agrees with the matrix.
tor_capable lost its last caller and is gone.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-24 22:42:55 +02:00

203 lines
7.6 KiB
Python

from __future__ import annotations
import io
import unittest
import unittest.mock as mock
from contextlib import redirect_stderr, redirect_stdout
from cli.meta.ci import validate
from utils.github.variant import axes, network
_ROWS = [
{"name": "web-app-a", "variant": 0, "test_compose": True, "test_swarm": True},
{"name": "web-app-a", "variant": 1, "test_compose": True, "test_swarm": False},
{"name": "web-app-b", "variant": 0, "test_compose": False, "test_swarm": True},
]
_VARIANTS = {
"web-app-a": [
{"services": {"tor": {"enabled": True}}},
{"services": {"tor": {"enabled": False}}},
],
"web-app-b": [
{
"services": {"tor": {"enabled": True}},
"networks": {"reachability": {"single_mode": True}},
}
],
}
def _problems(
tokens: str, *, network_input: str = "auto"
) -> tuple[list[str], list[str]]:
with (
mock.patch.object(validate.query, "discover_rows", return_value=_ROWS),
mock.patch.object(validate, "get_variants", return_value=_VARIANTS),
):
return validate.problems(
tokens,
modes=("compose", "swarm", "host"),
network_input=network_input,
distros=axes.DISTROS,
filesystems=axes.FILESYSTEMS,
lifecycles="",
label="priority",
)
class TestProblems(unittest.TestCase):
def test_nothing_to_check_is_not_a_problem(self) -> None:
self.assertEqual(_problems(""), ([], []))
def test_a_token_the_branch_can_deploy_passes(self) -> None:
self.assertEqual(_problems("web-app-a#0@swarm+tor"), ([], []))
def test_a_variant_the_role_no_longer_declares_is_an_error(self) -> None:
errors, warnings = _problems("web-app-a#7@compose+tor")
self.assertEqual(warnings, [])
self.assertIn("matches no discovered row", errors[0])
self.assertIn("2 variant(s)", errors[0])
def test_a_bare_role_that_matches_nothing_only_warns(self) -> None:
errors, warnings = _problems("web-app-gone")
self.assertEqual(errors, [])
self.assertIn("web-app-gone", warnings[0])
def test_a_pinned_role_the_run_never_discovers_only_warns(self) -> None:
errors, warnings = _problems("web-app-gone#0,2")
self.assertEqual(errors, [])
self.assertIn("web-app-gone", warnings[0])
def test_a_bare_role_the_run_discovers_is_silent(self) -> None:
self.assertEqual(_problems("web-app-a"), ([], []))
def test_a_bare_role_is_judged_on_existence_not_on_variant_zero(self) -> None:
rows = [{"name": "web-app-c", "variant": 4, "test_compose": True}]
with (
mock.patch.object(validate.query, "discover_rows", return_value=rows),
mock.patch.object(
validate, "get_variants", return_value={"web-app-c": [{"services": {}}]}
),
):
errors, warnings = validate.problems(
"web-app-c",
modes=("compose",),
network_input="auto",
distros=axes.DISTROS,
filesystems=axes.FILESYSTEMS,
lifecycles="",
label="priority",
)
self.assertEqual((errors, warnings), ([], []))
def test_a_role_without_a_bond_may_pin_multi_when_its_deploy_pulls_tor(
self,
) -> None:
rows = [
{
"name": "web-app-c",
"variant": 0,
"test_compose": True,
"services": [network.tor_provider()],
}
]
with (
mock.patch.object(validate.query, "discover_rows", return_value=rows),
mock.patch.object(
validate, "get_variants", return_value={"web-app-c": [{"services": {}}]}
),
):
problems = validate.problems(
"web-app-c#0@compose+multi",
modes=("compose",),
network_input="auto",
distros=axes.DISTROS,
filesystems=axes.FILESYSTEMS,
lifecycles="",
label="priority",
)
self.assertEqual(problems, ([], []))
def test_a_warning_still_separates_a_missing_role_from_a_present_one(self) -> None:
_errors, warnings = _problems("web-app-a web-app-gone web-app-b")
self.assertEqual(len(warnings), 1)
self.assertIn("web-app-gone", warnings[0])
def test_a_mode_the_row_does_not_offer_is_an_error(self) -> None:
errors, _warnings = _problems("web-app-a#1@swarm+clearnet")
self.assertIn("pinned mode 'swarm' is not available", errors[0])
def test_a_network_mode_the_variant_rules_out_is_an_error(self) -> None:
errors, _warnings = _problems("web-app-a#1@compose+tor")
self.assertIn("pinned network mode 'tor' is impossible", errors[0])
def test_a_network_mode_the_runs_network_axis_rules_out_is_an_error(self) -> None:
errors, _warnings = _problems(
"web-app-a#0@compose+clearnet", network_input="tor"
)
self.assertIn("pinned network mode 'clearnet' is impossible", errors[0])
def test_a_network_mode_the_roles_reachability_rules_out_is_an_error(self) -> None:
self.assertEqual(_problems("web-app-b#0@swarm+tor"), ([], []))
errors, _warnings = _problems("web-app-b#0@swarm+multi")
self.assertIn("pinned network mode 'multi' is impossible", errors[0])
def test_every_offender_is_reported_not_just_the_first(self) -> None:
errors, _warnings = _problems(
"web-app-a#7@compose+tor web-app-a#1@swarm+tor web-app-b#9@swarm+tor"
)
self.assertEqual(len(errors), 3)
def test_a_csv_token_is_checked_variant_by_variant(self) -> None:
errors, _warnings = _problems("web-app-a#0,7@compose+clearnet")
self.assertEqual(len(errors), 1)
self.assertIn("matches no discovered row", errors[0])
class TestMain(unittest.TestCase):
def _main(self, argv: list[str]) -> tuple[int, str, str]:
out, err = io.StringIO(), io.StringIO()
with (
mock.patch.object(validate.query, "discover_rows", return_value=_ROWS),
mock.patch.object(validate, "get_variants", return_value=_VARIANTS),
redirect_stdout(out),
redirect_stderr(err),
):
code = validate.main(argv)
return code, out.getvalue(), err.getvalue()
def test_a_valid_selection_exits_zero(self) -> None:
code, out, _err = self._main(["--priority", "web-app-a#0@swarm+tor"])
self.assertEqual(code, 0)
self.assertIn("valid for this branch", out)
def test_an_unusable_selection_exits_non_zero(self) -> None:
code, _out, err = self._main(["--priority", "web-app-a#7"])
self.assertEqual(code, 1)
self.assertIn("::error::", err)
def test_the_network_flag_is_the_runs_network_axis(self) -> None:
code, _out, err = self._main(
["--network", "clearnet", "--priority", "web-app-a#0@swarm+tor"]
)
self.assertEqual(code, 1)
self.assertIn("pinned network mode 'tor' is impossible", err)
def test_both_inputs_are_checked_and_named_in_the_message(self) -> None:
code, _out, err = self._main(
["--whitelist", "web-app-a#7", "--priority", "web-app-b#9"]
)
self.assertEqual(code, 1)
self.assertIn("whitelist:", err)
self.assertIn("priority:", err)
def test_a_warning_alone_does_not_fail_the_run(self) -> None:
code, out, _err = self._main(["--whitelist", "web-app-gone"])
self.assertEqual(code, 0)
self.assertIn("::warning::", out)
if __name__ == "__main__":
unittest.main()