mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-10 12:47:04 +00:00
Extends ``[tool.ruff.lint].select`` with six additional rule families:
``RUF`` (ruff-specific), ``SIM`` (flake8-simplify), ``C4``
(flake8-comprehensions), ``PERF`` (perflint), ``RET`` (flake8-return),
``PIE`` (flake8-pie). Adds ``RUF001/002/003`` to ``ignore`` because
those exist for homoglyph-attack detection, not English typography —
the project uses em/en-dashes and ``ℹ`` as a UI-hint glyph in CLI help
output deliberately.
Auto-fixed (~225, mechanical & behavior-preserving)
====================================================
- RUF005: ``a + [b]`` → ``[*a, b]``
- C408: ``dict(k=v)`` → ``{"k": v}``
- C420: ``dict.fromkeys(seq)`` shape simplifications
- SIM103/SIM110/SIM118: truthy-return / dict-key / dict-method
idiom simplifications
- RET504/RET505: drop unneeded final assigns / ``else`` after
``return``
- PIE810: combine ``str.startswith(a) or .startswith(b)``
into one call with a tuple
- RUF010: ``f"{str(x)}"`` → ``f"{x\!s}"``
- RUF100: removed unused ``# noqa`` directives that
became stale (mostly old ``# noqa: E402``
markers that ruff's smart E402 implementation
no longer flags after ``sys.path`` shims)
- RUF015 / RUF013 / RUF022 / RUF046 / RUF059: misc small wins
- I001: re-sorted import blocks where the new
imports above shifted them
Hand-fixed (~140, scripted where mechanical)
============================================
PERF401 (42): scripted AST rewrite of
``for x in iter: target.append(expr)`` (with optional ``if`` guard)
→ ``target.extend(expr for x in iter [if cond])``.
``list.extend(genexp)`` consumes the generator immediately, so the
rewrite is semantically equivalent to the original eager loop.
SIM117 (41): scripted AST combiner for nested ``with`` blocks where
the inner is the only statement of the outer (38 sites). The remaining
3 multi-line ``patch.object(...)`` chains were combined by hand using
the parenthesised ``with (\\n A,\\n B,\\n):`` form.
SIM102 (14): scripted AST combiner for nested ``if`` (10 sites). The
remaining 4 carry an inline ``# noqa: SIM102`` with rationale —
combining would either change semantics (the outer body has siblings
beyond the inner ``if``, e.g. ``cli/encrypt/inventory/__main__.py``
where ``encrypted_value = …`` runs regardless of ``ask_confirmation``)
or hurt readability (long literal whitelists).
RUF012 (9): annotated mutable class attributes with ``ClassVar[…]``
and added ``from typing import ClassVar``.
SIM105 (6): ``try/except: pass`` → ``with contextlib.suppress(…):``
with the necessary ``import contextlib`` added. Verified on
``utils/templating.py`` (3 occurrences in a ``finally`` block) —
cleanup behaviour is identical.
SIM115 (4): wrapped ``tempfile.NamedTemporaryFile(..., delete=False)``
call patterns in a ``with`` block; ``delete=False`` keeps the file
on disk after the block, so callers' ``return f.name`` still works.
PERF402, PERF403, B007, SIM108, SIM103: 5 misc fixes (loop→list-copy,
loop→dict-comprehension, unused-loop-var rename, ternary, return
expression).
Verification
============
- ``ruff check .`` → All checks passed
- ``ruff format --check .`` → 894 files already formatted
- 1621 unit tests → all pass
- Integration suite: 44 baseline failures, 1 skipped (unchanged)
- Sample-audited PERF401 rewrites in ``resolver.py``, ``inventory.py``,
``cache/files.py``, ``ressources/__main__.py``; SIM117 in
``test_main.py``; SIM105 in ``utils/templating.py``; SIM102 noqa
rationales in ``cli/encrypt/inventory/__main__.py``.
182 files changed, +993 / −960. With this and the prior strict-rule
commit the branch satisfies 11 ruff rule families end-to-end.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
598 lines
20 KiB
Python
598 lines
20 KiB
Python
"""Unit tests for the development inventory build SPOT
|
|
(`cli.deploy.development.inventory`)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import unittest
|
|
from pathlib import Path
|
|
from typing import ClassVar
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
from cli.deploy.development.common import DEV_INVENTORY_VARS_FILE
|
|
from cli.deploy.development.inventory import (
|
|
DevInventorySpec,
|
|
_bake_overrides,
|
|
_resolve_variant_payloads,
|
|
build_dev_inventory,
|
|
build_dev_inventory_matrix,
|
|
filter_plan_to_variant,
|
|
plan_dev_inventory_matrix,
|
|
)
|
|
|
|
|
|
def _spec(**overrides) -> DevInventorySpec:
|
|
base = {
|
|
"inventory_dir": "/tmp/inv",
|
|
"include": ("web-app-keycloak", "web-app-nextcloud"),
|
|
"storage_constrained": False,
|
|
"runtime": "dev",
|
|
}
|
|
base.update(overrides)
|
|
return DevInventorySpec(**base)
|
|
|
|
|
|
class TestDevInventorySpec(unittest.TestCase):
|
|
def test_empty_include_rejected(self):
|
|
with self.assertRaises(ValueError):
|
|
DevInventorySpec(
|
|
inventory_dir="/tmp/inv",
|
|
include=(),
|
|
storage_constrained=False,
|
|
runtime="dev",
|
|
)
|
|
|
|
def test_list_include_normalised_to_tuple(self):
|
|
spec = DevInventorySpec(
|
|
inventory_dir="/tmp/inv",
|
|
include=["web-app-keycloak"],
|
|
storage_constrained=True,
|
|
runtime="dev",
|
|
)
|
|
self.assertIsInstance(spec.include, tuple)
|
|
self.assertEqual(spec.include, ("web-app-keycloak",))
|
|
|
|
def test_overrides_default(self):
|
|
spec = _spec(storage_constrained=True, runtime="prod")
|
|
self.assertEqual(
|
|
spec.overrides(),
|
|
{"STORAGE_CONSTRAINED": True, "RUNTIME": "prod"},
|
|
)
|
|
|
|
def test_extra_vars_win_over_implicit_overrides(self):
|
|
spec = _spec(
|
|
storage_constrained=False,
|
|
runtime="dev",
|
|
extra_vars={"STORAGE_CONSTRAINED": True, "FOO": "bar"},
|
|
)
|
|
self.assertEqual(
|
|
spec.overrides(),
|
|
{"STORAGE_CONSTRAINED": True, "RUNTIME": "dev", "FOO": "bar"},
|
|
)
|
|
|
|
def test_inventory_root_strips_trailing_slash(self):
|
|
spec = _spec(inventory_dir="/tmp/inv///")
|
|
self.assertEqual(spec.inventory_root(), "/tmp/inv")
|
|
|
|
def test_variant_selectors_default_to_empty_dict(self):
|
|
self.assertEqual(_spec().variant_selectors(), {})
|
|
|
|
def test_variant_selectors_returned_as_plain_dict(self):
|
|
spec = _spec(active_variants={"web-app-keycloak": 2})
|
|
self.assertEqual(spec.variant_selectors(), {"web-app-keycloak": 2})
|
|
|
|
|
|
class TestResolveVariantPayloads(unittest.TestCase):
|
|
@patch("cli.deploy.development.inventory.get_variants", autospec=True)
|
|
def test_app_without_meta_collapses_to_variant_zero_payload(
|
|
self, get_variants_mock: MagicMock
|
|
) -> None:
|
|
# No `meta/variants.yml` -> loader exposes a single empty variant,
|
|
# i.e. `[{}]`. The 1st entry MUST be picked even though no variant
|
|
# was authored, so the per-app bake stays uniform.
|
|
get_variants_mock.return_value = {"web-app-foo": [{}]}
|
|
|
|
resolved = _resolve_variant_payloads(
|
|
roles_dir="/roles",
|
|
include=["web-app-foo"],
|
|
active_variants={},
|
|
)
|
|
self.assertEqual(resolved, {"web-app-foo": {}})
|
|
|
|
@patch("cli.deploy.development.inventory.get_variants", autospec=True)
|
|
def test_active_variants_picks_named_variant(
|
|
self, get_variants_mock: MagicMock
|
|
) -> None:
|
|
get_variants_mock.return_value = {
|
|
"web-app-multi": [{"variant": 0}, {"variant": 1}],
|
|
}
|
|
resolved = _resolve_variant_payloads(
|
|
roles_dir="/roles",
|
|
include=["web-app-multi"],
|
|
active_variants={"web-app-multi": 1},
|
|
)
|
|
self.assertEqual(resolved, {"web-app-multi": {"variant": 1}})
|
|
|
|
@patch("cli.deploy.development.inventory.get_variants", autospec=True)
|
|
def test_out_of_range_index_falls_back_to_variant_zero(
|
|
self, get_variants_mock: MagicMock
|
|
) -> None:
|
|
get_variants_mock.return_value = {
|
|
"web-app-multi": [{"variant": 0}, {"variant": 1}],
|
|
}
|
|
resolved = _resolve_variant_payloads(
|
|
roles_dir="/roles",
|
|
include=["web-app-multi"],
|
|
active_variants={"web-app-multi": 99},
|
|
)
|
|
self.assertEqual(resolved, {"web-app-multi": {"variant": 0}})
|
|
|
|
|
|
class TestBakeOverrides(unittest.TestCase):
|
|
def test_user_supplied_applications_override_wins_over_variant_payload(self):
|
|
baked = _bake_overrides(
|
|
base_overrides={
|
|
"STORAGE_CONSTRAINED": False,
|
|
"RUNTIME": "dev",
|
|
"applications": {
|
|
"web-app-foo": {"feature_flag": True},
|
|
},
|
|
},
|
|
variant_payloads={
|
|
"web-app-foo": {"feature_flag": False, "image": "v1"},
|
|
},
|
|
)
|
|
# User override wins on conflicting keys; non-conflicting keys
|
|
# from the variant payload remain visible.
|
|
self.assertEqual(
|
|
baked["applications"]["web-app-foo"],
|
|
{"feature_flag": True, "image": "v1"},
|
|
)
|
|
|
|
def test_no_variant_payloads_yields_unchanged_base(self):
|
|
base = {"STORAGE_CONSTRAINED": False, "RUNTIME": "dev"}
|
|
baked = _bake_overrides(base_overrides=base, variant_payloads={})
|
|
self.assertEqual(baked, base)
|
|
|
|
|
|
class TestPlanDevInventoryMatrix(unittest.TestCase):
|
|
@patch(
|
|
"cli.deploy.development.inventory._resolve_round_include",
|
|
autospec=True,
|
|
)
|
|
@patch(
|
|
"cli.deploy.development.inventory._build_services_overrides_for_round",
|
|
autospec=True,
|
|
return_value={},
|
|
)
|
|
@patch("cli.deploy.development.inventory.get_variants", autospec=True)
|
|
def test_single_variant_apps_share_one_unsuffixed_folder(
|
|
self,
|
|
get_variants_mock: MagicMock,
|
|
_overrides_mock: MagicMock,
|
|
resolve_include_mock: MagicMock,
|
|
) -> None:
|
|
get_variants_mock.return_value = {
|
|
"web-app-foo": [{}],
|
|
"web-app-bar": [{}],
|
|
}
|
|
resolve_include_mock.return_value = ("web-app-foo", "web-app-bar")
|
|
plan = plan_dev_inventory_matrix(
|
|
roles_dir="/roles",
|
|
primary_apps=["web-app-foo", "web-app-bar"],
|
|
base_inventory_dir="/srv/inv",
|
|
)
|
|
self.assertEqual(
|
|
plan,
|
|
[
|
|
(
|
|
0,
|
|
"/srv/inv",
|
|
{"web-app-foo": 0, "web-app-bar": 0},
|
|
("web-app-foo", "web-app-bar"),
|
|
)
|
|
],
|
|
)
|
|
|
|
@patch(
|
|
"cli.deploy.development.inventory._resolve_round_include",
|
|
autospec=True,
|
|
)
|
|
@patch(
|
|
"cli.deploy.development.inventory._build_services_overrides_for_round",
|
|
autospec=True,
|
|
return_value={},
|
|
)
|
|
@patch("cli.deploy.development.inventory.get_variants", autospec=True)
|
|
def test_two_variant_primary_drives_two_suffixed_folders(
|
|
self,
|
|
get_variants_mock: MagicMock,
|
|
_overrides_mock: MagicMock,
|
|
resolve_include_mock: MagicMock,
|
|
) -> None:
|
|
get_variants_mock.return_value = {
|
|
"web-app-multi": [{"variant": 0}, {"variant": 1}],
|
|
"web-app-keycloak": [{}],
|
|
}
|
|
resolve_include_mock.side_effect = [
|
|
("web-app-multi", "web-app-keycloak"),
|
|
("web-app-multi", "web-app-keycloak"),
|
|
]
|
|
plan = plan_dev_inventory_matrix(
|
|
roles_dir="/roles",
|
|
primary_apps=["web-app-multi"],
|
|
base_inventory_dir="/srv/inv",
|
|
)
|
|
self.assertEqual(
|
|
plan,
|
|
[
|
|
(
|
|
0,
|
|
"/srv/inv-0",
|
|
{"web-app-multi": 0, "web-app-keycloak": 0},
|
|
("web-app-multi", "web-app-keycloak"),
|
|
),
|
|
(
|
|
1,
|
|
"/srv/inv-1",
|
|
{"web-app-multi": 1, "web-app-keycloak": 0},
|
|
("web-app-multi", "web-app-keycloak"),
|
|
),
|
|
],
|
|
)
|
|
|
|
@patch(
|
|
"cli.deploy.development.inventory._resolve_round_include",
|
|
autospec=True,
|
|
)
|
|
@patch(
|
|
"cli.deploy.development.inventory._build_services_overrides_for_round",
|
|
autospec=True,
|
|
return_value={},
|
|
)
|
|
@patch("cli.deploy.development.inventory.get_variants", autospec=True)
|
|
def test_round_count_driven_by_primary_max_variants(
|
|
self,
|
|
get_variants_mock: MagicMock,
|
|
_overrides_mock: MagicMock,
|
|
resolve_include_mock: MagicMock,
|
|
) -> None:
|
|
# Both primaries; rounds = max(3, 2) = 3.
|
|
get_variants_mock.return_value = {
|
|
"web-app-three": [{}, {}, {}],
|
|
"web-app-two": [{}, {}],
|
|
}
|
|
resolve_include_mock.side_effect = [
|
|
("web-app-three", "web-app-two"),
|
|
("web-app-three", "web-app-two"),
|
|
("web-app-three", "web-app-two"),
|
|
]
|
|
plan = plan_dev_inventory_matrix(
|
|
roles_dir="/roles",
|
|
primary_apps=["web-app-three", "web-app-two"],
|
|
base_inventory_dir="/srv/inv",
|
|
)
|
|
# Round 0,1,2 -> three has 0,1,2 / two has 0,1,0 (clamped on R=2).
|
|
self.assertEqual(
|
|
plan,
|
|
[
|
|
(
|
|
0,
|
|
"/srv/inv-0",
|
|
{"web-app-three": 0, "web-app-two": 0},
|
|
("web-app-three", "web-app-two"),
|
|
),
|
|
(
|
|
1,
|
|
"/srv/inv-1",
|
|
{"web-app-three": 1, "web-app-two": 1},
|
|
("web-app-three", "web-app-two"),
|
|
),
|
|
(
|
|
2,
|
|
"/srv/inv-2",
|
|
{"web-app-three": 2, "web-app-two": 0},
|
|
("web-app-three", "web-app-two"),
|
|
),
|
|
],
|
|
)
|
|
|
|
def test_empty_primary_apps_rejected(self):
|
|
with self.assertRaises(ValueError):
|
|
plan_dev_inventory_matrix(
|
|
roles_dir="/roles",
|
|
primary_apps=[],
|
|
base_inventory_dir="/srv/inv",
|
|
)
|
|
|
|
|
|
class TestFilterPlanToVariant(unittest.TestCase):
|
|
PLAN: ClassVar[list[tuple]] = [
|
|
(
|
|
0,
|
|
"/srv/inv-0",
|
|
{"web-app-multi": 0, "web-app-keycloak": 0},
|
|
("web-app-multi", "web-app-keycloak"),
|
|
),
|
|
(
|
|
1,
|
|
"/srv/inv-1",
|
|
{"web-app-multi": 1, "web-app-keycloak": 0},
|
|
("web-app-multi", "web-app-keycloak"),
|
|
),
|
|
]
|
|
|
|
def test_none_returns_full_plan(self):
|
|
self.assertEqual(filter_plan_to_variant(self.PLAN, None), self.PLAN)
|
|
|
|
def test_pins_to_named_round(self):
|
|
self.assertEqual(
|
|
filter_plan_to_variant(self.PLAN, 1),
|
|
[self.PLAN[1]],
|
|
)
|
|
|
|
def test_out_of_range_raises_value_error(self):
|
|
with self.assertRaisesRegex(ValueError, "variant 7 out of range"):
|
|
filter_plan_to_variant(self.PLAN, 7)
|
|
|
|
|
|
class TestBuildDevInventory(unittest.TestCase):
|
|
def setUp(self) -> None:
|
|
self.compose = MagicMock()
|
|
# Used by inventory.py to locate roles_dir for variant lookup.
|
|
self.compose.repo_root = Path("/tmp/infinito-nexus")
|
|
|
|
@patch(
|
|
"cli.deploy.development.inventory.get_variants",
|
|
autospec=True,
|
|
return_value={
|
|
"web-app-keycloak": [{}],
|
|
"web-app-nextcloud": [{}],
|
|
},
|
|
)
|
|
@patch(
|
|
"cli.deploy.development.inventory.should_use_mirrors_on_ci",
|
|
autospec=True,
|
|
return_value=False,
|
|
)
|
|
def test_invokes_infinito_create_inventory_with_spot_vars_file(
|
|
self,
|
|
_mirrors_mock: MagicMock,
|
|
_variants_mock: MagicMock,
|
|
) -> None:
|
|
build_dev_inventory(self.compose, _spec())
|
|
|
|
# First exec call is the `infinito create inventory ...` cmd; the
|
|
# second is the password-file ensure step.
|
|
self.assertEqual(self.compose.exec.call_count, 2)
|
|
first_cmd = self.compose.exec.call_args_list[0].args[0]
|
|
self.assertEqual(first_cmd[0:3], ["infinito", "create", "inventory"])
|
|
# SPOT enforcement: the vars-file MUST come from the common.py constant.
|
|
vars_file_index = first_cmd.index("--vars-file") + 1
|
|
self.assertEqual(first_cmd[vars_file_index], DEV_INVENTORY_VARS_FILE)
|
|
|
|
include_index = first_cmd.index("--include") + 1
|
|
self.assertEqual(first_cmd[include_index], "web-app-keycloak,web-app-nextcloud")
|
|
|
|
@patch(
|
|
"cli.deploy.development.inventory.get_variants",
|
|
autospec=True,
|
|
return_value={
|
|
"web-app-multi": [
|
|
{"server": {"domains": {"canonical": ["multi.example"]}}},
|
|
{
|
|
"server": {
|
|
"domains": {"canonical": ["blog.multi.example"]},
|
|
},
|
|
# Per req-008: services live at applications.<app>.services
|
|
# directly (no `compose.services` envelope).
|
|
"services": {"x": {"flag": True}},
|
|
},
|
|
],
|
|
},
|
|
)
|
|
@patch(
|
|
"cli.deploy.development.inventory.should_use_mirrors_on_ci",
|
|
autospec=True,
|
|
return_value=False,
|
|
)
|
|
def test_active_variant_one_is_baked_into_applications_overrides(
|
|
self,
|
|
_mirrors_mock: MagicMock,
|
|
_variants_mock: MagicMock,
|
|
) -> None:
|
|
spec = _spec(
|
|
include=("web-app-multi",),
|
|
active_variants={"web-app-multi": 1},
|
|
)
|
|
build_dev_inventory(self.compose, spec)
|
|
|
|
first_cmd = self.compose.exec.call_args_list[0].args[0]
|
|
vars_index = first_cmd.index("--vars") + 1
|
|
baked = json.loads(first_cmd[vars_index])
|
|
|
|
self.assertEqual(
|
|
baked["applications"]["web-app-multi"]["server"]["domains"]["canonical"],
|
|
["blog.multi.example"],
|
|
)
|
|
self.assertEqual(
|
|
baked["applications"]["web-app-multi"]["services"]["x"],
|
|
{"flag": True},
|
|
)
|
|
# Implicit overrides untouched:
|
|
self.assertEqual(baked["STORAGE_CONSTRAINED"], False)
|
|
self.assertEqual(baked["RUNTIME"], "dev")
|
|
|
|
@patch(
|
|
"cli.deploy.development.inventory.generate_ci_mirrors_file",
|
|
autospec=True,
|
|
return_value="/etc/mirrors.yml",
|
|
)
|
|
@patch(
|
|
"cli.deploy.development.inventory.get_variants",
|
|
autospec=True,
|
|
return_value={"web-app-keycloak": [{}], "web-app-nextcloud": [{}]},
|
|
)
|
|
@patch(
|
|
"cli.deploy.development.inventory.should_use_mirrors_on_ci",
|
|
autospec=True,
|
|
return_value=True,
|
|
)
|
|
def test_appends_mirror_arg_on_ci(
|
|
self,
|
|
_mirrors_active_mock: MagicMock,
|
|
_variants_mock: MagicMock,
|
|
mirrors_file_mock: MagicMock,
|
|
) -> None:
|
|
build_dev_inventory(self.compose, _spec())
|
|
|
|
first_cmd = self.compose.exec.call_args_list[0].args[0]
|
|
mirror_index = first_cmd.index("--mirror") + 1
|
|
self.assertEqual(first_cmd[mirror_index], "/etc/mirrors.yml")
|
|
mirrors_file_mock.assert_called_once_with(
|
|
self.compose, inventory_dir="/tmp/inv"
|
|
)
|
|
|
|
@patch(
|
|
"cli.deploy.development.inventory.get_variants",
|
|
autospec=True,
|
|
return_value={"web-app-keycloak": [{}], "web-app-nextcloud": [{}]},
|
|
)
|
|
@patch(
|
|
"cli.deploy.development.inventory.should_use_mirrors_on_ci",
|
|
autospec=True,
|
|
return_value=False,
|
|
)
|
|
def test_propagates_services_disabled_into_extra_env(
|
|
self,
|
|
_mirrors_mock: MagicMock,
|
|
_variants_mock: MagicMock,
|
|
) -> None:
|
|
build_dev_inventory(self.compose, _spec(services_disabled="svc-foo,svc-bar"))
|
|
|
|
first_kwargs = self.compose.exec.call_args_list[0].kwargs
|
|
self.assertEqual(
|
|
first_kwargs.get("extra_env"),
|
|
{"SERVICES_DISABLED": "svc-foo,svc-bar"},
|
|
)
|
|
|
|
@patch(
|
|
"cli.deploy.development.inventory.get_variants",
|
|
autospec=True,
|
|
return_value={"web-app-keycloak": [{}], "web-app-nextcloud": [{}]},
|
|
)
|
|
@patch(
|
|
"cli.deploy.development.inventory.should_use_mirrors_on_ci",
|
|
autospec=True,
|
|
return_value=False,
|
|
)
|
|
def test_omits_extra_env_when_services_disabled_unset(
|
|
self,
|
|
_mirrors_mock: MagicMock,
|
|
_variants_mock: MagicMock,
|
|
) -> None:
|
|
build_dev_inventory(self.compose, _spec())
|
|
|
|
first_kwargs = self.compose.exec.call_args_list[0].kwargs
|
|
self.assertIsNone(first_kwargs.get("extra_env"))
|
|
|
|
@patch(
|
|
"cli.deploy.development.inventory.get_variants",
|
|
autospec=True,
|
|
return_value={"web-app-keycloak": [{}], "web-app-nextcloud": [{}]},
|
|
)
|
|
@patch(
|
|
"cli.deploy.development.inventory.should_use_mirrors_on_ci",
|
|
autospec=True,
|
|
return_value=False,
|
|
)
|
|
def test_runs_password_file_ensure_step(
|
|
self,
|
|
_mirrors_mock: MagicMock,
|
|
_variants_mock: MagicMock,
|
|
) -> None:
|
|
build_dev_inventory(self.compose, _spec(inventory_dir="/srv/inv/"))
|
|
|
|
password_cmd = self.compose.exec.call_args_list[1].args[0]
|
|
self.assertEqual(password_cmd[0], "sh")
|
|
self.assertEqual(password_cmd[1], "-lc")
|
|
shell_script = password_cmd[2]
|
|
self.assertIn("mkdir -p /srv/inv", shell_script)
|
|
self.assertIn("/srv/inv/.password", shell_script)
|
|
|
|
|
|
class TestBuildDevInventoryMatrix(unittest.TestCase):
|
|
def setUp(self) -> None:
|
|
self.compose = MagicMock()
|
|
self.compose.repo_root = Path("/tmp/infinito-nexus")
|
|
|
|
@patch(
|
|
"cli.deploy.development.inventory._resolve_round_include",
|
|
autospec=True,
|
|
)
|
|
@patch(
|
|
"cli.deploy.development.inventory._build_services_overrides_for_round",
|
|
autospec=True,
|
|
return_value={},
|
|
)
|
|
@patch("cli.deploy.development.inventory.build_dev_inventory", autospec=True)
|
|
@patch(
|
|
"cli.deploy.development.inventory.get_variants",
|
|
autospec=True,
|
|
return_value={
|
|
"web-app-multi": [{"v": 0}, {"v": 1}],
|
|
"web-app-keycloak": [{}],
|
|
},
|
|
)
|
|
def test_builds_one_inventory_per_round_and_returns_plan(
|
|
self,
|
|
_variants_mock: MagicMock,
|
|
build_inventory_mock: MagicMock,
|
|
_overrides_mock: MagicMock,
|
|
resolve_include_mock: MagicMock,
|
|
) -> None:
|
|
resolve_include_mock.side_effect = [
|
|
("web-app-multi", "web-app-keycloak"),
|
|
("web-app-multi", "web-app-keycloak"),
|
|
]
|
|
plan = build_dev_inventory_matrix(
|
|
self.compose,
|
|
base_inventory_dir="/srv/inv",
|
|
primary_apps=("web-app-multi",),
|
|
storage_constrained=False,
|
|
runtime="dev",
|
|
)
|
|
|
|
self.assertEqual(
|
|
[(idx, inv, vs, inc) for idx, inv, vs, inc in plan],
|
|
[
|
|
(
|
|
0,
|
|
"/srv/inv-0",
|
|
{"web-app-multi": 0, "web-app-keycloak": 0},
|
|
("web-app-multi", "web-app-keycloak"),
|
|
),
|
|
(
|
|
1,
|
|
"/srv/inv-1",
|
|
{"web-app-multi": 1, "web-app-keycloak": 0},
|
|
("web-app-multi", "web-app-keycloak"),
|
|
),
|
|
],
|
|
)
|
|
# One build_dev_inventory call per round; each gets a spec whose
|
|
# active_variants matches the round's plan entry and whose include
|
|
# is the round's variant-resolved include set.
|
|
self.assertEqual(build_inventory_mock.call_count, 2)
|
|
for (_round_idx, inv_dir, round_vars, include_R), call in zip(
|
|
plan, build_inventory_mock.call_args_list, strict=False
|
|
):
|
|
spec_arg = call.args[1]
|
|
self.assertEqual(spec_arg.inventory_dir, inv_dir)
|
|
self.assertEqual(dict(spec_arg.active_variants or {}), round_vars)
|
|
self.assertEqual(tuple(spec_arg.include), include_R)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|