Files
computer-playbook/tests/unit/python/cli/administration/deploy/development/test_compose.py
T
kevinveenbirkenbachandClaude Opus 5 7c13e96c96 Merge branch 'main' into feature/store-i18n
Twelve files conflicted; most resolutions keep both sides rather than pick
one.

cli env.py and compose.py: both branches extend the same spots. The tools
override is appended before write_override, because that call filters the
mirror override to the compose files it is handed. compose.py keeps this
branch's CACHE_ROLE_SHELL_DIR, since the cache scripts moved into a role and
main's literal scripts/docker/cache path is their old home, and takes main's
mirrored init image alongside it.

utils/docker/image/discovery.py: this branch's version_variants expansion
plus main's whole runner-image block, which runner_mirror imports.

sys-svc-compose network create: main's side wins on substance and this
branch's on structure. main carries eda6249d7b, the fix for a probe that
matched an id prefix and let a sibling shadow the exact name; this branch had
refactored the task into create/overlay.yml and create/bridge.yml but brought
back the docker_network_info probe that fix replaced. The subfiles stay and
now probe through the exact-name lookup. main's half also read
SWARM_CLUSTER_AVAILABLE, which this branch removed in favour of
IS_SWARM_MODE, as the merge base confirms, so the gates follow this branch.
The lookup's import follows this branch's get_entity_name to entity_name
rename.

web-app-pixelfed: the custom entrypoints stay removed, since the new image
brings its own and the variables behind them no longer exist, and main's
host_alias=true comes along.

web-app-suitecrm: main's session.yaml and its config/packages path, because
only that version wires framework.session and only that directory is read by
Symfony.

web-app-odoo: main's onionOptions import is required by the test.use below
it; this branch's test-seaweedfs registration stays.

Four defects surfaced that no conflict marker shows.

The auto-merge of web-app-suitecrm/vars/main.yml duplicated
SUITECRM_SESSION_CONFIG_HOST, which ansible-lint rejects. Removing it exposed
the second: SUITECRM_SESSION_CONFIG_DOCKER still named config/session.yaml
while the mount had moved to config/packages, so the freshness comparison in
tasks/01_manager_ops.yml would have tested a path nothing writes.

svc-db-elasticsearch carried environment: on two include_tasks, which Ansible
accepts on include_role only. ELASTIC_PW now sits on the five tasks that read
it, which is also the tighter scope for a credential.

The openclaw templates render NETWORK_TRUSTED_PROXY_CIDRS while their test
renderer passes only its explicit kwargs, so seven tests raised Undefined.
That predates the merge: the template line is on this branch and not on main,
and both sides share the same test file.

test_runner_mirror neutralises four environment variables under clear=False
and missed three, so the gpu and tools overrides leaked in from the container
and the assertion on the layered list broke. It now neutralises all seven.

make quality-high passes both phases.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-13 10:57:22 +02:00

342 lines
11 KiB
Python

# tests/cli/administration/deploy/development/test_compose.py
from __future__ import annotations
import os
import subprocess
import unittest
from pathlib import Path
from unittest.mock import MagicMock, patch
from cli.administration.deploy.development.compose import Compose
class TestComposeUpRetries(unittest.TestCase):
def _compose(self) -> Compose:
return Compose(repo_root=Path("/tmp/infinito-nexus"), distro="arch")
@patch("time.sleep", autospec=True)
def test_compose_up_with_retries_succeeds_after_transient_failures(
self, sleep_mock: MagicMock
) -> None:
compose = self._compose()
compose.run = MagicMock(
side_effect=[
subprocess.CalledProcessError(1, ["docker", "compose"], "out1", "err1"),
subprocess.CalledProcessError(1, ["docker", "compose"], "out2", "err2"),
subprocess.CompletedProcess(
["docker", "compose"], 0, stdout="", stderr=""
),
]
)
args = [
"up",
"-d",
"--no-build",
"coredns",
"infinito",
]
compose._compose_up_with_retries(args, attempts=6, delay_s=30)
self.assertEqual(compose.run.call_count, 3)
self.assertEqual(sleep_mock.call_count, 2)
sleep_mock.assert_any_call(30)
self.assertTrue(all(call.args == (30,) for call in sleep_mock.call_args_list))
@patch("time.sleep", autospec=True)
def test_compose_up_with_retries_raises_after_exhausting_attempts(
self, sleep_mock: MagicMock
) -> None:
compose = self._compose()
compose.run = MagicMock(
side_effect=subprocess.CalledProcessError(
1, ["docker", "compose"], "out", "err"
)
)
args = ["up", "-d", "coredns", "infinito"]
with self.assertRaises(subprocess.CalledProcessError):
compose._compose_up_with_retries(args, attempts=6, delay_s=30)
self.assertEqual(compose.run.call_count, 6)
self.assertEqual(sleep_mock.call_count, 5)
self.assertTrue(all(call.args == (30,) for call in sleep_mock.call_args_list))
@patch("time.sleep", autospec=True)
def test_compose_up_with_retries_no_sleep_if_first_try_succeeds(
self, sleep_mock: MagicMock
) -> None:
compose = self._compose()
compose.run = MagicMock(
return_value=subprocess.CompletedProcess(
["docker", "compose"], 0, stdout="", stderr=""
)
)
args = ["up", "-d", "coredns", "infinito"]
compose._compose_up_with_retries(args, attempts=6, delay_s=30)
self.assertEqual(compose.run.call_count, 1)
sleep_mock.assert_not_called()
@patch.dict(
os.environ,
{
"INFINITO_IMAGE": "test-image/arch",
"GITHUB_ACTIONS": "true",
"INFINITO_RUNNING_ON_GITHUB": "true",
"CI": "true",
"INFINITO_GIT_COMMON_DIR": "",
"INFINITO_CACHE_NETWORK": "",
"INFINITO_CACHE_STACK": "",
"INFINITO_GPU_COUNT": "0",
},
clear=False,
)
@patch(
"cli.administration.deploy.development.runner_mirror.write_override",
return_value="build/compose/runner-mirror.override.yml",
)
@patch("subprocess.run", autospec=True)
def test_run_skips_cache_override_on_github_runner(
self, run_mock: MagicMock, _write_override: MagicMock
) -> None:
compose = self._compose()
run_mock.return_value = subprocess.CompletedProcess(
["docker", "compose", "ps", "-q", "infinito"],
0,
stdout="cid\n",
stderr="",
)
r = compose.run(["ps", "-q", "infinito"], check=False, capture=True)
self.assertEqual(r.returncode, 0)
self.assertEqual(run_mock.call_count, 1)
cmd = run_mock.call_args.args[0]
env = run_mock.call_args.kwargs["env"]
self.assertEqual(
cmd,
[
"docker",
"compose",
"-f",
"compose.yml",
"-f",
"compose/tools.override.yml",
"-f",
"build/compose/runner-mirror.override.yml",
"ps",
"-q",
"infinito",
],
)
self.assertEqual(env["INFINITO_DISTRO"], "arch")
self.assertNotIn("COMPOSE_PROFILES", env)
@patch.dict(
os.environ,
{
"INFINITO_IMAGE": "test-image/arch",
"GITHUB_ACTIONS": "",
"INFINITO_RUNNING_ON_GITHUB": "",
"CI": "",
"INFINITO_GIT_COMMON_DIR": "",
"INFINITO_CACHE_NETWORK": "",
"INFINITO_CACHE_STACK": "",
"INFINITO_GPU_COUNT": "0",
},
clear=False,
)
@patch("subprocess.run", autospec=True)
def test_run_layers_cache_override_locally(self, run_mock: MagicMock) -> None:
compose = self._compose()
run_mock.return_value = subprocess.CompletedProcess(
["docker", "compose"], 0, stdout="", stderr=""
)
compose.run(["ps", "-q", "infinito"], check=False, capture=True)
cmd = run_mock.call_args.args[0]
env = run_mock.call_args.kwargs["env"]
self.assertEqual(
cmd,
[
"docker",
"compose",
"-f",
"compose.yml",
"-f",
"compose/cache.override.yml",
"-f",
"compose/tools.override.yml",
"ps",
"-q",
"infinito",
],
)
self.assertEqual(
env["INFINITO_CACHE_REGISTRY_PROXY_CONF"], # nocheck: test-fixture
"./compose/registry-cache/proxy.conf",
)
self.assertTrue(
env["INFINITO_CACHE_PACKAGE_FRONTEND_CA_FILE"].endswith(
"/ca.crt"
) # nocheck: test-fixture
)
@patch.dict(
os.environ,
{
"INFINITO_BUILD": "1",
"INFINITO_IMAGE": "infinito-debian",
"INFINITO_PULL_POLICY": "never",
"CI": "true",
"GITHUB_ACTIONS": "true",
"INFINITO_RUNNING_ON_GITHUB": "true",
"INFINITO_CACHE_STACK": "",
},
clear=False,
)
def test_up_builds_when_build_flag_is_enabled(self) -> None:
compose = self._compose()
compose._render_coredns_corefile = MagicMock()
compose._compose_up_with_retries = MagicMock()
compose.wait_for_healthy = MagicMock()
compose.up(run_entry_init=False)
compose._compose_up_with_retries.assert_called_once_with(
["up", "-d", "coredns", "infinito"],
attempts=6,
delay_s=30,
)
compose.wait_for_healthy.assert_called_once_with()
@patch.dict(
os.environ,
{
"INFINITO_BUILD": "0",
"INFINITO_IMAGE": "test-image/arch",
"CI": "true",
"GITHUB_ACTIONS": "true",
"INFINITO_RUNNING_ON_GITHUB": "true",
"INFINITO_CACHE_STACK": "",
},
clear=False,
)
def test_up_skips_build_when_build_flag_is_disabled(self) -> None:
compose = self._compose()
compose._render_coredns_corefile = MagicMock()
compose._compose_up_with_retries = MagicMock()
compose.wait_for_healthy = MagicMock()
compose.up(run_entry_init=False)
compose._compose_up_with_retries.assert_called_once_with(
[
"up",
"-d",
"--no-build",
"coredns",
"infinito",
],
attempts=6,
delay_s=30,
)
compose.wait_for_healthy.assert_called_once_with()
@patch.dict(
os.environ,
{
"INFINITO_BUILD": "1",
"INFINITO_IMAGE": "infinito-debian",
"INFINITO_PULL_POLICY": "never",
"CI": "",
"GITHUB_ACTIONS": "",
"INFINITO_RUNNING_ON_GITHUB": "",
"INFINITO_GIT_COMMON_DIR": "",
"INFINITO_CACHE_NETWORK": "",
"INFINITO_CACHE_STACK": "",
},
clear=False,
)
def test_up_includes_cache_services_when_local(self) -> None:
compose = self._compose()
compose._render_coredns_corefile = MagicMock()
compose._compose_up_with_retries = MagicMock()
compose.wait_for_healthy = MagicMock()
compose._bootstrap_package_cache = MagicMock()
compose._generate_package_frontend_certs = MagicMock()
compose._install_package_frontend_ca_in_runner = MagicMock()
compose.up(run_entry_init=False)
compose._compose_up_with_retries.assert_called_once_with(
[
"up",
"-d",
"registry-cache",
"package-cache",
"package-cache-frontend",
"coredns",
"infinito",
],
attempts=6,
delay_s=30,
)
compose.wait_for_healthy.assert_called_once_with()
compose._bootstrap_package_cache.assert_called_once()
compose._generate_package_frontend_certs.assert_called_once()
compose._install_package_frontend_ca_in_runner.assert_called_once()
@patch.dict(
os.environ,
{
"INFINITO_BUILD": "1",
"INFINITO_IMAGE": "infinito-debian",
"INFINITO_PULL_POLICY": "never",
"CI": "",
"GITHUB_ACTIONS": "",
"INFINITO_RUNNING_ON_GITHUB": "",
"INFINITO_GIT_COMMON_DIR": "/primary/.git",
"INFINITO_CACHE_NETWORK": "infinito-nexus-core_default",
"INFINITO_CACHE_STACK": "",
},
clear=False,
)
def test_up_applies_the_declarations_to_a_cache_stack_it_shares(self) -> None:
compose = self._compose()
compose._render_coredns_corefile = MagicMock()
compose._compose_up_with_retries = MagicMock()
compose.wait_for_healthy = MagicMock()
compose._bootstrap_package_cache = MagicMock()
compose._generate_package_frontend_certs = MagicMock()
compose._apply_shared_cache = MagicMock()
compose._install_package_frontend_ca_in_runner = MagicMock()
compose.up(run_entry_init=False)
compose._compose_up_with_retries.assert_called_once_with(
["up", "-d", "coredns", "infinito"], attempts=6, delay_s=30
)
compose._generate_package_frontend_certs.assert_not_called()
compose._bootstrap_package_cache.assert_not_called()
compose._apply_shared_cache.assert_called_once()
compose._install_package_frontend_ca_in_runner.assert_called_once()
if __name__ == "__main__": # pragma: no cover
unittest.main()