Files
computer-playbook/tests/unit/python/utils/env/test_builder.py
T
kevinveenbirkenbachandClaude Opus 5 c9b3f84b48 feat(env): honour the assigned architecture on a single-CPU host
In CI the architecture axis is satisfied by the runner: the matrix picks a
row's architecture and `runs-on` lands the job on that hardware. A workstation
has one CPU and cannot be asked for another, so the same INFINITO_ARCHITECTURE
is honoured by telling the docker client which platform to pull and create, and
letting qemu execute it.

Two decisions here are easy to tidy away and should not be:

The key is deliberately not spelled DOCKER_DEFAULT_PLATFORM. The env layer is
sourced into the operator's own shell, where that name would pin every docker
call, including the ones that build the tooling image itself. compose.yml maps
it to docker's name inside the deploy container, so the platform applies where
app containers are created and nowhere else.

builder.py applies the handler a second time, after custom.env. That file
overrides handler output, so an operator naming the architecture there would
otherwise get the architecture without the platform it implies, and the deploy
would run on the host's own CPU under an arm64 label.

The key is emitted even when empty, because compose.yml reads it bare and this
repository forbids an inline default in a compose file. Empty means what unset
always meant: the host's own architecture.

test_walks_full_handler_chain counted calls against handlers as a proxy for
"every handler is reachable", which only holds while each runs exactly once.
The deliberate second application made that 42 != 41. It now compares the two
sets, which is what its own docstring claims to check.

Verified: 116 unit tests under tests/unit/python/utils/env pass; a deploy with
INFINITO_ARCHITECTURE=arm64 carries DOCKER_DEFAULT_PLATFORM=linux/arm64 into
the container and reaches ok=1444, with aarch64-linux-gnu paths in an image
build proving the containers are genuinely arm64.

Known gap: switching architecture needs `rm -f .env` first. Plain `make dotenv`
reads the previous INFINITO_DOCKER_PLATFORM back out of .env and the
contradiction guard treats it as an operator assertion.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-19 06:52:50 +02:00

302 lines
12 KiB
Python

"""Unit tests for :mod:`utils.env.builder`.
Covers the orchestration layer:
* :class:`EnvBuilder` — setdefault/set/get semantics around the
caller-env precedence and per-key comment recording.
* :class:`BuildContext` — dataclass shape, immutability.
* :func:`build_env` — walks ``ORDERED_HANDLERS`` so its output reflects
every handler's contribution. Asserted at the registry level (every
handler ran, no value got dropped, comments propagate end-to-end).
"""
from __future__ import annotations
import dataclasses
import unittest
from pathlib import Path
from unittest.mock import patch
from utils.env.builder import BuildContext, EnvBuilder, build_env
from utils.env.handlers import ORDERED_HANDLERS, PASSTHROUGH_STATIC_KEYS
class TestEnvBuilderSetdefault(unittest.TestCase):
def test_falls_back_to_value_when_env_unset(self) -> None:
with patch.dict("os.environ", {}, clear=True):
eb = EnvBuilder()
self.assertEqual(eb.setdefault("FOO", "bar"), "bar")
self.assertEqual(eb.values["FOO"], "bar")
def test_caller_env_wins_when_non_empty(self) -> None:
with patch.dict("os.environ", {"FOO": "from-env"}, clear=True):
eb = EnvBuilder()
self.assertEqual(eb.setdefault("FOO", "default"), "from-env")
self.assertEqual(eb.values["FOO"], "from-env")
def test_empty_string_env_falls_back_to_default(self) -> None:
with patch.dict("os.environ", {"FOO": ""}, clear=True):
eb = EnvBuilder()
self.assertEqual(eb.setdefault("FOO", "default"), "default")
def test_whitespace_only_env_falls_back_to_default(self) -> None:
with patch.dict("os.environ", {"FOO": " "}, clear=True):
eb = EnvBuilder()
self.assertEqual(eb.setdefault("FOO", "default"), "default")
def test_records_comment_first_time(self) -> None:
with patch.dict("os.environ", {}, clear=True):
eb = EnvBuilder()
eb.setdefault("K", "v", comment="why")
self.assertEqual(eb.comments["K"], "why")
def test_comment_first_wins(self) -> None:
with patch.dict("os.environ", {}, clear=True):
eb = EnvBuilder()
eb.setdefault("K", "v", comment="first")
eb.setdefault("K", "v2", comment="second")
self.assertEqual(eb.comments["K"], "first")
def test_empty_comment_arg_does_not_register(self) -> None:
with patch.dict("os.environ", {}, clear=True):
eb = EnvBuilder()
eb.setdefault("K", "v")
self.assertNotIn("K", eb.comments)
class TestEnvBuilderSet(unittest.TestCase):
def test_unconditional_overwrite(self) -> None:
with patch.dict("os.environ", {"FOO": "from-env"}, clear=True):
eb = EnvBuilder()
eb.set("FOO", "forced")
self.assertEqual(eb.values["FOO"], "forced")
def test_set_overwrites_previously_setdefault(self) -> None:
with patch.dict("os.environ", {}, clear=True):
eb = EnvBuilder()
eb.setdefault("FOO", "first")
eb.set("FOO", "second")
self.assertEqual(eb.values["FOO"], "second")
def test_set_first_comment_wins(self) -> None:
with patch.dict("os.environ", {}, clear=True):
eb = EnvBuilder()
eb.set("K", "v1", comment="first")
eb.set("K", "v2", comment="second")
self.assertEqual(eb.comments["K"], "first")
def test_set_returns_value(self) -> None:
with patch.dict("os.environ", {}, clear=True):
eb = EnvBuilder()
self.assertEqual(eb.set("K", "v"), "v")
class TestEnvBuilderGet(unittest.TestCase):
def test_reads_from_builder_first(self) -> None:
with patch.dict("os.environ", {"FOO": "env"}, clear=True):
eb = EnvBuilder()
eb.set("FOO", "builder")
self.assertEqual(eb.get("FOO"), "builder")
def test_falls_back_to_os_environ(self) -> None:
with patch.dict("os.environ", {"FOO": "env"}, clear=True):
eb = EnvBuilder()
self.assertEqual(eb.get("FOO"), "env")
def test_missing_returns_empty_string(self) -> None:
with patch.dict("os.environ", {}, clear=True):
eb = EnvBuilder()
self.assertEqual(eb.get("MISSING"), "")
class TestBuildContext(unittest.TestCase):
def test_dataclass_shape(self) -> None:
ctx = BuildContext(
static={"K": "v"},
static_comments={"K": "c"},
repo_root=Path("/repo"),
on_gha=False,
on_act=False,
)
self.assertEqual(ctx.static, {"K": "v"})
self.assertEqual(ctx.static_comments, {"K": "c"})
self.assertEqual(ctx.repo_root, Path("/repo"))
self.assertFalse(ctx.on_gha)
self.assertFalse(ctx.on_act)
def test_is_frozen(self) -> None:
ctx = BuildContext(
static={},
static_comments={},
repo_root=Path("/"),
on_gha=False,
on_act=False,
)
with self.assertRaises(dataclasses.FrozenInstanceError):
ctx.on_gha = True # type: ignore[misc]
class TestBuildEnvOrchestration(unittest.TestCase):
"""End-to-end smoke of build_env against a minimal static map.
Mocks ``detect_gha_act`` so we don't depend on the host env, and
mocks the inventory helper so the test never shells out.
"""
def _clean_env(self) -> dict[str, str]:
return {"PATH": "/usr/bin"}
def test_passes_static_through_builder(self) -> None:
with (
patch.dict("os.environ", self._clean_env(), clear=True),
patch("utils.env.builder.detect_gha_act", return_value=(False, False)),
patch("utils.env.handlers.infinito.inventory.run_helper", return_value=""),
):
static = {
"INFINITO_DISTRO": "debian",
"INFINITO_PULL_POLICY": "never",
}
eb = build_env(static, repo_root=Path("/repo"), comments={})
self.assertEqual(eb.values["INFINITO_DISTRO"], "debian")
self.assertEqual(eb.values["INFINITO_PULL_POLICY"], "never")
def test_propagates_static_comments(self) -> None:
with (
patch.dict("os.environ", self._clean_env(), clear=True),
patch("utils.env.builder.detect_gha_act", return_value=(False, False)),
patch("utils.env.handlers.infinito.inventory.run_helper", return_value=""),
):
static = {"INFINITO_DISTRO": "debian"}
comments = {"INFINITO_DISTRO": "selected distro"}
eb = build_env(static, repo_root=Path("/repo"), comments=comments)
self.assertEqual(eb.comments["INFINITO_DISTRO"], "selected distro")
def test_container_derived_from_distro(self) -> None:
with (
patch.dict("os.environ", self._clean_env(), clear=True),
patch("utils.env.builder.detect_gha_act", return_value=(False, False)),
patch("utils.env.handlers.infinito.inventory.run_helper", return_value=""),
):
static = {"INFINITO_DISTRO": "arch"}
eb = build_env(static, repo_root=Path("/repo"))
self.assertEqual(eb.values["INFINITO_CONTAINER"], "infinito_nexus_arch")
def test_caller_env_wins_over_static(self) -> None:
with (
patch.dict(
"os.environ",
{**self._clean_env(), "INFINITO_DISTRO": "ubuntu"},
clear=True,
),
patch("utils.env.builder.detect_gha_act", return_value=(False, False)),
patch("utils.env.handlers.infinito.inventory.run_helper", return_value=""),
):
eb = build_env({"INFINITO_DISTRO": "debian"}, repo_root=Path("/repo"))
self.assertEqual(eb.values["INFINITO_DISTRO"], "ubuntu")
self.assertEqual(eb.values["INFINITO_CONTAINER"], "infinito_nexus_ubuntu")
def test_gha_branch_skipped_locally(self) -> None:
with (
patch.dict("os.environ", self._clean_env(), clear=True),
patch("utils.env.builder.detect_gha_act", return_value=(False, False)),
patch("utils.env.handlers.infinito.inventory.run_helper", return_value=""),
):
eb = build_env(
{"INFINITO_BUILD": "1", "INFINITO_DISTRO": "debian"},
repo_root=Path("/repo"),
)
self.assertNotIn("INFINITO_GHCR_MIRROR_PREFIX", eb.values)
self.assertNotIn("INFINITO_IMAGE", eb.values)
self.assertEqual(eb.values["INFINITO_BUILD"], "1")
def test_gha_branch_emits_overrides(self) -> None:
env = {
**self._clean_env(),
"GITHUB_REPOSITORY_OWNER": "owner",
}
with (
patch.dict("os.environ", env, clear=True),
patch("utils.env.builder.detect_gha_act", return_value=(True, False)),
patch(
"utils.env.handlers.infinito.image_repository.run_helper",
return_value="repo",
),
patch("utils.env.handlers.infinito.inventory.run_helper", return_value=""),
):
static = {
"INFINITO_BUILD": "0",
"INFINITO_DISTRO": "debian",
"INFINITO_PULL_POLICY": "never",
"INFINITO_DOCKER_VOLUME": "docker",
"INFINITO_GHCR_MIRROR_PREFIX": "mirror",
"INFINITO_IMAGE_TAG": "latest",
}
eb = build_env(static, repo_root=Path("/repo"))
self.assertEqual(eb.values["INFINITO_PULL_POLICY"], "always")
self.assertEqual(eb.values["INFINITO_DOCKER_VOLUME"], "/mnt/docker")
self.assertEqual(eb.values["GITHUB_REPOSITORY_OWNER"], "owner")
self.assertIn("INFINITO_IMAGE", eb.values)
self.assertTrue(eb.values["INFINITO_IMAGE"].startswith("ghcr.io/owner/repo/"))
def test_walks_full_handler_chain(self) -> None:
"""Every handler in ORDERED_HANDLERS must be reachable; this
guards against a registry entry that never runs."""
called: list[str] = []
real_handlers = list(ORDERED_HANDLERS)
for h in real_handlers:
orig_apply = h.apply
def trace(eb, ctx, *, _orig=orig_apply, _name=h.__name__):
called.append(_name)
return _orig(eb, ctx)
h.apply = trace # type: ignore[assignment]
try:
with (
patch.dict("os.environ", self._clean_env(), clear=True),
patch("utils.env.builder.detect_gha_act", return_value=(False, False)),
patch(
"utils.env.handlers.infinito.inventory.run_helper",
return_value="",
),
):
build_env({"INFINITO_DISTRO": "debian"}, repo_root=Path("/repo"))
finally:
for h in real_handlers:
h.apply = (
h.apply.__wrapped__ if hasattr(h.apply, "__wrapped__") else h.apply
) # type: ignore[attr-defined]
self.assertEqual(set(called), {h.__name__ for h in real_handlers})
def test_returns_envbuilder_instance(self) -> None:
with (
patch.dict("os.environ", self._clean_env(), clear=True),
patch("utils.env.builder.detect_gha_act", return_value=(False, False)),
patch("utils.env.handlers.infinito.inventory.run_helper", return_value=""),
):
eb = build_env({"INFINITO_DISTRO": "debian"}, repo_root=Path("/repo"))
self.assertIsInstance(eb, EnvBuilder)
def test_passthrough_static_keys_round_trip(self) -> None:
"""Keys named by PASSTHROUGH_STATIC_KEYS that are present in
the static map must land in the builder verbatim."""
with (
patch.dict("os.environ", self._clean_env(), clear=True),
patch("utils.env.builder.detect_gha_act", return_value=(False, False)),
patch("utils.env.handlers.infinito.inventory.run_helper", return_value=""),
):
sample = {
"INFINITO_BIND_IP": "127.0.0.1",
"INFINITO_MEM_LIMIT": "0",
"INFINITO_DISTRO": "debian",
}
for k in sample:
self.assertIn(k, PASSTHROUGH_STATIC_KEYS)
eb = build_env(sample, repo_root=Path("/repo"))
self.assertEqual(eb.values["INFINITO_BIND_IP"], "127.0.0.1")
self.assertEqual(eb.values["INFINITO_MEM_LIMIT"], "0")
if __name__ == "__main__":
unittest.main()