Files
computer-playbook/tests/integration/meta/env/test_dotenv_generator.py
T
kevinveenbirkenbachandClaude Opus 5 8aba198aec feat(worktree): isolated per-branch checkouts sharing one cache stack
Parallel checkouts collided on the compose subnet, host ports and container
names, so a quality run in a worktree died at compose-up. Slot each checkout
via custom.env and layer two overrides on top: the shared cache network in
place of a second cache stack, and the shared .git directory without which
git aborts inside the container.

Both overlays gate on the resource they need, not on the slot number, so a
stray INFINITO_INSTANCE cannot make compose demand a value nobody set.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 16:15:59 +02:00

246 lines
8.5 KiB
Python

"""Drift + smoke guards for the project env generator.
The generator at ``cli/meta/env/__main__.py`` is the single source of
truth that materialises ``.env`` from the committed ``default.env``
at the repo root plus runtime context. This integration test locks in
two invariants:
1. **Drift**: every key declared in ``default.env`` is read by a
handler in :mod:`utils.env.handlers` (exposed via
``PASSTHROUGH_STATIC_KEYS`` / ``GHA_STATIC_KEYS``). No silent drops.
This is a cross-file consistency check, not a single-file lint.
2. **Smoke**: running ``python -m cli.meta.env`` end-to-end with a
clean environment writes a ``.env`` containing the expected
baseline of variables. Subprocess-driven, end-to-end -- hence
integration, not unit / lint.
"""
from __future__ import annotations
import os
import re
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
from utils.cache.files import iter_project_files, read_text
from utils.env.handlers import GHA_STATIC_KEYS, PASSTHROUGH_STATIC_KEYS
from . import PROJECT_ROOT
STATIC_ENV = PROJECT_ROOT / "default.env"
EXPECTED_BASELINE_KEYS = frozenset(
{
"INFINITO_SRC_DIR",
"INFINITO_BIND_IP",
"INFINITO_SUBNET",
"INFINITO_GATEWAY",
"INFINITO_DISTRO",
"INFINITO_CONTAINER",
"INFINITO_CONTAINER_CA_ENABLED",
"INFINITO_RUNNING_ON_ACT",
"INFINITO_RUNNING_ON_GITHUB",
"INFINITO_IS_WSL2",
"INFINITO_PULL_POLICY",
"INFINITO_MEM_LIMIT",
"INFINITO_MEMSWAP_LIMIT",
"INFINITO_CPUS",
"INFINITO_WAIT_HEALTH_TIMEOUT_S",
"INFINITO_DOCKER_VOLUME",
"INFINITO_DOCKER_MOUNT",
"INFINITO_OUTER_NETWORK_MTU",
"INFINITO_TEST_PATTERN",
"INFINITO_TEST_RUNNER",
"INFINITO_LINT_RUNNER",
"INFINITO_DISTROS",
"INFINITO_DEBUG",
"INFINITO_WHITELIST",
"INFINITO_LIMIT_HOST",
"INFINITO_RUN_FLAGS",
"INFINITO_WORKER_ENABLED",
"INFINITO_WORKER_CPU",
"INFINITO_WORKER_FETCH",
"INFINITO_INVENTORY_VARS_FILE",
"INFINITO_CACHE_REGISTRY_HOST_PATH",
"INFINITO_CACHE_REGISTRY_CA_HOST_PATH",
"INFINITO_CACHE_REGISTRY_MAX_SIZE",
"INFINITO_CACHE_PACKAGE_HOST_PATH",
"INFINITO_CACHE_PACKAGE_PORT",
"INFINITO_CACHE_PACKAGE_HEAP",
"INFINITO_CACHE_PACKAGE_DIRECT_MEM",
"INFINITO_CACHE_PACKAGE_BLOBSTORE_MAX",
"INFINITO_CACHE_PACKAGE_ADMIN_PASSWORD",
"INFINITO_CACHE_PACKAGE_FRONTEND_CA_DIR",
"INFINITO_CACHE_PACKAGE_FRONTEND_CERTS_DIR",
"INFINITO_CACHE_PACKAGE_FRONTEND_IP",
"INFINITO_CACHE_PACKAGE_FRONTEND_INIT_IMAGE",
"INFINITO_CACHE_PACKAGE_MAX_AGE_MIN",
"INFINITO_IMAGE_TAG",
}
)
_LINE_RE = re.compile(r"^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$")
def parse_static_env(path: Path) -> dict[str, str]:
result: dict[str, str] = {}
for raw in read_text(str(path)).splitlines():
stripped = raw.lstrip()
if not stripped or stripped.startswith("#"):
continue
match = _LINE_RE.match(stripped)
if not match:
continue
key, value = match.group(1), match.group(2)
if value and value[0] not in ('"', "'"):
value = value.split("#", 1)[0].rstrip()
if len(value) >= 2 and value[0] == value[-1] and value[0] in ('"', "'"):
value = value[1:-1]
result[key] = value
return result
def parse_dotenv(path: Path) -> dict[str, str]:
result: dict[str, str] = {}
for raw in read_text(str(path)).splitlines():
stripped = raw.strip()
if not stripped or stripped.startswith("#") or "=" not in stripped:
continue
key, _, value = stripped.partition("=")
key = key.strip()
value = value.strip()
if len(value) >= 2 and value[0] == value[-1] and value[0] in ('"', "'"):
value = value[1:-1]
result[key] = value
return result
def _dynamic_handler_static_reads() -> set[str]:
"""Collect ``STATIC_READS`` keys declared by dynamic handler modules.
Dynamic handlers that pull their fallback from ``ctx.static[KEY]``
must list the key in a module-level ``STATIC_READS`` tuple so this
drift test recognises the read.
"""
import importlib
handlers_dir = PROJECT_ROOT / "utils" / "env" / "handlers"
referenced: set[str] = set()
module_paths = sorted(
Path(p)
for p in iter_project_files(extensions=(".py",))
if Path(p).is_relative_to(handlers_dir)
)
for module_path in module_paths:
if module_path.name in ("__init__.py", "passthrough.py", "gha_passthrough.py"):
continue
module_name = "utils.env.handlers." + ".".join(
module_path.relative_to(handlers_dir).with_suffix("").parts
)
module = importlib.import_module(module_name)
referenced.update(getattr(module, "STATIC_READS", ()))
return referenced
def generator_referenced_keys() -> set[str]:
"""Return the set of default.env keys the generator actually reads.
Sourced from the handler registry's ``PASSTHROUGH_STATIC_KEYS`` and
``GHA_STATIC_KEYS`` tuples plus any dynamic handler module's
``STATIC_READS`` tuple. Catches the most common drift: a new key in
default.env that no handler references."""
return (
set(PASSTHROUGH_STATIC_KEYS)
| set(GHA_STATIC_KEYS)
| _dynamic_handler_static_reads()
)
class TestStaticYamlDrift(unittest.TestCase):
def test_every_static_env_key_is_referenced_by_generator(self) -> None:
static_keys = set(parse_static_env(STATIC_ENV))
ref_keys = generator_referenced_keys()
unread = static_keys - ref_keys
self.assertFalse(
unread,
"Keys in default.env are not read by any handler under "
"utils/env/handlers/ (silent drop). Add them to "
"PASSTHROUGH_STATIC_KEYS or GHA_STATIC_KEYS: "
f"{sorted(unread)}",
)
def test_generator_only_references_existing_static_keys(self) -> None:
static_keys = set(parse_static_env(STATIC_ENV))
ref_keys = generator_referenced_keys()
phantom = ref_keys - static_keys
self.assertFalse(
phantom,
"Generator references keys that do not exist in default.env: "
f"{sorted(phantom)}",
)
class TestDotenvSmoke(unittest.TestCase):
"""End-to-end: run the generator in a temp dir and check the
produced .env contains the expected baseline keys."""
def test_clean_run_produces_expected_keys(self) -> None:
if (PROJECT_ROOT / "custom.env").is_file():
self.skipTest(
"custom.env overrides handler-computed values by design "
"(see utils/env/builder.py:apply_custom_env), so this checkout "
"cannot produce a clean run; 'make worktree-up' writes one."
)
with tempfile.TemporaryDirectory() as td:
env = {
k: v
for k, v in os.environ.items()
if not k.startswith("INFINITO_")
and k
not in (
"GITHUB_ACTIONS",
"ACT",
"VIRTUAL_ENV",
"GITHUB_REPOSITORY",
"GITHUB_REPOSITORY_OWNER",
)
}
env.setdefault("PATH", os.environ.get("PATH", ""))
result = subprocess.run(
[sys.executable, "-m", "cli.meta.env"],
env=env,
cwd=str(PROJECT_ROOT),
text=True,
capture_output=True,
check=False,
)
self.assertEqual(
result.returncode,
0,
f"Generator failed: {result.stderr}",
)
dotenv = PROJECT_ROOT / ".env"
self.assertTrue(dotenv.is_file(), "Generator did not produce .env")
values = parse_dotenv(dotenv)
missing = EXPECTED_BASELINE_KEYS - set(values)
self.assertFalse(
missing,
f"Generated .env is missing expected baseline keys: {sorted(missing)}",
)
self.assertEqual(
values.get("INFINITO_CONTAINER"),
f"infinito_nexus_{values.get('INFINITO_DISTRO')}",
"INFINITO_CONTAINER must be infinito_nexus_${INFINITO_DISTRO}",
)
_ = td
if __name__ == "__main__":
unittest.main()