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>
This commit is contained in:
kevinveenbirkenbachandClaude Opus 5 committed 2026-07-28 16:15:59 +02:00
1 parent cf82841be8
commit 8aba198aec
16 files changed
+480 -18

No files matched your search

+20
View File
@@ -767,6 +767,26 @@ test-unit: install
INFINITO_COMPILE=0 \
bash scripts/tests/code/wrapper.sh
.PHONY: worktree-down
# Stop a branch worktree's stack, release the checkout and free its slot.
# Usage: make worktree-down branch=<name> [base=<dir>] [force=true]
# Param branch: branch the worktree was created for (required).
# Param base: parent directory the worktree lives in (default /tmp).
# Param force: true discards uncommitted changes in the worktree.
worktree-down:
@test -n '$(branch)' || { echo 'usage: make worktree-down branch=<name> [base=<dir>] [force=true]'; exit 2; }
@bash scripts/system/worktree/down.sh '$(branch)' '$(or $(base),/tmp)' '$(or $(force),false)'
.PHONY: worktree-up
# Check a branch out into an isolated worktree with its own subnet, ports and container names.
# Usage: make worktree-up branch=<name> [base=<dir>]
# Note: the worktree shares the primary checkout's cache stack instead of starting its own.
# Param branch: branch to check out (required).
# Param base: parent directory for the worktree (default /tmp).
worktree-up:
@test -n '$(branch)' || { echo 'usage: make worktree-up branch=<name> [base=<dir>]'; exit 2; }
@bash scripts/system/worktree/up.sh '$(branch)' '$(or $(base),/tmp)'
.PHONY: wsl2-dns-setup
# Set up DNS on WSL2.
wsl2-dns-setup:
+2
View File
@@ -135,5 +135,7 @@ alias i8tenu='m test-unit'
alias i8tesi='m test-signed'
alias i8texe='m test-external'
alias i8wo='m wsl2-dns-setup'
alias i8wodo='m worktree-down'
alias i8wose='m wsl2-systemd-check'
alias i8wotu='m wsl2-trust-windows'
alias i8woup='m worktree-up'
@@ -190,7 +190,7 @@ class Compose:
args: list[str] = ["up", "-d"]
if no_build:
args.append("--no-build")
if self.profile.registry_cache_active():
if self.profile.owns_cache_stack():
self._generate_package_frontend_certs(env)
args += ["registry-cache", "package-cache", "package-cache-frontend"]
args += ["coredns", "infinito"]
@@ -199,8 +199,9 @@ class Compose:
self.wait_for_healthy()
if self.profile.registry_cache_active():
if self.profile.owns_cache_stack():
self._bootstrap_package_cache(env)
if self.profile.registry_cache_active():
self._install_package_frontend_ca_in_runner()
if run_entry_init:
+13 -2
View File
@@ -8,12 +8,23 @@ VALID_DISTROS: tuple[str, ...] = distro_names()
def compose_file_args() -> list[str]:
"""Compose `-f` flags shared by up and down flows."""
"""Compose `-f` flags shared by up and down flows.
Each override is gated on the resource it needs being present, not on the
instance slot: an override whose `:?` guard is unsatisfied would abort the
stack outright, so a checkout that was never handed a shared git dir or
cache network simply does not layer that file in.
"""
from .profile import Profile
profile = Profile()
out = ["-f", "compose.yml"]
if Profile().registry_cache_active():
if profile.shared_git_dir():
out += ["-f", "compose/worktree.override.yml"]
if profile.registry_cache_active():
out += ["-f", "compose/cache.override.yml"]
if profile.shared_cache_network():
out += ["-f", "compose/cache.shared.override.yml"]
if (os.environ.get("INFINITO_PUBLISH_PORTS") or "").strip().lower() == "false":
out += ["-f", "compose/noports.override.yml"]
return out
@@ -19,3 +19,25 @@ class Profile:
def registry_cache_active(self) -> bool:
"""True iff the cache stack should be loaded (local dev only)."""
return not self.is_ci()
def instance(self) -> int:
"""Slot index of this checkout; 0 is the primary one.
Bookkeeping only: the compose layering below keys off the concrete
resources a worktree was handed, never off this number, so a stray
slot cannot make compose demand a value nobody set.
"""
raw = (os.environ.get("INFINITO_INSTANCE") or "0").strip()
return int(raw) if raw.isdigit() else 0
def shared_git_dir(self) -> str:
"""Path of the checkout's shared .git directory, empty when it owns one."""
return (os.environ.get("INFINITO_GIT_COMMON_DIR") or "").strip()
def shared_cache_network(self) -> str:
"""Docker network of a cache stack owned elsewhere, empty when unset."""
return (os.environ.get("INFINITO_CACHE_NETWORK") or "").strip()
def owns_cache_stack(self) -> bool:
"""True iff this instance runs the cache stack rather than sharing it."""
return self.registry_cache_active() and not self.shared_cache_network()
+18
View File
@@ -0,0 +1,18 @@
---
# Layered on cache.override.yml for secondary instances, which reach the
# primary instance's cache stack instead of running one of their own.
# See docs/contributing/environment/cache.md.
services:
infinito:
# Exception: required:false still makes compose CREATE the cache services,
# which would claim the primary instance's IPs outside this subnet.
depends_on: !reset null
networks:
cache:
networks:
cache:
external: true
name: ${INFINITO_CACHE_NETWORK:?Set by 'make worktree-up' from the running
cache stack; empty on the primary instance}
+12
View File
@@ -0,0 +1,12 @@
---
# Layered on compose.yml for secondary instances. A git worktree's .git is a
# file pointing at an absolute path inside the primary checkout, so git only
# works in the container when that directory is mounted where the pointer
# expects it.
services:
infinito:
volumes:
- ${INFINITO_GIT_COMMON_DIR:?Set by 'make worktree-up' from git rev-parse
--git-common-dir}:${INFINITO_GIT_COMMON_DIR:?Set by 'make worktree-up'
from git rev-parse --git-common-dir}
+9
View File
@@ -34,6 +34,9 @@ INFINITO_BUILD=1
# `1` keeps BuildKit on (default); `0` forces the compose wrapper to use the legacy builder.
INFINITO_BUILDKIT_ENABLED=1
# External docker network carrying the shared cache stack; empty on the primary instance, which owns the stack itself.
INFINITO_CACHE_NETWORK=
# Bind-mounted apt sources.list fragment for the package-cache frontend.
INFINITO_CACHE_PACKAGE_APT_LIST=./compose/package-cache/apt.list
@@ -139,12 +142,18 @@ INFINITO_GATEWAY=172.30.0.1
# Prefix path under which GHCR mirror lookups search for cached images.
INFINITO_GHCR_MIRROR_PREFIX=mirror
# Shared .git directory a worktree checkout points at; empty on the primary instance, which carries its own.
INFINITO_GIT_COMMON_DIR=
# Run-context flag like INFINITO_DEPLOY_MODE: the single-node workflow sets 'true' so host discovery drops compose-covered roles.
INFINITO_HOST_EXCLUDE_COMPOSE=
# Image tag used to address the published infinito image (CI publishes 'latest' on push).
INFINITO_IMAGE_TAG=latest
# Slot index isolating a parallel checkout: 0 owns the cache stack, 1..255 shift subnet, bind IP and container names.
INFINITO_INSTANCE=0
# Repo-relative path to the Ansible vars file `infinito administration inventory provision --vars-file <...>` consumes.
INFINITO_INVENTORY_VARS_FILE=inventories/development/default.yml
+51
View File
@@ -0,0 +1,51 @@
#!/usr/bin/env bash
# shellcheck shell=bash
#
# Tear down the worktree a branch was checked out into: stop its compose
# stack, then release the checkout and free its slot. Refuses to drop a
# worktree with uncommitted changes unless force=true is passed.
#
# Use `make worktree-down` rather than calling this script directly.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)"
# shellcheck source=scripts/system/worktree/lib.sh
source "${SCRIPT_DIR}/lib.sh"
branch="${1:?branch name required}"
base="${2:-/tmp}"
force="${3:-false}"
cd "${REPO_ROOT}"
slug="$(worktree_slug "${branch}")"
path="$(worktree_path "${slug}" "${base}")"
if [ ! -d "${path}" ]; then
echo "FAILURE: no worktree at ${path} for branch '${branch}'" >&2
exit 1
fi
if [ "${force}" != "true" ] && [ -n "$(git -C "${path}" status --porcelain)" ]; then
echo "FAILURE: ${path} has uncommitted changes; commit them or pass force=true" >&2
git -C "${path}" status --short >&2
exit 1
fi
slot="$(worktree_slot_of "${path}")"
echo ">>> Stopping the compose stack in ${path} (slot ${slot})"
if ! make -C "${path}" compose-down; then
echo ">>> WARNING: compose-down failed; removing the worktree anyway"
fi
echo ">>> Removing worktree ${path}"
if [ "${force}" = "true" ]; then
git worktree remove --force "${path}"
else
git worktree remove "${path}"
fi
echo "Slot ${slot} released."
+68
View File
@@ -0,0 +1,68 @@
#!/usr/bin/env bash
# shellcheck shell=bash
#
# Shared helpers for the worktree slot manager. A slot is an integer that
# isolates one checkout from every other: it shifts the compose subnet, the
# host bind IP and the swarm lab subnet, so several worktrees can run their
# stacks side by side. Slot 0 is the primary checkout and owns the shared
# cache stack; every worktree gets the next free slot and joins that cache
# over an external docker network.
#
# Volumes are deliberately not slotted: compose already namespaces them by
# project, and INFINITO_DOCKER_VOLUME names a top-level compose key, so
# renaming it would point the mount at an undeclared volume.
#
# The pins are written to custom.env AND passed to the generator in a cleared
# environment: custom.env keeps them reproducible across later `make dotenv`
# runs, while the explicit env covers branches whose generator predates
# custom.env support. Inheriting the caller's environment would leak the
# invoking worktree's own slot into the new one.
#
# Slot discovery reads custom.env first: a branch whose default.env predates
# INFINITO_INSTANCE never materialises the key into .env, which would make the
# checkout look like slot 0 and hand its number out twice.
#
# Sourced by up.sh and down.sh; not meant to be executed directly.
WORKTREE_DIR_PREFIX="infinito-wt-"
worktree_slug() {
printf '%s' "$1" | sed -e 's#[^a-zA-Z0-9._-]#-#g' -e 's#^[^a-zA-Z0-9]*##' -e 's#[-.]*$##'
}
worktree_path() {
printf '%s/%s%s' "$2" "${WORKTREE_DIR_PREFIX}" "$1"
}
worktree_slot_of() {
local slot=0 file
for file in "$1/custom.env" "$1/.env"; do
if [ -f "${file}" ]; then
slot="$(awk -F= '/^INFINITO_INSTANCE=/{print $2}' "${file}" | tail -n1)"
if [ -n "${slot}" ]; then
break
fi
fi
done
case "${slot}" in
'' | *[!0-9]*) slot=0 ;;
esac
printf '%s' "${slot}"
}
worktree_next_slot() {
local max=0 path slot
while read -r path; do
slot="$(worktree_slot_of "${path}")"
if [ "${slot}" -gt "${max}" ]; then
max="${slot}"
fi
done < <(git worktree list --porcelain | awk '/^worktree /{print $2}')
printf '%s' "$((max + 1))"
}
worktree_cache_network() {
docker inspect infinito-registry-cache \
--format '{{range $k, $v := .NetworkSettings.Networks}}{{$k}}{{"\n"}}{{end}}' 2>/dev/null |
head -n1
}
+102
View File
@@ -0,0 +1,102 @@
#!/usr/bin/env bash
# shellcheck shell=bash
#
# Create an isolated git worktree for a branch and pin it to a free slot, so
# its compose stack no longer collides with the primary checkout on subnet,
# host ports, container names or volumes. The cache stack is not duplicated:
# the worktree joins the running one over an external network.
#
# Use `make worktree-up` rather than calling this script directly.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)"
# shellcheck source=scripts/system/worktree/lib.sh
source "${SCRIPT_DIR}/lib.sh"
branch="${1:?branch name required}"
base="${2:-/tmp}"
max_slot=255
cd "${REPO_ROOT}"
slug="$(worktree_slug "${branch}")"
if [ -z "${slug}" ]; then
echo "FAILURE: branch '${branch}' slugs to an empty name" >&2
exit 1
fi
path="$(worktree_path "${slug}" "${base}")"
if [ -e "${path}" ]; then
echo "FAILURE: ${path} already exists; run 'make worktree-down branch=${branch}' first" >&2
exit 1
fi
slot="$(worktree_next_slot)"
if [ "${slot}" -gt "${max_slot}" ]; then
echo "FAILURE: no free slot left (max ${max_slot}); tear down an existing worktree" >&2
exit 1
fi
cache_network="$(worktree_cache_network)"
if [ -z "${cache_network}" ]; then
echo ">>> WARNING: no running cache stack found; the worktree will start without a shared cache"
fi
echo ">>> Creating worktree for '${branch}' at ${path} (slot ${slot})"
git worktree add "${path}" "${branch}"
git_common_dir="$(git rev-parse --path-format=absolute --git-common-dir)"
echo ">>> Pinning slot ${slot} in ${path}/custom.env"
cat >"${path}/custom.env" <<PINS
INFINITO_INSTANCE=${slot}
INFINITO_GIT_COMMON_DIR=${git_common_dir}
INFINITO_SUBNET=172.30.${slot}.0/24
INFINITO_GATEWAY=172.30.${slot}.1
INFINITO_DNS_IP=172.30.${slot}.53
INFINITO_IP4=172.30.${slot}.10
INFINITO_BIND_IP=127.0.0.$((slot + 1))
INFINITO_SWARM_LAB_SUBNET=192.168.$((244 + slot)).0/24
INFINITO_RUNNER_PREFIX=infinito-${slug}
INFINITO_CONTAINER=infinito_nexus_${slug//[.-]/_}
INFINITO_CACHE_NETWORK=${cache_network}
PINS
echo ">>> Generating .env from the pins"
env -i -C "${path}" \
HOME="${HOME}" \
PATH="${PATH}" \
INFINITO_INSTANCE="${slot}" \
INFINITO_GIT_COMMON_DIR="${git_common_dir}" \
INFINITO_SUBNET="172.30.${slot}.0/24" \
INFINITO_GATEWAY="172.30.${slot}.1" \
INFINITO_DNS_IP="172.30.${slot}.53" \
INFINITO_IP4="172.30.${slot}.10" \
INFINITO_BIND_IP="127.0.0.$((slot + 1))" \
INFINITO_SWARM_LAB_SUBNET="192.168.$((244 + slot)).0/24" \
INFINITO_RUNNER_PREFIX="infinito-${slug}" \
INFINITO_CACHE_NETWORK="${cache_network}" \
python3 -m cli.meta.env
cat <<SUMMARY
Worktree ready.
branch ${branch}
path ${path}
slot ${slot}
subnet 172.30.${slot}.0/24
bind IP 127.0.0.$((slot + 1))
container infinito_nexus_${slug//[.-]/_}
cache net ${cache_network:-<none>}
Next:
cd ${path}
make compose-up
Release with:
make worktree-down branch=${branch}
SUMMARY
+6
View File
@@ -188,6 +188,12 @@ class TestDotenvSmoke(unittest.TestCase):
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
@@ -12,8 +12,6 @@ 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)
@@ -22,7 +20,6 @@ class TestComposeUpRetries(unittest.TestCase):
) -> None:
compose = self._compose()
# Fail 2 times, then succeed.
compose.run = MagicMock(
side_effect=[
subprocess.CalledProcessError(1, ["docker", "compose"], "out1", "err1"),
@@ -42,13 +39,10 @@ class TestComposeUpRetries(unittest.TestCase):
]
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)
@@ -57,7 +51,6 @@ class TestComposeUpRetries(unittest.TestCase):
) -> None:
compose = self._compose()
# Always fail.
compose.run = MagicMock(
side_effect=subprocess.CalledProcessError(
1, ["docker", "compose"], "out", "err"
@@ -69,10 +62,8 @@ class TestComposeUpRetries(unittest.TestCase):
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))
@@ -101,6 +92,8 @@ class TestComposeUpRetries(unittest.TestCase):
"GITHUB_ACTIONS": "true",
"INFINITO_RUNNING_ON_GITHUB": "true",
"CI": "true",
"INFINITO_GIT_COMMON_DIR": "",
"INFINITO_CACHE_NETWORK": "",
},
clear=False,
)
@@ -125,7 +118,6 @@ class TestComposeUpRetries(unittest.TestCase):
cmd = run_mock.call_args.args[0]
env = run_mock.call_args.kwargs["env"]
# CI: cache override is not layered in.
self.assertEqual(
cmd,
[
@@ -148,6 +140,8 @@ class TestComposeUpRetries(unittest.TestCase):
"GITHUB_ACTIONS": "",
"INFINITO_RUNNING_ON_GITHUB": "",
"CI": "",
"INFINITO_GIT_COMMON_DIR": "",
"INFINITO_CACHE_NETWORK": "",
},
clear=False,
)
@@ -164,7 +158,6 @@ class TestComposeUpRetries(unittest.TestCase):
cmd = run_mock.call_args.args[0]
env = run_mock.call_args.kwargs["env"]
# Local: cache override is layered onto compose.yml.
self.assertEqual(
cmd,
[
@@ -195,7 +188,6 @@ class TestComposeUpRetries(unittest.TestCase):
"INFINITO_BUILD": "1",
"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",
@@ -258,6 +250,8 @@ class TestComposeUpRetries(unittest.TestCase):
"CI": "",
"GITHUB_ACTIONS": "",
"INFINITO_RUNNING_ON_GITHUB": "",
"INFINITO_GIT_COMMON_DIR": "",
"INFINITO_CACHE_NETWORK": "",
},
clear=False,
)
@@ -272,7 +266,6 @@ class TestComposeUpRetries(unittest.TestCase):
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(
[
"up",
@@ -0,0 +1,102 @@
"""Unit tests for compose_file_args.
Pins which override files each runtime context layers on compose.yml. Every
override is gated on the resource it needs rather than on the instance slot,
because an unsatisfied `:?` guard inside one of them aborts the whole stack.
"""
from __future__ import annotations
import os
import unittest
from unittest.mock import patch
from cli.administration.deploy.development.env import compose_file_args
_LOCAL_PRIMARY = {
"GITHUB_ACTIONS": "",
"INFINITO_RUNNING_ON_GITHUB": "",
"CI": "",
"INFINITO_INSTANCE": "0",
"INFINITO_GIT_COMMON_DIR": "",
"INFINITO_CACHE_NETWORK": "",
"INFINITO_PUBLISH_PORTS": "",
}
_WORKTREE = {
**_LOCAL_PRIMARY,
"INFINITO_INSTANCE": "2",
"INFINITO_GIT_COMMON_DIR": "/repo/.git",
"INFINITO_CACHE_NETWORK": "primary_default",
}
class TestComposeFileArgs(unittest.TestCase):
@patch.dict(os.environ, _LOCAL_PRIMARY, clear=False)
def test_primary_instance_loads_the_cache_stack(self) -> None:
self.assertEqual(
compose_file_args(),
["-f", "compose.yml", "-f", "compose/cache.override.yml"],
)
@patch.dict(os.environ, _WORKTREE, clear=False)
def test_worktree_adds_the_git_and_shared_cache_overrides(self) -> None:
self.assertEqual(
compose_file_args(),
[
"-f",
"compose.yml",
"-f",
"compose/worktree.override.yml",
"-f",
"compose/cache.override.yml",
"-f",
"compose/cache.shared.override.yml",
],
)
@patch.dict(os.environ, {**_LOCAL_PRIMARY, "INFINITO_INSTANCE": "5"}, clear=False)
def test_a_stray_slot_alone_changes_nothing(self) -> None:
self.assertEqual(
compose_file_args(),
["-f", "compose.yml", "-f", "compose/cache.override.yml"],
)
@patch.dict(os.environ, {**_LOCAL_PRIMARY, "INFINITO_GIT_COMMON_DIR": "/repo/.git"})
def test_a_shared_git_dir_alone_adds_only_the_worktree_override(self) -> None:
self.assertEqual(
compose_file_args(),
[
"-f",
"compose.yml",
"-f",
"compose/worktree.override.yml",
"-f",
"compose/cache.override.yml",
],
)
@patch.dict(os.environ, {**_WORKTREE, "CI": "true"}, clear=False)
def test_ci_keeps_the_git_mount_but_drops_every_cache_override(self) -> None:
self.assertEqual(
compose_file_args(),
["-f", "compose.yml", "-f", "compose/worktree.override.yml"],
)
@patch.dict(os.environ, {**_LOCAL_PRIMARY, "CI": "true"}, clear=False)
def test_ci_loads_no_override_at_all(self) -> None:
self.assertEqual(compose_file_args(), ["-f", "compose.yml"])
@patch.dict(
os.environ,
{**_LOCAL_PRIMARY, "INFINITO_PUBLISH_PORTS": "false"},
clear=False,
)
def test_unpublished_ports_append_the_noports_override(self) -> None:
self.assertEqual(
compose_file_args()[-2:], ["-f", "compose/noports.override.yml"]
)
if __name__ == "__main__": # pragma: no cover
unittest.main()
@@ -8,11 +8,13 @@ from unittest.mock import patch
from cli.administration.deploy.development.profile import Profile
# Blank explicit signals; clear=True would unset PATH.
_BLANK_CI_ENV = {
"GITHUB_ACTIONS": "",
"INFINITO_RUNNING_ON_GITHUB": "",
"CI": "",
"INFINITO_INSTANCE": "0",
"INFINITO_GIT_COMMON_DIR": "",
"INFINITO_CACHE_NETWORK": "",
}
@@ -73,5 +75,45 @@ class TestProfileRegistryCacheActive(unittest.TestCase):
self.assertEqual(p.registry_cache_active(), not p.is_ci())
class TestProfileInstance(unittest.TestCase):
@patch.dict(os.environ, {**_BLANK_CI_ENV, "INFINITO_INSTANCE": ""}, clear=False)
def test_unset_instance_is_primary(self) -> None:
self.assertEqual(Profile().instance(), 0)
@patch.dict(os.environ, {**_BLANK_CI_ENV, "INFINITO_INSTANCE": "7"}, clear=False)
def test_numeric_instance_is_parsed(self) -> None:
self.assertEqual(Profile().instance(), 7)
@patch.dict(os.environ, {**_BLANK_CI_ENV, "INFINITO_INSTANCE": " 3 "}, clear=False)
def test_surrounding_whitespace_is_tolerated(self) -> None:
self.assertEqual(Profile().instance(), 3)
@patch.dict(os.environ, {**_BLANK_CI_ENV, "INFINITO_INSTANCE": "abc"}, clear=False)
def test_garbage_falls_back_to_primary(self) -> None:
self.assertEqual(Profile().instance(), 0)
class TestProfileOwnsCacheStack(unittest.TestCase):
@patch.dict(os.environ, _BLANK_CI_ENV, clear=False)
def test_owns_the_stack_when_no_network_was_handed_over(self) -> None:
self.assertTrue(Profile().owns_cache_stack())
@patch.dict(
os.environ, {**_BLANK_CI_ENV, "INFINITO_CACHE_NETWORK": "primary_default"}
)
def test_shares_instead_of_owning_when_a_network_is_named(self) -> None:
p = Profile()
self.assertFalse(p.owns_cache_stack())
self.assertTrue(p.registry_cache_active())
@patch.dict(os.environ, {**_BLANK_CI_ENV, "INFINITO_INSTANCE": "7"}, clear=False)
def test_a_stray_slot_alone_does_not_give_the_stack_away(self) -> None:
self.assertTrue(Profile().owns_cache_stack())
@patch.dict(os.environ, {**_BLANK_CI_ENV, "CI": "true"}, clear=False)
def test_ci_never_owns_the_stack(self) -> None:
self.assertFalse(Profile().owns_cache_stack())
if __name__ == "__main__": # pragma: no cover
unittest.main()
+3
View File
@@ -88,6 +88,9 @@ STATIC_KEYS = (
"INFINITO_RESCUE_DIAGNOSTICS_BASE",
"INFINITO_RUNNER_PREFIX",
"INFINITO_TIMEOUT_MULTIPLIER",
"INFINITO_INSTANCE",
"INFINITO_GIT_COMMON_DIR",
"INFINITO_CACHE_NETWORK",
"INFINITO_CACHE_REGISTRY_HOST_PATH",
"INFINITO_CACHE_REGISTRY_CA_HOST_PATH",
"INFINITO_CACHE_REGISTRY_IP",