mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-09 20:26:33 +00:00
Three new lint tests close gaps in the INFINITO_* SPOT contract:
* tests/lint/filesystem/env/test_default_env_keys_handled.py
Every default.env key must be wired through a handler.
* tests/lint/filesystem/env/test_used_keys_declared.py
Every INFINITO_* in .sh/.py must live in env/{default,ci}.env or
a handler; per-line `# nocheck:` suppresses runtime-only refs.
* tests/lint/filesystem/python/test_no_infinito_get_defaults.py
Forbid ANY default in `.get("INFINITO_*", ...)` -- including the
empty-string fallback (which silently masks an unset key). The
nocheck check walks every physical line the call spans so an
autoformat-split call still honours the marker.
Cache namespace flattens to a single prefix:
INFINITO_PACKAGE_CACHE_* -> INFINITO_CACHE_PACKAGE_*
INFINITO_REGISTRY_CACHE_* -> INFINITO_CACHE_REGISTRY_*
The five previously driver-injected paths (APT_LIST, NPMRC,
PIP_CONF, REGISTRY_PROXY_CONF, FRONTEND_CA_FILE) move into
env/default.env and passthrough.STATIC_KEYS; the
`cache_env_overrides()` helper and its two call sites are gone
along with the test-fixture override for FRONTEND_CA_DIR.
SPOT-strict rewrites:
- 19 sites: `something.get("INFINITO_X", "")` -> subscript
`something["INFINITO_X"]` (loud KeyError when env loader did
not run; all keys live in env/default.env).
- 2 handler-internal bootstrap reads suppressed with
`# nocheck: handler-bootstrap-read`.
- 30 runtime-only / driver-injected / test-fixture references
annotated with `# nocheck: <reason>` per-line.
- Unit tests that previously removed SPOT-guaranteed keys via
`{k: v ... if k \!= KEY}` or `clear=True` switch to setting
the key to "" explicitly, matching the new contract.
Side fixes:
- INFINITO_DNS_DOMAIN + INFINITO_DNS_HOSTS_FILE move from
shell-side defaults in common.sh to env/default.env and
passthrough.STATIC_KEYS.
- dns_systemd_is_operational() drops the dead
INFINITO_DNS_FORCE_HOSTS_FALLBACK override branch (the
variable was never set anywhere).
- INFINITO_WORKER_FETCH switches from `os.environ.get(KEY, "1")`
to `os.environ[KEY]`.
- scripts/tests/code/wrapper.sh exec args move into a bash
array so `# nocheck:` on INFINITO_TEST_TYPE can live on its
own element line (SC2215 fix).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
307 lines
9.6 KiB
Python
307 lines
9.6 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:
|
|
# repo_root is only used as a cwd in subprocess calls; for these unit tests
|
|
# we do not execute real subprocesses.
|
|
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()
|
|
|
|
# Fail 2 times, then succeed.
|
|
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 = [
|
|
"--env-file",
|
|
"env/ci.env",
|
|
"up",
|
|
"-d",
|
|
"--no-build",
|
|
"coredns",
|
|
"infinito",
|
|
]
|
|
compose._compose_up_with_retries(args, attempts=6, delay_s=30)
|
|
|
|
# 3 calls: fail, fail, succeed
|
|
self.assertEqual(compose.run.call_count, 3)
|
|
|
|
# Sleep after each failure (2 times), not after success
|
|
self.assertEqual(sleep_mock.call_count, 2)
|
|
sleep_mock.assert_any_call(30)
|
|
# Ensure all sleeps used exactly 30 seconds
|
|
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()
|
|
|
|
# Always fail.
|
|
compose.run = MagicMock(
|
|
side_effect=subprocess.CalledProcessError(
|
|
1, ["docker", "compose"], "out", "err"
|
|
)
|
|
)
|
|
|
|
args = ["--env-file", "env/ci.env", "up", "-d", "coredns", "infinito"]
|
|
|
|
with self.assertRaises(subprocess.CalledProcessError):
|
|
compose._compose_up_with_retries(args, attempts=6, delay_s=30)
|
|
|
|
# Called exactly 6 times
|
|
self.assertEqual(compose.run.call_count, 6)
|
|
|
|
# Sleep between attempts: after attempts 1..5 => 5 sleeps
|
|
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 = ["--env-file", "env/ci.env", "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",
|
|
},
|
|
clear=False,
|
|
)
|
|
@patch("subprocess.run", autospec=True)
|
|
def test_run_uses_only_ci_profile_on_github_runner(
|
|
self, run_mock: MagicMock
|
|
) -> None:
|
|
compose = self._compose()
|
|
|
|
run_mock.return_value = subprocess.CompletedProcess(
|
|
["docker", "compose", "--profile", "ci", "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"]
|
|
|
|
# CI: only the ci profile fires; cache override is not layered in.
|
|
self.assertEqual(
|
|
cmd,
|
|
[
|
|
"docker",
|
|
"compose",
|
|
"-f",
|
|
"compose.yml",
|
|
"--profile",
|
|
"ci",
|
|
"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": "",
|
|
},
|
|
clear=False,
|
|
)
|
|
@patch("subprocess.run", autospec=True)
|
|
def test_run_activates_cache_profile_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"]
|
|
|
|
# Local: cache override is layered onto compose.yml; no --profile cache.
|
|
self.assertEqual(
|
|
cmd,
|
|
[
|
|
"docker",
|
|
"compose",
|
|
"-f",
|
|
"compose.yml",
|
|
"-f",
|
|
"compose/cache.override.yml",
|
|
"--profile",
|
|
"ci",
|
|
"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_NO_BUILD": "0",
|
|
"INFINITO_IMAGE": "infinito-debian",
|
|
"INFINITO_PULL_POLICY": "never",
|
|
# CI-pinned so cache stays inactive; this test asserts build behaviour only.
|
|
"CI": "true",
|
|
"GITHUB_ACTIONS": "true",
|
|
"INFINITO_RUNNING_ON_GITHUB": "true",
|
|
},
|
|
clear=False,
|
|
)
|
|
def test_up_builds_when_no_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(
|
|
["--env-file", "env/ci.env", "up", "-d", "coredns", "infinito"],
|
|
attempts=6,
|
|
delay_s=30,
|
|
)
|
|
compose.wait_for_healthy.assert_called_once_with()
|
|
|
|
@patch.dict(
|
|
os.environ,
|
|
{
|
|
"INFINITO_NO_BUILD": "1",
|
|
"INFINITO_IMAGE": "test-image/arch",
|
|
"CI": "true",
|
|
"GITHUB_ACTIONS": "true",
|
|
"INFINITO_RUNNING_ON_GITHUB": "true",
|
|
},
|
|
clear=False,
|
|
)
|
|
def test_up_skips_build_when_no_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(
|
|
[
|
|
"--env-file",
|
|
"env/ci.env",
|
|
"up",
|
|
"-d",
|
|
"--no-build",
|
|
"coredns",
|
|
"infinito",
|
|
],
|
|
attempts=6,
|
|
delay_s=30,
|
|
)
|
|
compose.wait_for_healthy.assert_called_once_with()
|
|
|
|
@patch.dict(
|
|
os.environ,
|
|
{
|
|
"INFINITO_NO_BUILD": "0",
|
|
"INFINITO_IMAGE": "infinito-debian",
|
|
"INFINITO_PULL_POLICY": "never",
|
|
"CI": "",
|
|
"GITHUB_ACTIONS": "",
|
|
"INFINITO_RUNNING_ON_GITHUB": "",
|
|
},
|
|
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)
|
|
|
|
# Cache services precede coredns + infinito so depends_on health gates resolve.
|
|
compose._compose_up_with_retries.assert_called_once_with(
|
|
[
|
|
"--env-file",
|
|
"env/ci.env",
|
|
"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()
|
|
|
|
|
|
if __name__ == "__main__": # pragma: no cover
|
|
unittest.main()
|