Skip to content

matrix

matrix

Multi-distro test-matrix engine shared by the terok-* siblings.

Historically each sibling carried its own ~500-line run-matrix.sh plus a set of Containerfiles, ~90% identical across repos and already drifting. This package is the single engine; a consuming repo keeps only a declarative tests/containers/matrix.yml and (optionally) per-slot Containerfile fragments.

Collaborators, in reading order:

  • catalog — facts about the shared slot images (expected podman versions, non-systemd slots, slot kinds);
  • config — the matrix.yml schema and loader (MatrixConfig);
  • contract — the consumer conftest's half of the TEROK_EXPECT capability contract (check_capability_contract);
  • inner — generates the scripts that run inside a slot's test container;
  • runner — host-side build/run/prune;
  • cli — the terok-matrix entry point (uv run terok-matrix from a consuming repo's root).

External tooling (the superbuild TUI, CI matrix generation) should not shell-parse anything: load the same matrix.yml via load_config or ask the CLI (terok-matrix --slots-json / --image-prefix).

SLOTS = {'debian12': SlotSpec(expected_podman='4.3.1'), 'ubuntu2404': SlotSpec(expected_podman='4.9.3'), 'ubuntu2604': SlotSpec(expected_podman='5.7.0'), 'debian13': SlotSpec(expected_podman='5.4.2'), 'fedora43': SlotSpec(expected_podman='5.8.4'), 'fedora44': SlotSpec(expected_podman='5.8.4'), 'podman': SlotSpec(expected_podman='5.8.4', user='podman'), 'alpine': SlotSpec(expected_podman='5.3.2', non_systemd=True, pasta_symlink=True), 'void': SlotSpec(expected_podman='5.8.3', non_systemd=True, pasta_symlink=True), 'mageia': SlotSpec(expected_podman='4.9.5'), 'manjaro': SlotSpec(expected_podman='6.1.0', pasta_symlink=True), 'nix': SlotSpec(kind=SlotKind.NIX), 'nixos': SlotSpec(expected_podman='5.8.7', requires_boot=True, boot_init='/init', bash_path='/run/current-system/sw/bin/bash')} module-attribute

__all__ = ['SLOTS', 'MatrixConfig', 'MatrixConfigError', 'SlotKind', 'SlotSpec', 'binary_on_path', 'check_capability_contract', 'load_config', 'tcp_reachable'] module-attribute

SlotKind

Bases: StrEnum

How a slot's test container is driven.

CONTAINER = 'container' class-attribute instance-attribute

NIX = 'nix' class-attribute instance-attribute

SlotSpec(expected_podman='latest', non_systemd=False, user='testrunner', kind=SlotKind.CONTAINER, pasta_symlink=False, requires_boot=False, boot_init=SYSTEMD_INIT, bash_path='/bin/bash') dataclass

Facts about one matrix slot's base image.

Parameters:

Name Type Description Default
expected_podman str

Distro-shipped podman version the slot is pinned to; "latest" for rolling images (and for slot kinds that never read it, like nix).

'latest'
non_systemd bool

The runner hard-fails the slot if systemd is present — these slots exist to prove the systemd-free floor.

False
user str

Non-root user baked into the image (uid 1000).

'testrunner'
kind SlotKind

Driving mode, see SlotKind.

CONTAINER
pasta_symlink bool

The distro ships /usr/bin/pasta as a symlink to passt (upstream's default; Debian hard-links, Fedora builds it apart). AppArmor attaches profiles by the resolved path and is not container-namespaced, so on a host with the distro passt profile the nested pasta runs under that profile and loses its netns — the runner skips such slots there.

False
requires_boot bool

The image needs its normal boot to provision runtime state (NixOS security wrappers); skip it outside --krun.

False
boot_init str

Image entry point for a booted slot.

SYSTEMD_INIT
bash_path str

Bash path for scripts and systemd units in this image.

'/bin/bash'

expected_podman = 'latest' class-attribute instance-attribute

non_systemd = False class-attribute instance-attribute

user = 'testrunner' class-attribute instance-attribute

kind = SlotKind.CONTAINER class-attribute instance-attribute

requires_boot = False class-attribute instance-attribute

boot_init = SYSTEMD_INIT class-attribute instance-attribute

bash_path = '/bin/bash' class-attribute instance-attribute

runs_nested_podman(flavor)

Whether this slot runs nested rootless podman under flavor.

True only for a container-kind slot on the podman flavor: the nix slot has no podman inside, and the dbus flavor runs none. This gates the resolv.conf fix and, under krun, the device setup and store binds.

Source code in src/terok_util/matrix/catalog.py
def runs_nested_podman(self, flavor: str) -> bool:
    """Whether this slot runs nested rootless podman under *flavor*.

    True only for a container-kind slot on the ``podman`` flavor: the nix
    slot has no podman inside, and the dbus flavor runs none.  This gates
    the resolv.conf fix and, under krun, the device setup and store binds.
    """
    return flavor == "podman" and self.kind is SlotKind.CONTAINER

may_boot_systemd(flavor, krun)

Whether this slot boots systemd as PID 1, where its image ships one.

Only under krun: the microVM owns its kernel and cgroup tree, so the image's systemd can be PID 1 and give the tests a real per-user manager. Under a shared kernel the slot stays a plain --init container, and the systemd-free floor slots stay systemd-free everywhere. NixOS requires boot even on the dbus flavor. Otherwise the runner checks for the image's own systemd and system bus.

Source code in src/terok_util/matrix/catalog.py
def may_boot_systemd(self, flavor: str, krun: bool) -> bool:
    """Whether this slot boots systemd as PID 1, where its image ships one.

    Only under krun: the microVM owns its kernel and cgroup tree, so the
    image's systemd can be PID 1 and give the tests a real per-user
    manager.  Under a shared kernel the slot stays a plain ``--init``
    container, and the systemd-free floor slots stay systemd-free
    everywhere.  NixOS requires boot even on the dbus flavor.  Otherwise
    the runner checks for the image's own systemd and system bus.
    """
    return krun and (
        self.requires_boot or (self.runs_nested_podman(flavor) and not self.non_systemd)
    )

MatrixConfig(image_prefix, flavor, groups, expect, slots, phases, containers_dir, repo_root, krun=False, host_confines_pasta=False) dataclass

A repo's whole matrix declaration, resolved and validated.

Parameters:

Name Type Description Default
image_prefix str

Image/container name prefix and prune-label value.

required
flavor str

Shared Containerfile family (podman or dbus).

required
groups tuple[str, ...]

Dependency groups synced before testing.

required
expect tuple[str, ...]

TEROK_EXPECT capability contract; empty = not exported.

required
slots dict[str, SlotConfig]

Selected slots in declaration order.

required
phases tuple[Phase, ...]

Repo-level test flow.

required
containers_dir Path

Directory of the matrix.yml (fragments live here).

required
repo_root Path

Build context and bind-mounted source tree.

required
host_confines_pasta bool

The host's AppArmor passt profile is the distro's, so a nested pasta reached through a symlink loses its netns; slots with such a pasta are skipped. Probed once in main; never set under krun.

False
krun bool

Run each slot as a libkrun microVM (its own kernel) instead of a shared-kernel container. A run-mode set from --krun, not from matrix.yml — the CLI injects it after the declaration loads.

False

image_prefix instance-attribute

flavor instance-attribute

groups instance-attribute

expect instance-attribute

slots instance-attribute

phases instance-attribute

containers_dir instance-attribute

repo_root instance-attribute

krun = False class-attribute instance-attribute

host_confines_pasta = False class-attribute instance-attribute

slot_groups(name)

Dependency groups effective for slot name.

Source code in src/terok_util/matrix/config.py
def slot_groups(self, name: str) -> tuple[str, ...]:
    """Dependency groups effective for slot ``name``."""
    override = self.slots[name].groups
    return self.groups if override is None else override

slot_expect(name)

Capability contract effective for slot name.

Source code in src/terok_util/matrix/config.py
def slot_expect(self, name: str) -> tuple[str, ...]:
    """Capability contract effective for slot ``name``."""
    override = self.slots[name].expect
    return self.expect if override is None else override

slot_phases(name)

Test flow effective for slot name.

Source code in src/terok_util/matrix/config.py
def slot_phases(self, name: str) -> tuple[Phase, ...]:
    """Test flow effective for slot ``name``."""
    override = self.slots[name].phases
    return self.phases if override is None else override

MatrixConfigError

Bases: ValueError

A matrix.yml failed validation.

load_config(path)

Load and validate a matrix.yml.

Parameters:

Name Type Description Default
path Path

The config file; its grandparent directory is taken as the repo root (<root>/tests/containers/matrix.yml).

required

Raises:

Type Description
MatrixConfigError

On unknown keys, unknown slots, or a phase that is neither a command phase nor a pytest phase.

Source code in src/terok_util/matrix/config.py
def load_config(path: Path) -> MatrixConfig:
    """Load and validate a ``matrix.yml``.

    Args:
        path: The config file; its grandparent directory is taken as the
            repo root (``<root>/tests/containers/matrix.yml``).

    Raises:
        MatrixConfigError: On unknown keys, unknown slots, or a phase that
            is neither a command phase nor a pytest phase.
    """
    raw = yaml_load(path.read_text(encoding="utf-8"))
    if not isinstance(raw, dict):
        raise MatrixConfigError(f"{path}: expected a YAML mapping at top level")

    data = _Section(dict(raw), where=str(path))
    flavor = data.string("flavor")
    if flavor not in FLAVORS:
        raise MatrixConfigError(f"{path}: unknown flavor '{flavor}'; known: {list(FLAVORS)}")
    config = MatrixConfig(
        image_prefix=data.string("image-prefix"),
        flavor=flavor,
        groups=data.strings("groups", default=("test",)),
        expect=data.strings("expect", default=()),
        slots=_parse_slots(data.mapping("slots"), where=str(path)),
        phases=_parse_phases(data.list_of_maps("phases"), where=str(path)),
        containers_dir=path.parent.resolve(),
        repo_root=path.parent.parent.parent.resolve(),
    )
    data.reject_leftovers()
    return config

binary_on_path(name)

Whether the launching environment can actually discover name.

Source code in src/terok_util/matrix/contract.py
def binary_on_path(name: str) -> bool:
    """Whether the launching environment can actually discover *name*."""
    return find_host_tool(name) is not None

check_capability_contract(probes)

Verify TEROK_EXPECT against probes; return the failure, if any.

Parameters:

Name Type Description Default
probes Mapping[str, Callable[[], bool]]

Capability name to presence probe, the repo-specific half of the contract.

required

Returns:

Type Description
str | None

A human-readable failure message when the contract names unknown

str | None

capabilities or a declared capability is missing; None when

str | None

the contract holds (or none is declared — dev-machine runs).

Source code in src/terok_util/matrix/contract.py
def check_capability_contract(probes: Mapping[str, Callable[[], bool]]) -> str | None:
    """Verify ``TEROK_EXPECT`` against *probes*; return the failure, if any.

    Args:
        probes: Capability name to presence probe, the repo-specific half
            of the contract.

    Returns:
        A human-readable failure message when the contract names unknown
        capabilities or a declared capability is missing; ``None`` when
        the contract holds (or none is declared — dev-machine runs).
    """
    expected = {c for c in os.environ.get(EXPECT_ENV, "").split(",") if c}
    if not expected:
        return None
    unknown = expected - probes.keys()
    if unknown:
        return f"{EXPECT_ENV} names unknown capabilities: {sorted(unknown)}"
    missing = sorted(cap for cap in expected if not probes[cap]())
    if missing:
        return "matrix capability contract broken - expected but missing: " + ", ".join(missing)
    return None

tcp_reachable(ip, port, timeout=5.0)

Whether a TCP connection to ip:port succeeds within timeout.

Source code in src/terok_util/matrix/contract.py
def tcp_reachable(ip: str, port: int, timeout: float = 5.0) -> bool:
    """Whether a TCP connection to ``ip:port`` succeeds within *timeout*."""
    try:
        with socket.create_connection((ip, port), timeout=timeout):
            return True
    except OSError:
        return False