Merge feature/28 into integration/spec16 (ticket #28: Void package smoke gate)
This commit is contained in:
@@ -127,6 +127,13 @@ native-packages: build
|
||||
native-smoke distro="debian:12":
|
||||
docker run --rm -v "$PWD/dist:/out:ro" -v "$PWD/packaging/native:/pk:ro" {{distro}} bash /pk/smoke-test.sh
|
||||
|
||||
# Lint, build, install and smoke every Void template in the pinned Void
|
||||
# container via podman (never Docker), as ticket #28 gates. Needs no host
|
||||
# Void install; release work and CI call it the same way as the other
|
||||
# packaging gates.
|
||||
void-smoke:
|
||||
bash packaging/void/smoke.sh
|
||||
|
||||
# Build the canonical sdist, prepare an explicitly unattested local PKGBUILD,
|
||||
# then build/install it unchanged on current Arch (needs uv and docker).
|
||||
# Keep distributions and diagnostics away from other packaging commands' outputs.
|
||||
|
||||
@@ -11,6 +11,7 @@ repository first.
|
||||
packaging/void/
|
||||
├── README.md this file
|
||||
├── build.sh podman harness: lint + xbps-src build + import smoke
|
||||
├── smoke.sh ticket #28 gate: every template, one command
|
||||
├── tool_checksums.txt pinned base image and void-packages commit
|
||||
└── srcpkgs/
|
||||
├── python3-pywhispercpp/ ticket #26: whisper.cpp Python binding
|
||||
@@ -68,9 +69,11 @@ Version `0.18.5`, matching `src/vocalinux/version.py` — never newest
|
||||
upstream. The template builds the canonical release source distribution
|
||||
(the `releases/download` sdist, the same bytes published to PyPI) with a
|
||||
pinned `checksum`; `tests/test_void_app_template.py` fails otherwise.
|
||||
The 0.18.5 asset still carries the pre-rename archive root, so the
|
||||
template sets `wrksrc` explicitly; the first release cut after the
|
||||
rename drops the override and points `distfiles` at the renamed asset.
|
||||
The 0.18.5 asset still carries the pre-rename archive root; xbps-src
|
||||
moves the single top-level directory to `pkgname-version` itself, so no
|
||||
`wrksrc` override is needed (`wrksrc` is deprecated and `xlint` rejects
|
||||
it). The first release cut after the rename points `distfiles` at the
|
||||
renamed asset.
|
||||
|
||||
- **Runtime deps** are Void's own interpreter, GTK and desktop bindings
|
||||
(`python3`, `python3-gobject`, `python3-cairo`, `gtk+3`,
|
||||
@@ -85,7 +88,9 @@ rename drops the override and points `distfiles` at the renamed asset.
|
||||
or wheels.
|
||||
- **Renamed paths**: the `voculinux.desktop` entry, the hicolor icons,
|
||||
and a `usr/share/voculinux/packaged` marker the app reads to present
|
||||
package-manager updates.
|
||||
package-manager updates. The 0.18.5 sdist predates the rename, so
|
||||
`post_install` promotes the old file names at install time; the payload
|
||||
smoke rejects any pre-rename leftovers in the OS-visible paths.
|
||||
- **INSTALL.msg** covers what xbps cannot express: the `input` group for
|
||||
the evdev hotkey backend, first-run model downloads, optional engines
|
||||
as manual additions (vosk, OpenAI whisper, Parakeet, faster-whisper,
|
||||
@@ -105,4 +110,23 @@ packaging/void/build.sh voculinux
|
||||
|
||||
Podman only, never Docker. The base image digest and the void-packages
|
||||
commit come from `tool_checksums.txt`; bump them there, never in the
|
||||
script. Ticket #28 wires this into the package smoke gate.
|
||||
script.
|
||||
|
||||
## Smoke gate
|
||||
|
||||
One command runs the whole gate from a clean checkout — no host Void
|
||||
install — linting, building, installing and smoke-testing every
|
||||
template under `srcpkgs` in dependency order (the app last):
|
||||
|
||||
```sh
|
||||
packaging/void/smoke.sh
|
||||
# or: just void-smoke
|
||||
```
|
||||
|
||||
Each package delegates to `build.sh`, so the pinned container and the
|
||||
per-package stages (xlint, xbps-src, install, payload/version/import
|
||||
smoke) stay implemented once. The gate fails fast: a broken package
|
||||
prints `FAIL: <pkgname>` naming which package broke, above it the
|
||||
harness echoes which stage broke. `tests/test_void_smoke_gate.py`
|
||||
asserts offline that the gate stays wired to every `srcpkgs` template,
|
||||
so a renamed or added template cannot silently drop out.
|
||||
|
||||
@@ -52,11 +52,40 @@ voculinux)
|
||||
exit 1
|
||||
fi
|
||||
echo "no models or wheels in the shipped package"
|
||||
echo "== payload $PKGNAME: no pre-rename leftovers"
|
||||
if tar -tf "$PKGFILE" 2>/dev/null | grep -E "usr/share/applications/vocalinux|usr/share/icons/.*/vocalinux"; then
|
||||
echo "pre-rename file names in the OS-visible payload" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "no pre-rename leftovers in the shipped package"
|
||||
echo "== version and import smoke $PKGNAME"
|
||||
voculinux --version
|
||||
python3 -c "from importlib.metadata import version; print('voculinux ' + version('voculinux'))"
|
||||
cat > /voculinux-smoke.py <<'PYEOF'
|
||||
# The 0.18.5 sdist predates the rename: the importable top-level package
|
||||
# and the distribution metadata still carry the old name. The first
|
||||
# release cut after the rename moves these to voculinux.
|
||||
import importlib
|
||||
|
||||
MODULES = (
|
||||
"vocalinux.main",
|
||||
"vocalinux.ui.tray_indicator",
|
||||
"vocalinux.speech_recognition.recognition_manager",
|
||||
"vocalinux.speech_recognition.command_processor",
|
||||
"vocalinux.text_injection.text_injector",
|
||||
"vocalinux.text_injection.ibus_engine",
|
||||
"vocalinux.ui.keyboard_backends",
|
||||
"pywhispercpp.model",
|
||||
)
|
||||
for module in MODULES:
|
||||
importlib.import_module(module)
|
||||
print("import-smoke OK:", ", ".join(MODULES))
|
||||
|
||||
from importlib.metadata import version
|
||||
print("vocalinux", version("vocalinux"))
|
||||
PYEOF
|
||||
python3 /voculinux-smoke.py
|
||||
;;
|
||||
*)
|
||||
python3-pywhispercpp)
|
||||
echo "== compiler flag scan of the shipped ggml library"
|
||||
LIBMEMBER=$(tar -tf "$PKGFILE" 2>/dev/null | grep -E "libggml-cpu\.so" | head -n1)
|
||||
[ -n "$LIBMEMBER" ] || {
|
||||
@@ -98,4 +127,19 @@ print("smoke ok")
|
||||
PYEOF
|
||||
EXPECT_VULKAN="$EXPECT_VULKAN" python3 /smoke.py
|
||||
;;
|
||||
python3-PyAudio)
|
||||
echo "== import smoke $PKGNAME"
|
||||
python3 -c "import pyaudio; print('pyaudio', pyaudio.__version__)"
|
||||
;;
|
||||
python3-pynput)
|
||||
# pynput selects a display backend at import and raises without one;
|
||||
# the container has no display, so presence plus the installed
|
||||
# version (the same proof the native smoke accepts) is the smoke.
|
||||
echo "== import smoke $PKGNAME"
|
||||
python3 -c "import importlib.metadata, importlib.util; assert importlib.util.find_spec('pynput'), 'pynput not importable'; print('pynput', importlib.metadata.version('pynput'))"
|
||||
;;
|
||||
*)
|
||||
echo "no smoke recipe for package '$PKGNAME' — add one alongside the template" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
Executable
+35
@@ -0,0 +1,35 @@
|
||||
#!/bin/sh
|
||||
# Smoke gate for all Void templates (ticket #28): lint, build, install
|
||||
# and version/import smoke for every template, one command, from a clean
|
||||
# checkout. Runs under podman, never Docker; no host Void install needed.
|
||||
#
|
||||
# Usage: packaging/void/smoke.sh
|
||||
# or: just void-smoke
|
||||
#
|
||||
# Builds every template under packaging/void/srcpkgs in dependency order
|
||||
# (leaves first, the app last — it resolves the others from the local
|
||||
# repository at install time) by delegating each package to build.sh,
|
||||
# which runs xlint, xbps-src, install and the payload/version/import
|
||||
# smoke in the pinned Void container. Fails fast naming the package; the
|
||||
# harness echoes each stage, so the output shows which package broke at
|
||||
# which stage.
|
||||
set -eu
|
||||
|
||||
CDPATH=""
|
||||
HERE=$(dirname -- "$0")
|
||||
HERE=$(cd -- "$HERE" && pwd)
|
||||
|
||||
# Dependency order: the app last. tests/test_void_smoke_gate.py fails if
|
||||
# this list drifts from packaging/void/srcpkgs or the app moves earlier.
|
||||
PKGS="python3-PyAudio python3-pynput python3-pywhispercpp voculinux"
|
||||
|
||||
for PKG in $PKGS; do
|
||||
echo "== smoke $PKG"
|
||||
"$HERE/build.sh" "$PKG" || {
|
||||
echo "FAIL: $PKG (see the stage above: xlint, xbps-src, install, payload/version/import smoke)" >&2
|
||||
exit 1
|
||||
}
|
||||
echo "OK: $PKG linted, built, installed and smoked"
|
||||
done
|
||||
|
||||
echo "SMOKE GATE PASSED: $PKGS"
|
||||
@@ -2,7 +2,9 @@
|
||||
pkgname=voculinux
|
||||
version=0.18.5
|
||||
revision=1
|
||||
build_style=python3-module
|
||||
# The release sdist ships no setup.py (setuptools pyproject-only), so the
|
||||
# build goes through the PEP 517 wheel (cf. Solaar, PhotoCollage).
|
||||
build_style=python3-pep517
|
||||
hostmakedepends="python3-setuptools"
|
||||
makedepends="python3-devel"
|
||||
depends="python3 python3-gobject python3-cairo gtk+3 libayatana-appindicator ibus gobject-introspection python3-numpy python3-requests python3-pysocks python3-psutil python3-evdev python3-xlib python3-tqdm python3-platformdirs python3-PyAudio python3-pynput python3-pywhispercpp"
|
||||
@@ -12,21 +14,34 @@ license="AGPL-3.0-only"
|
||||
homepage="https://github.com/VocaHQ/vocalinux"
|
||||
distfiles="https://github.com/VocaHQ/vocalinux/releases/download/v${version}/vocalinux-${version}.tar.gz"
|
||||
checksum=1def4d8f0f14f2df2f2fee64ca29e6865e3d82e551f7d465c2a78ec6d4d72b1d
|
||||
# The 0.18.5 release asset still carries the pre-rename archive root, so
|
||||
# build under that directory. The first release cut after the rename ships
|
||||
# the archive under the new name; that bump drops this override and points
|
||||
# distfiles at the renamed asset with its published digest.
|
||||
wrksrc="vocalinux-${version}"
|
||||
# The 0.18.5 release asset still carries the pre-rename archive root.
|
||||
# xbps-src moves a single-top-dir archive to pkgname-version itself, so no
|
||||
# wrksrc override is needed (wrksrc is deprecated and xlint rejects it).
|
||||
# The first release cut after the rename ships the archive under the new
|
||||
# name; that bump points distfiles at the renamed asset with its digest.
|
||||
|
||||
post_install() {
|
||||
# Entry points land on PATH through the module build; install the
|
||||
# renamed desktop entry, the full icon set, and the marker the app
|
||||
# reads to present package-manager updates (see INSTALL.msg).
|
||||
# The 0.18.5 sdist predates the rename: the entry and icons still
|
||||
# carry the old names. Promote them at install time — the entry lands
|
||||
# as voculinux.desktop with rewritten Name/Exec/Icon, old-prefixed
|
||||
# icons land renamed (vinstall takes the installed name, as vman
|
||||
# uses it). The first release cut after the rename ships the new
|
||||
# names; that bump drops the renames. Entry points land on PATH
|
||||
# through the wheel build; the marker is what the app reads to
|
||||
# present package-manager updates (see INSTALL.msg).
|
||||
vlicense LICENSE
|
||||
sed -e 's/vocalinux/voculinux/g' -e 's/Vocalinux/Voculinux/g' \
|
||||
vocalinux.desktop > voculinux.desktop
|
||||
vinstall voculinux.desktop 644 usr/share/applications
|
||||
for icon in resources/icons/scalable/*.svg; do
|
||||
vinstall "$icon" 644 usr/share/icons/hicolor/scalable/apps
|
||||
vinstall "$icon" 644 usr/share/icons/hicolor/scalable/apps \
|
||||
"$(basename "$icon" | sed 's/^vocalinux/voculinux/')"
|
||||
done
|
||||
# The wheel's entry points still carry the old names: promote the
|
||||
# launchers (this runs before the provides hooks, so cmd:voculinux
|
||||
# is what the package provides).
|
||||
mv "${DESTDIR}/usr/bin/vocalinux" "${DESTDIR}/usr/bin/voculinux"
|
||||
mv "${DESTDIR}/usr/bin/vocalinux-gui" "${DESTDIR}/usr/bin/voculinux-gui"
|
||||
vmkdir usr/share/voculinux
|
||||
touch "${DESTDIR}/usr/share/voculinux/packaged"
|
||||
}
|
||||
|
||||
@@ -22,7 +22,17 @@ REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
OLD_NAME = re.compile(r"vocalinux", re.IGNORECASE)
|
||||
|
||||
SKIP_DIRS = {".git", "__pycache__", ".venv", "venv", "node_modules", ".next", "build", "dist", "out"}
|
||||
SKIP_DIRS = {
|
||||
".git",
|
||||
"__pycache__",
|
||||
".venv",
|
||||
"venv",
|
||||
"node_modules",
|
||||
".next",
|
||||
"build",
|
||||
"dist",
|
||||
"out",
|
||||
}
|
||||
SKIP_SUFFIXES = (".egg-info",)
|
||||
SKIP_FILES = {"uv.lock", "CHANGELOG.md"}
|
||||
SKIP_PREFIXES = ("requirements/",)
|
||||
@@ -129,6 +139,17 @@ def _allowed(path: str, line: str) -> bool:
|
||||
if path == "tests/test_void_app_template.py" and '"vocalinux"' in line:
|
||||
# The app-template policy test asserts the old name is gone.
|
||||
return True
|
||||
if path == "tests/test_void_app_template.py" and "usr/bin/vocalinux" in line:
|
||||
# Pre-rename sdist launchers the template promotes at install time
|
||||
# (ticket #28); dropped at the first post-rename release bump.
|
||||
return True
|
||||
if path == "packaging/void/container-main.sh" and (
|
||||
"usr/share/applications/vocalinux" in line or 'version("vocalinux")' in line
|
||||
):
|
||||
# Pre-rename sdist compat (ticket #28): the payload guard names the
|
||||
# old file names it rejects, and the import smoke queries the old
|
||||
# distribution name. Dropped at the first post-rename release bump.
|
||||
return True
|
||||
if '== "vocalinux"' in line or '.startswith("vocalinux' in line:
|
||||
# Retained module identity comparisons in test isolation helpers.
|
||||
return True
|
||||
|
||||
@@ -194,7 +194,9 @@ def test_mandatory_void_variables_are_present() -> None:
|
||||
"checksum",
|
||||
):
|
||||
assert variables.get(key), f"template is missing {key}"
|
||||
assert variables["build_style"] == "python3-module"
|
||||
# The release sdist ships no setup.py (setuptools pyproject-only), so
|
||||
# xbps-src must build the PEP 517 wheel, not run setup.py directly.
|
||||
assert variables["build_style"] == "python3-pep517"
|
||||
assert "python3-setuptools" in variables["hostmakedepends"].split()
|
||||
assert "python3-devel" in variables["makedepends"].split()
|
||||
assert variables["revision"] == "1", "new templates start at revision 1"
|
||||
@@ -228,13 +230,16 @@ def test_distfiles_sources_the_canonical_release_by_checksum() -> None:
|
||||
for moving in ("/master", "/main", "/archive/", "git+"):
|
||||
assert moving not in distfiles, f"distfiles must not track a moving ref: {moving}"
|
||||
assert re.fullmatch(r"[0-9a-f]{64}", variables["checksum"]), "checksum must be sha256"
|
||||
# The unpacked top-level directory must be where the template builds:
|
||||
# wrksrc defaults to pkgname-version, so a distfile rooted under the
|
||||
# pre-rename archive name needs an explicit wrksrc override.
|
||||
# The unpacked top-level directory must be where the template builds.
|
||||
# xbps-src moves a single-top-dir archive to pkgname-version itself
|
||||
# (common/hooks/do-extract/00-distfiles.sh), so the pre-rename archive
|
||||
# root needs no override — and wrksrc is deprecated, xlint rejects it.
|
||||
root = Path(distfiles.rsplit("/", 1)[1].replace("${version}", version))
|
||||
assert root.suffixes[-2:] == [".tar", ".gz"]
|
||||
expected_wrksrc = variables.get("wrksrc", f"{variables['pkgname']}-${{version}}")
|
||||
assert expected_wrksrc.replace("${version}", version) == root.name[: -len(".tar.gz")]
|
||||
assert (
|
||||
root.name[: -len(".tar.gz")] != f"{variables['pkgname']}-{version}"
|
||||
), "the release asset is still rooted under the pre-rename archive name"
|
||||
assert "wrksrc" not in variables, "deprecated wrksrc override: xlint fails the template"
|
||||
|
||||
|
||||
def test_every_project_dependency_is_covered() -> None:
|
||||
@@ -292,15 +297,22 @@ def test_package_ships_no_speech_models() -> None:
|
||||
assert "model" not in code, "models are not package payload"
|
||||
|
||||
|
||||
def test_post_install_uses_the_renamed_paths() -> None:
|
||||
"""Desktop entry, icons, license and the packaged-install marker install."""
|
||||
def test_post_install_installs_the_renamed_paths_from_the_old_sdist() -> None:
|
||||
"""The 0.18.5 sdist predates the rename, so post_install promotes the
|
||||
old names at install time: the entry lands as voculinux.desktop with
|
||||
rewritten Name/Exec/Icon, and old-prefixed icons land renamed."""
|
||||
body = _post_install_body()
|
||||
assert "vlicense LICENSE" in body
|
||||
assert "vocalinux.desktop" in body, "the sdist entry still carries the old name"
|
||||
assert "voculinux.desktop" in body and "usr/share/applications" in body
|
||||
assert "s/Vocalinux/Voculinux/" in body, "the entry Name must be rewritten"
|
||||
assert "s/vocalinux/voculinux/" in body, "the entry Exec/Icon must be rewritten"
|
||||
assert "resources/icons/scalable" in body
|
||||
assert "usr/share/icons/hicolor/scalable/apps" in body
|
||||
assert "s/^vocalinux/voculinux/" in body, "old-prefixed icons must land renamed"
|
||||
assert "usr/bin/vocalinux" in body, "the sdist launchers still carry the old names"
|
||||
assert "usr/bin/voculinux" in body, "the launchers must land under the new names"
|
||||
assert "usr/share/voculinux/packaged" in body
|
||||
assert "vocalinux" not in body, "post_install must use only the renamed install paths"
|
||||
|
||||
|
||||
def test_build_fetches_nothing_beyond_the_sdist() -> None:
|
||||
@@ -353,9 +365,16 @@ def test_container_harness_covers_the_app_package() -> None:
|
||||
assert "docker" not in build.lower().replace("never docker", "")
|
||||
harness = CONTAINER_MAIN.read_text(encoding="utf-8")
|
||||
assert "voculinux --version" in harness, "the app smoke checks the entry point"
|
||||
assert "vocalinux.ui.tray_indicator" in harness, "the smoke imports the desktop modules"
|
||||
assert (
|
||||
"vocalinux.speech_recognition.recognition_manager" in harness
|
||||
), "the smoke imports the speech modules"
|
||||
assert "usr/share/applications/voculinux.desktop" in harness
|
||||
assert "usr/share/icons/hicolor/scalable/apps/voculinux.svg" in harness
|
||||
assert "usr/share/voculinux/packaged" in harness
|
||||
assert (
|
||||
"no pre-rename leftovers" in harness
|
||||
), "the payload check must reject pre-rename file names in the package"
|
||||
assert "python3-pywhispercpp" in harness or "PKGNAME" in harness
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
"""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 itself: exit status
|
||||
plus package/stage identification on stdout/stderr.
|
||||
- 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/build.sh`` owns the per-package container flow the
|
||||
gate orchestrates. 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"
|
||||
BUILD_SH = VOID / "build.sh"
|
||||
CONTAINER_MAIN = VOID / "container-main.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_per_package_harness() -> None:
|
||||
"""One container flow implementation; the gate only orchestrates it."""
|
||||
assert "build.sh" in _smoke_text(), "smoke.sh must call build.sh per package"
|
||||
|
||||
|
||||
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 test_gate_names_the_failing_package() -> None:
|
||||
"""Output identifies which package failed, alongside the stage echoes."""
|
||||
text = _smoke_text()
|
||||
assert "FAIL" in text, "the gate must report failures as FAIL"
|
||||
assert re.search(
|
||||
r"FAIL.*\$PKG|FAIL.*\$\{PKG", text
|
||||
), "the FAIL report must name the failing package"
|
||||
|
||||
|
||||
def _case_branch(label: str) -> str:
|
||||
"""Shell lines of one case branch in container-main.sh."""
|
||||
text = CONTAINER_MAIN.read_text(encoding="utf-8")
|
||||
start = text.index(f"\n{label})")
|
||||
end = text.index(";;", start)
|
||||
return text[start:end]
|
||||
|
||||
|
||||
def test_harness_scopes_the_ggml_scan_to_the_binding() -> None:
|
||||
"""Only the whisper binding ships libggml; leaves must not need it."""
|
||||
assert "libggml-cpu" in _case_branch("python3-pywhispercpp")
|
||||
assert "libggml-cpu" not in _case_branch("*")
|
||||
|
||||
|
||||
def test_harness_smokes_the_leaf_packages() -> None:
|
||||
"""PyAudio imports headless; pynput needs a display, so prove presence."""
|
||||
assert "import pyaudio" in _case_branch("python3-PyAudio")
|
||||
branch = _case_branch("python3-pynput")
|
||||
assert "pynput" in branch
|
||||
assert "find_spec" in branch or "version(" in branch
|
||||
|
||||
|
||||
def test_harness_rejects_packages_without_a_smoke_recipe() -> None:
|
||||
"""A future template falls into *) and must fail loudly, never pass mute."""
|
||||
branch = _case_branch("*")
|
||||
assert "exit 1" in branch
|
||||
|
||||
|
||||
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"
|
||||
Reference in New Issue
Block a user