Files
voculinux/tests/test_python_version_policy.py
T
soubarnak ea7be5f111 feat(identity): rename user-visible identity to voculinux with one-time legacy import
The fork takes its own name everywhere a user can see it: binaries
(voculinux, voculinux-gui), desktop entry, autostart, tray, D-Bus
namespace (com.voculinux.Voculinux, no alias for the old name),
config/data/cache dirs, single-instance lock, IBus engine identity,
tray icons, resource prefixes, VOCULINUX_* environment names, and the
update-checker user agent. The internal Python module stays vocalinux
and the distribution metadata keeps the upstream URLs as provenance.

On first run a previous Vocalinux install's config.json, custom
dictionary, and portal restore token are copied once into the new
locations (new config always wins, no merge, no write-back); models
re-resolve through the pinned-checksum download path. An offline
naming-sweep test proves no old-name strings remain on user-visible
surfaces outside the allowlist.

Refs #18
2026-11-06 01:29:39 +05:30

129 lines
4.5 KiB
Python

"""Guard against Python-version drift between the places that declare it.
`requires-python`, the classifiers, the CI matrix and install.sh's MIN_VERSION
used to disagree (3.9 floor, 3.9-3.13 matrix, classifiers promising 3.14), which
is how an unsupported interpreter can quietly become the one users install with.
"""
import re
import sys
import tomllib
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
PYPROJECT = REPO_ROOT / "pyproject.toml"
INSTALL_SH = REPO_ROOT / "install.sh"
PIPELINE = REPO_ROOT / ".github" / "workflows" / "unified-pipeline.yml"
PKGBUILD = REPO_ROOT / "packaging" / "aur" / "voculinux" / "PKGBUILD"
# Everything user-facing that could state the floor. Scanned as trees rather than
# a fixed list: docs and the website drifted to "3.9+" for a full release cycle
# after the floor moved, and a new page must not be able to reintroduce it.
DOC_AND_SITE_GLOBS = [
(REPO_ROOT / "docs", "**/*.md"),
(REPO_ROOT / "web" / "src", "**/*.tsx"),
(REPO_ROOT / "web" / "src", "**/*.ts"),
(REPO_ROOT, "README.md"),
(REPO_ROOT, "CONTRIBUTING.md"),
]
def _doc_and_site_files() -> list:
files = []
for base, pattern in DOC_AND_SITE_GLOBS:
files.extend(p for p in base.glob(pattern) if "node_modules" not in p.parts)
return sorted(set(files))
def _version_tuple(version: str) -> tuple:
return tuple(int(part) for part in version.split("."))
def _pyproject() -> dict:
with PYPROJECT.open("rb") as handle:
return tomllib.load(handle)
def _requires_python_floor() -> str:
requires = _pyproject()["project"]["requires-python"]
match = re.fullmatch(r">=\s*(\d+\.\d+)", requires.strip())
assert match, f"unexpected requires-python format: {requires!r}"
return match.group(1)
def _classifier_versions() -> list:
versions = []
for classifier in _pyproject()["project"]["classifiers"]:
match = re.fullmatch(r"Programming Language :: Python :: (\d+\.\d+)", classifier)
if match:
versions.append(match.group(1))
return versions
def _ci_matrix_versions() -> list:
match = re.search(r"python-version: \[([^\]]+)\]", PIPELINE.read_text(encoding="utf-8"))
assert match, "python-version matrix not found in unified-pipeline.yml"
return re.findall(r"'(\d+\.\d+)'", match.group(1))
def _install_sh_min_version() -> str:
# Anchored on the function: install.sh also carries an Ubuntu MIN_VERSION.
match = re.search(
r'check_python_version\(\) \{.*?local MIN_VERSION="(\d+\.\d+)"',
INSTALL_SH.read_text(encoding="utf-8"),
re.DOTALL,
)
assert match, "check_python_version's MIN_VERSION not found in install.sh"
return match.group(1)
def test_installer_floor_matches_requires_python():
assert _install_sh_min_version() == _requires_python_floor()
def test_classifiers_match_supported_range():
floor = _requires_python_floor()
classifiers = _classifier_versions()
assert classifiers, "no Programming Language :: Python :: X.Y classifiers"
assert min(map(_version_tuple, classifiers)) == _version_tuple(floor)
def test_ci_matrix_covers_every_supported_version():
# The matrix is the only thing that proves a promised version works.
assert sorted(_ci_matrix_versions()) == sorted(_classifier_versions())
def test_running_interpreter_is_supported():
assert sys.version_info[:2] >= _version_tuple(_requires_python_floor())
def _pkgbuild_python_floor() -> str:
match = re.search(r"'python>=(\d+\.\d+)'", PKGBUILD.read_text(encoding="utf-8"))
assert match, "python>= dependency not found in the AUR PKGBUILD"
return match.group(1)
def test_aur_package_matches_requires_python():
assert _pkgbuild_python_floor() == _requires_python_floor()
def test_docs_and_site_do_not_advertise_an_older_floor():
"""No user-facing file may promise a Python older than the real floor."""
floor = _version_tuple(_requires_python_floor())
files = _doc_and_site_files()
assert files, "found nothing to scan; the docs/website layout moved"
offenders = []
for path in files:
text = path.read_text(encoding="utf-8", errors="ignore")
for number, line in enumerate(text.splitlines(), 1):
for claimed in re.findall(r"Python (\d+\.\d+)\+", line):
if _version_tuple(claimed) < floor:
rel = path.relative_to(REPO_ROOT)
offenders.append(f"{rel}:{number} claims Python {claimed}+")
assert not offenders, "user-facing files promise an unsupported Python:\n" + "\n".join(
offenders
)