Void's stock python3-PyAudio (0.2.12_5) and python3-pysocks (1.7.0_8) sit below the project floors (pyaudio>=0.2.13, PySocks>=1.7.1). voculinux depended on both by bare name, which an installed stock package already satisfies, so a user who had the stock packages was never upgraded. - add packaging/void/srcpkgs/python3-pysocks: PySocks 1.7.1 from the PyPI sdist that uv.lock pins (sha256 3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0) - voculinux depends on python3-PyAudio>=0.2.14_1 and python3-pysocks>=1.7.1_1 - the install smoke installs the stock packages first, then voculinux, and fails unless each reads back as its carried pkgver - tests: each constraint must name its carried template's current version and revision; the stock version must sit below the floor; the depends parser refuses malformed tokens instead of stripping them - docs: "Version floors" in packaging/void/README.md; a pointer in docs/RELEASE_PROCESS.md Refs #16
507 lines
22 KiB
Python
507 lines
22 KiB
Python
"""Offline policy guards for the Void application template (ticket #27).
|
|
|
|
Spec #16 ships Voculinux itself as a Void package: an in-repo xbps
|
|
template in void-packages layout under ``packaging/void`` builds the app
|
|
from the canonical, checksum-verified release source distribution, which the
|
|
release path builds from the tag and stages for xbps-src (see
|
|
``tests/test_void_release_build.py``),
|
|
depends on Void's own interpreter, GTK, desktop bindings and packaged
|
|
Python libraries plus the dependency templates from tickets #25 and #26,
|
|
and installs the renamed desktop entry and icons. Speech models are not
|
|
package payload, nothing is vendored, and no pip runs at build or
|
|
install time.
|
|
|
|
Independent sources of truth: ``src/vocalinux/version.py`` owns the
|
|
version, ``pyproject.toml`` owns the dependency ranges, ``MANIFEST.in``
|
|
plus the renamed ``voculinux.desktop`` own the install paths, and the
|
|
fork's release path (``releases/download/<tag>``) plus ``xbps-src``
|
|
conventions own the fetch shape. The template must agree with all of
|
|
them; the container smoke gate (ticket #28) runs the same release path.
|
|
"""
|
|
|
|
import re
|
|
import tomllib
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
VOID = REPO_ROOT / "packaging" / "void"
|
|
SRC = VOID / "srcpkgs"
|
|
TEMPLATE = SRC / "voculinux" / "template"
|
|
INSTALL_MSG = SRC / "voculinux" / "INSTALL.msg"
|
|
PINS = VOID / "tool_checksums.txt"
|
|
RELEASE_PATH = VOID / "build-release.sh"
|
|
CONTAINER_RELEASE = VOID / "container-release.sh"
|
|
VERSION_PY = REPO_ROOT / "src" / "vocalinux" / "version.py"
|
|
PYPROJECT = REPO_ROOT / "pyproject.toml"
|
|
DISTRO_MAP = REPO_ROOT / "scripts" / "distro-package-map.yaml"
|
|
DISTRO_DOC = REPO_ROOT / "docs" / "DISTRO_COMPATIBILITY.md"
|
|
|
|
#: pip name (lowercase, dashes) -> the Void binary package providing it.
|
|
#: pywhispercpp/pynput/PyAudio come from the ticket #25/#26 templates in
|
|
#: this same tree; the rest are Void's own packages.
|
|
PIP_TO_VOID = {
|
|
"pywhispercpp": "python3-pywhispercpp",
|
|
"pynput": "python3-pynput",
|
|
"evdev": "python3-evdev",
|
|
"requests": "python3-requests",
|
|
"pysocks": "python3-pysocks",
|
|
"numpy": "python3-numpy",
|
|
"pyaudio": "python3-PyAudio",
|
|
"pygobject": "python3-gobject",
|
|
"psutil": "python3-psutil",
|
|
}
|
|
|
|
#: Void packages beyond the direct pip mapping: pywhispercpp's own
|
|
#: install_requires (tqdm, platformdirs), pynput's X11 backend (xlib),
|
|
#: the directly imported cairo binding, and the desktop substrate.
|
|
EXTRA_VOID_DEPS = (
|
|
"python3-tqdm",
|
|
"python3-platformdirs",
|
|
"python3-xlib",
|
|
"python3-cairo",
|
|
"python3",
|
|
"gtk+3",
|
|
"libayatana-appindicator",
|
|
"gobject-introspection",
|
|
)
|
|
|
|
#: Command-line helpers the app runs at runtime, and the Void package each
|
|
#: comes from. The release gate needs them on a packaged install (ticket
|
|
#: #27; spec #16 "Void packaging"). ydotool is deliberately absent: it is not
|
|
#: packaged for Void, so its fallback rung does not exist there.
|
|
RUNTIME_HELPER_PACKAGES = {
|
|
"pipewire": "pw-record and pw-dump for PipeWire capture",
|
|
"wtype": "direct keysym typing, the first rung on niri",
|
|
"wl-clipboard": "wl-copy and wl-paste for the clipboard paths",
|
|
"xdotool": "X11 and XWayland injection",
|
|
"pulseaudio-utils": "pactl source listing",
|
|
}
|
|
|
|
#: The placeholder the committed template carries until the first fork release
|
|
#: has a recorded digest. Every release build replaces it in its build tree; it
|
|
#: is never a real digest, and it never reaches a published package.
|
|
PLACEHOLDER_CHECKSUM = "0" * 64
|
|
|
|
#: sha256 of the upstream 0.18.5 archive. The template must never pin it: the
|
|
#: package is the fork's own release, not a copy of upstream's.
|
|
UPSTREAM_0_18_5_SHA256 = "1def4d8f0f14f2df2f2fee64ca29e6865e3d82e551f7d465c2a78ec6d4d72b1d"
|
|
|
|
#: The fork's release host. Every distfile and the homepage live here.
|
|
FORK_RELEASE_HOST = "https://git.bongbetic.com/xavierk/voculinux"
|
|
|
|
#: Every package name the app template may reference, verified to exist in
|
|
#: void-packages master on 2026-11-06 via the srcpkgs/<name>/template
|
|
#: paths (python3-devel is a subpackage of the python3 template, as
|
|
#: recorded for ticket #25). Re-verify with `xbps-query -Rs <name>` on a
|
|
#: Void system before extending this set.
|
|
VERIFIED_VOID_PACKAGES = frozenset(
|
|
{
|
|
"python3",
|
|
"python3-devel",
|
|
"python3-setuptools",
|
|
"python3-gobject",
|
|
"python3-cairo",
|
|
"gtk+3",
|
|
"libayatana-appindicator",
|
|
"gobject-introspection",
|
|
"pipewire",
|
|
"wtype",
|
|
"wl-clipboard",
|
|
"xdotool",
|
|
"pulseaudio-utils",
|
|
"python3-numpy",
|
|
"python3-requests",
|
|
"python3-pysocks",
|
|
"python3-psutil",
|
|
"python3-evdev",
|
|
"python3-xlib",
|
|
"python3-tqdm",
|
|
"python3-platformdirs",
|
|
"python3-PyAudio",
|
|
"python3-pynput",
|
|
"python3-pywhispercpp",
|
|
}
|
|
)
|
|
|
|
#: Template options that switch off dependency verification. The app
|
|
#: resolves every dependency deliberately through the mapping above,
|
|
#: never by hiding it behind one of these.
|
|
FORBIDDEN_VARS = ("noverifypydeps", "nopyprovides")
|
|
|
|
#: Carried templates whose stock Void package sits below the project floor
|
|
#: (tests/test_void_small_deps.py, GAP_PACKAGES). voculinux must require each
|
|
#: one as name>=<carried version>_<revision>. A bare name is satisfied by the
|
|
#: stock package a user already has, so it would never be upgraded.
|
|
GAP_DEPENDS = ("python3-PyAudio", "python3-pysocks")
|
|
|
|
#: One xbps dependency token: a bare name, or name, comparator and
|
|
#: version_revision. The whole token must match; a malformed token such as
|
|
#: "python3-PyAudio>=" is refused, never stripped down to its name.
|
|
DEPENDS_TOKEN = re.compile(
|
|
r"(?P<name>[A-Za-z0-9+._-]+)(?:(?P<op>>=|<=|>|<)(?P<version>[0-9][A-Za-z0-9.]*_[0-9]+))?"
|
|
)
|
|
|
|
|
|
def _template_text() -> str:
|
|
"""Read the raw template file."""
|
|
assert TEMPLATE.is_file(), f"missing Void app template: {TEMPLATE}"
|
|
return TEMPLATE.read_text(encoding="utf-8")
|
|
|
|
|
|
def _template_vars() -> dict:
|
|
"""Parse top-level VAR=value assignments from the template file."""
|
|
variables = {}
|
|
in_function = False
|
|
for line in _template_text().splitlines():
|
|
stripped = line.strip()
|
|
if not stripped or stripped.startswith("#"):
|
|
continue
|
|
if re.fullmatch(r"[A-Za-z0-9_+-]+\(\)\s*\{", stripped):
|
|
in_function = True
|
|
continue
|
|
if in_function:
|
|
if stripped == "}":
|
|
in_function = False
|
|
continue
|
|
key, separator, value = stripped.partition("=")
|
|
if not separator or not re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", key):
|
|
continue
|
|
value = value.strip()
|
|
if len(value) >= 2 and value[0] == value[-1] and value[0] in "\"'":
|
|
value = value[1:-1]
|
|
variables[key] = value
|
|
return variables
|
|
|
|
|
|
def _code_lines() -> str:
|
|
"""Template with comment lines stripped; behaviour lives in code."""
|
|
return "\n".join(
|
|
line for line in _template_text().splitlines() if not line.lstrip().startswith("#")
|
|
)
|
|
|
|
|
|
def _post_install_body() -> str:
|
|
"""Shell lines inside post_install(), without comments."""
|
|
lines = _template_text().splitlines()
|
|
start = next(i for i, line in enumerate(lines) if line.strip().startswith("post_install()"))
|
|
body = []
|
|
for line in lines[start + 1 :]:
|
|
if line.strip() == "}":
|
|
break
|
|
if line.strip() and not line.lstrip().startswith("#"):
|
|
body.append(line.strip())
|
|
return "\n".join(body)
|
|
|
|
|
|
def _app_version() -> str:
|
|
"""Version owned by src/vocalinux/version.py."""
|
|
match = re.search(r'^__version__ = "([^"]+)"', VERSION_PY.read_text(encoding="utf-8"), re.M)
|
|
assert match, "version.py no longer defines __version__"
|
|
return match.group(1)
|
|
|
|
|
|
def _project_dependencies() -> list:
|
|
"""Full dependency clauses from pyproject.toml."""
|
|
data = tomllib.loads(PYPROJECT.read_text(encoding="utf-8"))
|
|
return list(data["project"]["dependencies"])
|
|
|
|
|
|
def _depends_tokens(value: str) -> list:
|
|
"""Validated tokens of an xbps depends value; a malformed token fails the test."""
|
|
tokens = []
|
|
for token in value.split():
|
|
if token.startswith("${"):
|
|
continue
|
|
assert DEPENDS_TOKEN.fullmatch(token), f"malformed dependency token: {token!r}"
|
|
tokens.append(token)
|
|
return tokens
|
|
|
|
|
|
def _depends_names(value: str) -> set:
|
|
"""Package names of an xbps depends value, with any version constraint removed."""
|
|
return {DEPENDS_TOKEN.fullmatch(token).group("name") for token in _depends_tokens(value)}
|
|
|
|
|
|
def _carried_pkgver(name: str) -> str:
|
|
"""The pkgver a carried template builds: its version and revision, as written."""
|
|
text = (SRC / name / "template").read_text(encoding="utf-8")
|
|
version = re.search(r"^version=(\S+)$", text, re.M)
|
|
revision = re.search(r"^revision=(\d+)$", text, re.M)
|
|
assert version and revision, f"{name} template lacks a version or revision"
|
|
return f"{version.group(1)}_{revision.group(1)}"
|
|
|
|
|
|
def test_app_template_lives_in_void_packages_layout() -> None:
|
|
"""The app template sits where xbps-src expects it: srcpkgs/<pkgname>."""
|
|
assert TEMPLATE.is_file(), f"{TEMPLATE} is missing"
|
|
assert INSTALL_MSG.is_file(), f"{INSTALL_MSG} is missing"
|
|
|
|
|
|
def test_pkgname_matches_template_directory() -> None:
|
|
"""Void requires pkgname to match srcpkgs/<pkgname>, using the new name."""
|
|
assert _template_vars()["pkgname"] == TEMPLATE.parent.name == "voculinux"
|
|
|
|
|
|
def test_mandatory_void_variables_are_present() -> None:
|
|
"""The Manual's mandatory variables plus fetch inputs must be set."""
|
|
variables = _template_vars()
|
|
for key in (
|
|
"pkgname",
|
|
"version",
|
|
"revision",
|
|
"short_desc",
|
|
"maintainer",
|
|
"license",
|
|
"homepage",
|
|
"build_style",
|
|
"hostmakedepends",
|
|
"makedepends",
|
|
"depends",
|
|
"distfiles",
|
|
"checksum",
|
|
):
|
|
assert variables.get(key), f"template is missing {key}"
|
|
# 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"
|
|
|
|
|
|
def test_template_metadata_shape() -> None:
|
|
"""Maintainer, license, homepage and description follow the Manual."""
|
|
variables = _template_vars()
|
|
assert re.fullmatch(r".+ <[^@<>\s]+@[^@<>\s]+>", variables["maintainer"])
|
|
assert "noreply" not in variables["maintainer"]
|
|
assert variables["license"] == "AGPL-3.0-only"
|
|
assert variables["homepage"] == FORK_RELEASE_HOST
|
|
assert len(variables["short_desc"]) <= 72
|
|
assert re.fullmatch(r"[0-9][A-Za-z0-9.]*", variables["version"])
|
|
|
|
|
|
def test_template_version_matches_version_py() -> None:
|
|
"""The template builds the current source version, not a stale one."""
|
|
assert _template_vars()["version"] == _app_version()
|
|
|
|
|
|
def test_distfiles_sources_the_canonical_release_by_checksum() -> None:
|
|
"""One fork release-asset distfile, checksum-bound; never upstream or git."""
|
|
variables = _template_vars()
|
|
distfiles = variables["distfiles"]
|
|
assert distfiles == (
|
|
f"{FORK_RELEASE_HOST}/releases/download/${{_tag}}/"
|
|
f"{variables['pkgname']}-${{version}}.tar.gz"
|
|
), "distfiles must name the fork's release asset by its tag and xbps version"
|
|
assert "github.com" not in distfiles and "VocaHQ" not in distfiles, "no upstream archive"
|
|
for moving in ("/master", "/main", "/archive/", "git+"):
|
|
assert moving not in distfiles, f"distfiles must not track a moving ref: {moving}"
|
|
assert distfiles.rstrip().endswith(".tar.gz"), "the release payload is the sdist tarball"
|
|
assert variables["_tag"] == f"v{_app_version()}", "_tag is the tag, hyphen and all"
|
|
assert "v${version}" not in distfiles, "a pre-release tag keeps its hyphen in the URL"
|
|
assert "wrksrc" not in variables, "deprecated wrksrc override: xlint fails the template"
|
|
|
|
|
|
def test_checksum_is_a_recorded_digest_or_the_build_placeholder() -> None:
|
|
"""A 64-hex digest once recorded; until then the all-zero placeholder that
|
|
every release build replaces. Never the upstream digest, never empty."""
|
|
checksum = _template_vars()["checksum"]
|
|
assert checksum != UPSTREAM_0_18_5_SHA256, "the template must not pin the upstream archive"
|
|
assert re.fullmatch(r"[0-9a-f]{64}", checksum), f"not a sha256: {checksum!r}"
|
|
assert "PENDING" not in _template_text(), "no pending marker: the build pins the digest"
|
|
|
|
|
|
def test_every_project_dependency_is_covered() -> None:
|
|
"""pyproject.toml runtime deps plus substrate must all resolve to depends."""
|
|
depends = _depends_names(_template_vars()["depends"])
|
|
uncovered = []
|
|
for clause in _project_dependencies():
|
|
pip_name = re.match(r"^([A-Za-z0-9._-]+)", clause).group(1).lower().replace("_", "-")
|
|
void_name = PIP_TO_VOID.get(pip_name)
|
|
assert void_name, f"no Void mapping recorded for {pip_name}"
|
|
if void_name not in depends:
|
|
uncovered.append(f"{pip_name} -> {void_name}")
|
|
missing_extra = [name for name in EXTRA_VOID_DEPS if name not in depends]
|
|
assert not uncovered, f"deps with no Void coverage: {uncovered}"
|
|
assert not missing_extra, f"substrate missing from depends: {missing_extra}"
|
|
|
|
|
|
def test_template_dependencies_exist_in_void_packages() -> None:
|
|
"""Template dependency lists name only verified void-packages names."""
|
|
variables = _template_vars()
|
|
referenced = set()
|
|
for key in ("depends", "hostmakedepends", "makedepends", "checkdepends"):
|
|
referenced |= _depends_names(variables.get(key, ""))
|
|
unknown = sorted(referenced - VERIFIED_VOID_PACKAGES)
|
|
assert not unknown, f"template references unverified packages: {unknown}"
|
|
|
|
|
|
def test_depends_tokens_are_validated_not_stripped() -> None:
|
|
"""A constrained token is accepted only in full; a truncated one is malformed."""
|
|
assert DEPENDS_TOKEN.fullmatch("python3-PyAudio>=0.2.14_1")
|
|
assert DEPENDS_TOKEN.fullmatch("python3-pysocks")
|
|
for malformed in ("python3-PyAudio>=", "python3-PyAudio>=0.2.14", ">=0.2.14_1"):
|
|
assert not DEPENDS_TOKEN.fullmatch(malformed), malformed
|
|
with pytest.raises(AssertionError):
|
|
_depends_tokens(malformed)
|
|
|
|
|
|
def test_gap_packages_are_version_constrained_to_their_carried_templates() -> None:
|
|
"""voculinux requires name>=<carried pkgver>, never the bare name.
|
|
|
|
A bare name is satisfied by the stock package a user already has (below the
|
|
floor), so it would keep the stock package. The constraint must track the
|
|
carried template: bumping its version or revision without this line fails.
|
|
"""
|
|
tokens = _depends_tokens(_template_vars()["depends"])
|
|
for name in GAP_DEPENDS:
|
|
matching = [
|
|
token for token in tokens if DEPENDS_TOKEN.fullmatch(token).group("name") == name
|
|
]
|
|
expected = f"{name}>={_carried_pkgver(name)}"
|
|
assert matching == [expected], f"depends must name {expected}; found {matching}"
|
|
|
|
|
|
def test_no_stale_indicator_library_name() -> None:
|
|
"""The pre-rename libappindicator names do not exist in void-packages."""
|
|
variables = _template_vars()
|
|
for key in ("depends", "makedepends", "hostmakedepends"):
|
|
for token in variables.get(key, "").split():
|
|
assert token not in ("libappindicator", "libappindicator-gtk3"), (
|
|
f"{key} names {token}, which has no void-packages template; "
|
|
"the Ayatana fork (libayatana-appindicator) is the provider"
|
|
)
|
|
|
|
|
|
def test_no_pip_or_vendored_payload_at_build_or_install() -> None:
|
|
"""The build installs from the sdist; pip never runs, nothing is vendored."""
|
|
code = _code_lines().lower()
|
|
for forbidden in ("pip install", "pip3 install", "pip download", "requirements", ".whl"):
|
|
assert forbidden not in code, f"template runs pip or vendors wheels: {forbidden}"
|
|
assert "venv" not in code
|
|
body = _post_install_body().lower()
|
|
assert "pip" not in body, "post_install must not invoke pip"
|
|
|
|
|
|
def test_package_ships_no_speech_models() -> None:
|
|
"""Speech models download on first run; they are not package payload."""
|
|
code = _code_lines().lower()
|
|
assert "model" not in code, "models are not package payload"
|
|
|
|
|
|
def test_post_install_installs_the_fork_names_without_renaming() -> None:
|
|
"""The fork's sdist already carries the new names, so post_install installs
|
|
them as they are: no sed rewrite and no mv of upstream launchers."""
|
|
body = _post_install_body()
|
|
assert "vlicense LICENSE" in body
|
|
assert "vinstall voculinux.desktop 644 usr/share/applications" in body
|
|
assert "resources/icons/scalable" in body
|
|
assert "usr/share/icons/hicolor/scalable/apps" in body
|
|
assert "usr/share/voculinux/packaged" in body
|
|
for upstream_step in ("sed ", " mv ", "vocalinux"):
|
|
assert (
|
|
upstream_step not in body
|
|
), f"post_install still renames upstream names: {upstream_step}"
|
|
assert "usr/bin/vocalinux" not in _template_text(), "no upstream launcher is promoted"
|
|
|
|
|
|
def test_runtime_helpers_are_declared_and_ibus_is_not() -> None:
|
|
"""Capture, direct typing, the clipboard and X11 helpers come from xbps;
|
|
ibus is excluded because it is not an answer on niri (spec #16)."""
|
|
depends = set(_template_vars()["depends"].split())
|
|
for package, why in RUNTIME_HELPER_PACKAGES.items():
|
|
assert package in depends, f"depends must pull in {package} ({why})"
|
|
assert "ibus" not in depends, "ibus is excluded: it cannot inject on niri"
|
|
assert "ydotool" not in depends, "ydotool is not packaged for Void"
|
|
|
|
|
|
def test_build_fetches_nothing_beyond_the_sdist() -> None:
|
|
"""Only the homepage and the declared distfile may appear as URLs."""
|
|
text = _template_text()
|
|
assert text.count("https://") == 2, "homepage + distfiles are the only URLs a template needs"
|
|
assert "FetchContent" not in text and "ExternalProject" not in text
|
|
|
|
|
|
def test_no_verification_disabling_options() -> None:
|
|
"""Dependency verification stays on; distfiles stay checksum-bound."""
|
|
variables = _template_vars()
|
|
for key in FORBIDDEN_VARS:
|
|
assert key not in variables, f"template sets {key}"
|
|
assert "checksum" in variables, "the distfile must carry a checksum line"
|
|
|
|
|
|
def test_template_style_is_lint_clean() -> None:
|
|
"""Trailing whitespace and a missing final newline fail xlint."""
|
|
raw = TEMPLATE.read_bytes().decode("utf-8")
|
|
for lineno, line in enumerate(raw.splitlines(), 1):
|
|
assert line == line.rstrip(), f"trailing whitespace on line {lineno}"
|
|
assert "\t" not in line or line.startswith("\t"), f"stray tab on line {lineno}"
|
|
assert raw.endswith("\n"), "template must end with a newline"
|
|
|
|
|
|
def test_install_msg_covers_the_xbps_only_facts() -> None:
|
|
"""INSTALL.msg tells the user what xbps cannot express."""
|
|
assert INSTALL_MSG.is_file(), f"missing install message: {INSTALL_MSG}"
|
|
text = INSTALL_MSG.read_text(encoding="utf-8").lower()
|
|
assert "input" in text, "the evdev hotkey backend needs the input group"
|
|
assert "model" in text, "models download on first run"
|
|
for engine in ("vosk", "whisper", "parakeet", "faster-whisper", "onnxruntime"):
|
|
assert engine in text, f"optional-engine status must name {engine}"
|
|
assert "ydotool" in text, "the absent injection helper must be named"
|
|
assert "no permission" in text, "niri first run has no permission prompt"
|
|
assert "wtype" in text, "niri types direct through wtype"
|
|
assert "ibus" not in text, "IBus is excluded; the message must not point at it"
|
|
sentences = re.split(r"(?<=\.)\s+", " ".join(text.split()))
|
|
niri_sentences = [sentence for sentence in sentences if "niri" in sentence]
|
|
assert niri_sentences, "the message must describe the niri first run"
|
|
assert not any(
|
|
"portal" in sentence for sentence in niri_sentences
|
|
), "the portal is never recommended on niri"
|
|
|
|
|
|
def test_install_msg_line_length() -> None:
|
|
"""The Manual asks for at most 80 columns per line in INSTALL.msg."""
|
|
raw = INSTALL_MSG.read_bytes().decode("utf-8")
|
|
for lineno, line in enumerate(raw.splitlines(), 1):
|
|
assert len(line) <= 80, f"line {lineno} exceeds 80 columns"
|
|
assert raw.endswith("\n"), "INSTALL.msg must end with a newline"
|
|
|
|
|
|
def test_release_path_covers_the_app_package() -> None:
|
|
"""The install smoke runs the renamed binary and the app's payload checks."""
|
|
harness = CONTAINER_RELEASE.read_text(encoding="utf-8")
|
|
assert "voculinux --version" in harness, "the app smoke checks the entry point"
|
|
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" in harness or "pre-rename file names" in harness
|
|
), "the payload check must reject pre-rename file names in the package"
|
|
assert "models or wheels" in harness, "the payload check rejects models and wheels"
|
|
assert "import vocalinux.main" in harness, "the install smoke imports the app"
|
|
assert "pywhispercpp.model" in harness, "the install smoke imports the speech binding"
|
|
assert "PYNPUT_BACKEND=dummy" in harness and "import evdev" in harness
|
|
assert "build-release.sh" in RELEASE_PATH.read_text(encoding="utf-8")
|
|
|
|
|
|
def test_void_distro_inventory_matches_void_packages_reality() -> None:
|
|
"""The installer inventory names the Ayatana provider and marks ydotool absent."""
|
|
text = DISTRO_MAP.read_text(encoding="utf-8")
|
|
void_block = text.split(" void:", 1)[1].split("\n solus:", 1)[0]
|
|
code = "\n".join(line for line in void_block.splitlines() if not line.lstrip().startswith("#"))
|
|
assert "libayatana-appindicator" in code
|
|
assert "libappindicator-gtk3" not in code
|
|
assert "libappindicator " not in code.replace("libayatana-appindicator", "")
|
|
assert "absent" in void_block, "the ydotool absence must be marked in the inventory"
|
|
|
|
|
|
def test_void_install_docs_name_the_ayatana_provider() -> None:
|
|
"""The Void install snippet must install a package that exists."""
|
|
text = DISTRO_DOC.read_text(encoding="utf-8")
|
|
void_section = text.split("### Void Linux", 1)[1].split("###", 1)[0]
|
|
assert "libayatana-appindicator" in void_section
|
|
assert "libappindicator-gtk3" not in void_section
|
|
assert "libappindicator " not in void_section.replace("libayatana-appindicator", "")
|