voculinux now depends on python3-PyAudio and python3-pysocks at the carried versions, so a stock Void package below the project floors is upgraded rather than silently kept. The container smoke installs the stock packages first and asserts the upgrade.
181 lines
8.2 KiB
Python
181 lines
8.2 KiB
Python
"""Offline policy guards for the Void package smoke gate (ticket #28).
|
|
|
|
Spec #16 gives the packaged artefact the same treatment as the other
|
|
distribution formats: one documented command runs lint, build, install
|
|
and version/import smoke for every Void template from a clean checkout,
|
|
using podman and the pinned container, with no host Void install.
|
|
|
|
Seams under test (public interfaces, not internals):
|
|
|
|
- ``packaging/void/smoke.sh`` — the gate command. It runs the same release
|
|
path as a tag (``build-release.sh smoke``), so a passing gate is a release
|
|
that would build.
|
|
- The ``void-smoke`` just recipe — how release work and CI call the
|
|
gate, same shape as the other packaging gates.
|
|
- ``packaging/void/README.md`` — the documented one-command entry.
|
|
|
|
Independent sources of truth: ``packaging/void/srcpkgs/*/template``
|
|
owns the template set (a renamed or added template must not silently
|
|
drop out), ``packaging/void/tool_checksums.txt`` owns the pins, and
|
|
``packaging/void/container-release.sh`` owns the install smoke each
|
|
package must pass. The gate must agree with all of them; the container
|
|
run itself is the proof, these tests are the wiring guard.
|
|
"""
|
|
|
|
import re
|
|
from pathlib import Path
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
VOID = REPO_ROOT / "packaging" / "void"
|
|
SRC = VOID / "srcpkgs"
|
|
SMOKE_SH = VOID / "smoke.sh"
|
|
RELEASE_PATH = VOID / "build-release.sh"
|
|
CONTAINER_RELEASE = VOID / "container-release.sh"
|
|
PINS = VOID / "tool_checksums.txt"
|
|
JUSTFILE = REPO_ROOT / "justfile"
|
|
README = VOID / "README.md"
|
|
|
|
|
|
def _srcpkg_templates() -> list:
|
|
"""Every template directory under srcpkgs, in dependency build order."""
|
|
return sorted(p.name for p in SRC.iterdir() if (p / "template").is_file())
|
|
|
|
|
|
def _smoke_text() -> str:
|
|
assert SMOKE_SH.is_file(), f"missing Void smoke gate: {SMOKE_SH}"
|
|
return SMOKE_SH.read_text(encoding="utf-8")
|
|
|
|
|
|
def _gate_packages() -> list:
|
|
"""The ordered package list the gate builds, parsed from smoke.sh."""
|
|
match = re.search(r'^PKGS="([^"]+)"', _smoke_text(), re.M)
|
|
assert match, "smoke.sh must declare its ordered PKGS list"
|
|
return match.group(1).split()
|
|
|
|
|
|
def test_gate_builds_every_template_and_nothing_else() -> None:
|
|
"""A renamed or added template cannot silently drop out of the smoke."""
|
|
assert _gate_packages() and set(_gate_packages()) == set(
|
|
_srcpkg_templates()
|
|
), f"gate covers {_gate_packages()}, srcpkgs holds {_srcpkg_templates()}"
|
|
|
|
|
|
def test_gate_builds_the_app_last() -> None:
|
|
"""voculinux resolves the other templates from the local repository."""
|
|
assert (
|
|
_gate_packages()[-1] == "voculinux"
|
|
), f"the app must build last, gate order is {_gate_packages()}"
|
|
|
|
|
|
def test_gate_delegates_to_the_release_path() -> None:
|
|
"""One release path; the gate only selects the smoke mode, and its exit status is the path's."""
|
|
text = _smoke_text()
|
|
assert 'exec "$HERE/build-release.sh" smoke' in text, "smoke.sh must exec the release path"
|
|
assert "build-release.sh" in RELEASE_PATH.read_text(encoding="utf-8")
|
|
|
|
|
|
def test_gate_stays_podman_only_with_pinned_inputs() -> None:
|
|
"""Podman, never Docker; pins live in tool_checksums.txt, not the gate."""
|
|
text = _smoke_text()
|
|
assert "podman" in text, "the smoke gate must use podman"
|
|
assert "docker" not in text.lower().replace("never docker", "")
|
|
assert "ghcr.io" not in text, "the base image pin lives in tool_checksums.txt"
|
|
assert PINS.is_file(), f"missing pins file: {PINS}"
|
|
|
|
|
|
def _case_branch(label: str) -> str:
|
|
"""Shell lines of one case branch in the install phase of container-release.sh."""
|
|
text = CONTAINER_RELEASE.read_text(encoding="utf-8")
|
|
start = text.index(f"\n\t\t{label})")
|
|
end = text.index(";;", start)
|
|
return text[start:end]
|
|
|
|
|
|
def test_the_ggml_scan_belongs_to_the_whisper_binding_only() -> None:
|
|
"""Only the whisper binding ships libggml; the payload check is its own."""
|
|
assert "payload_pywhispercpp" in _case_branch("python3-pywhispercpp")
|
|
assert "payload_voculinux" not in _case_branch("python3-pywhispercpp")
|
|
text = CONTAINER_RELEASE.read_text(encoding="utf-8")
|
|
assert "libggml-cpu" in text and "libggml-vulkan" in text
|
|
|
|
|
|
def test_the_leaf_packages_are_imported_headless() -> None:
|
|
"""PyAudio, pynput and pysocks are proven by import: pynput's dummy backend needs no display."""
|
|
text = CONTAINER_RELEASE.read_text(encoding="utf-8")
|
|
assert "import pyaudio" in text
|
|
assert "PYNPUT_BACKEND=dummy" in text and "import pynput.keyboard" in text
|
|
assert "import socks" in text, "pysocks is proven by import"
|
|
|
|
|
|
def test_every_gate_package_has_an_install_recipe() -> None:
|
|
"""Each package the gate builds has a label of its own in the per-package case."""
|
|
text = CONTAINER_RELEASE.read_text(encoding="utf-8")
|
|
start = text.index('case "$PKG" in')
|
|
block = text[start : text.index("esac", start)]
|
|
labels = set()
|
|
for match in re.finditer(r"^\s*([^)\n]+)\)", block, re.M):
|
|
labels.update(part.strip() for part in match.group(1).split("|"))
|
|
for name in _gate_packages():
|
|
assert name in labels, f"container-release.sh has no install recipe for {name}"
|
|
|
|
|
|
def test_install_smoke_upgrades_the_stock_packages_below_the_floors() -> None:
|
|
"""A user who already has Void's stock audio binding and SOCKS module must get
|
|
the carried ones: the smoke installs the stock packages first, then voculinux,
|
|
then checks each one reads back as the carried pkgver."""
|
|
text = CONTAINER_RELEASE.read_text(encoding="utf-8")
|
|
stock = 'xbps-install -y --repository="$OFFICIAL" -S python3-PyAudio python3-pysocks'
|
|
assert stock in text, "the stock packages must be installed before voculinux"
|
|
assert text.index(stock) < text.index(
|
|
'xbps-install -y --repository="$XBPS_STAGE/repo" --repository="$OFFICIAL" -S voculinux'
|
|
), "the stock install must precede the voculinux install"
|
|
assert (
|
|
'xbps-query "$PKG" | grep -qx "pkgver: $PKGVER"' in text
|
|
), "each stock package must read back as the carried pkgver"
|
|
|
|
|
|
def test_stock_preinstall_matches_the_constrained_depends() -> None:
|
|
"""A carried dependency that voculinux version-constrains must be installed as
|
|
stock first and checked by pkgver. Three places name the set: the voculinux
|
|
depends, the stock pre-install line and the pkgver loop. They must agree, so a
|
|
third gap package cannot be missed in one of them."""
|
|
template = (SRC / "voculinux" / "template").read_text(encoding="utf-8")
|
|
depends = re.search(r'^depends="([^"]+)"', template, re.M).group(1).split()
|
|
constrained = {token.split(">=")[0] for token in depends if ">=" in token}
|
|
text = CONTAINER_RELEASE.read_text(encoding="utf-8")
|
|
stock = re.search(r'--repository="\$OFFICIAL" -S ([^>\n]+?) >/dev/null', text).group(1).split()
|
|
checked = re.search(r"for PKG in (python3-[^;]+); do", text).group(1).split()
|
|
assert set(stock) == constrained == set(checked), (
|
|
f"stock pre-install {stock}, constrained depends {sorted(constrained)}, "
|
|
f"pkgver loop {checked} must name the same packages"
|
|
)
|
|
|
|
|
|
def test_a_package_without_a_smoke_recipe_fails_the_release() -> None:
|
|
"""A future template falls into *) and must fail loudly, never pass mute."""
|
|
text = CONTAINER_RELEASE.read_text(encoding="utf-8")
|
|
assert "no smoke recipe for package" in _case_branch("*") or (
|
|
"no smoke recipe for package" in text
|
|
)
|
|
|
|
|
|
def test_just_recipe_calls_the_gate_like_other_packaging_gates() -> None:
|
|
"""`just void-smoke` runs the gate script with no shared-output step."""
|
|
recipe = re.search(
|
|
r"^void-smoke:([^\n]*)\n((?:[ \t].*\n|\n)+)",
|
|
JUSTFILE.read_text(encoding="utf-8"),
|
|
re.MULTILINE,
|
|
)
|
|
assert recipe, "justfile must define a void-smoke recipe"
|
|
assert not recipe.group(1).strip(), "void-smoke must not take a shared-output prerequisite"
|
|
assert "packaging/void/smoke.sh" in recipe.group(
|
|
2
|
|
), "void-smoke must run packaging/void/smoke.sh"
|
|
|
|
|
|
def test_readme_documents_the_one_command_gate() -> None:
|
|
"""AC1: one documented command from a clean checkout, no host Void."""
|
|
text = README.read_text(encoding="utf-8")
|
|
assert "void-smoke" in text or "smoke.sh" in text, "README must document the smoke gate command"
|
|
assert "podman" in text.lower(), "README must state the gate uses podman"
|