- build-release.sh: POSIX-only checks (dash runs the release), a release form is exactly X.Y.Z or X.Y.Z-alpha|beta|rcN, and the install smoke refuses a package that has no smoke recipe. - Tests: names and tag gate (exact match, committed template must agree), key refusals before any container starts (fake podman records calls), the render script pins only the build tree, podman wiring (only the sign phase sees the key, and it has no network), the xbps ordering of pre-release spellings, the workflow gates, tmpfs key staging, and the publish set (sdist, packages, signatures, repodata, client key).
136 lines
5.7 KiB
Python
136 lines
5.7 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 and pynput 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
|
|
|
|
|
|
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"
|