Files
voculinux/tests/test_rename_sweep.py
T
xavierk b47a65c91e
CI / security (push) Successful in 1m15s
CI / lint (push) Successful in 23s
CI / ai-review (push) Skipped
fix(void): build the xbps release from the tag, not an attached sdist (#37)
Every release is built from its tag alone by packaging/void/build-release.sh, shared by the tag workflow and local dry runs. Signs in a network-less container with the key mounted read-only and publishes to the tag release and the rolling latest release.
2026-11-06 15:31:47 +00:00

261 lines
10 KiB
Python

"""Ticket #18 + #24: offline naming-sweep: no old-name strings on surfaces.
The fork takes its own name everywhere a user can see it. Anything still
spelling the old name must match a retained surface below. Each rule is
scoped to what the surface is, not to a word that happens to co-occur:
- upstream provenance: ``VocaHQ/vocalinux`` and ``jatinkrmalik/vocalinux``
slugs, and prose that labels the old name as upstream or as the fork's
origin ("Vocalinux (upstream)", "a hard fork of Vocalinux");
- the retained internal Python package (ticket #17): ``src/vocalinux`` paths,
dotted module references to modules that exist under ``src/vocalinux``,
import and ``-m`` forms, ``vocalinux/<file>.py`` module paths, and the
retained CamelCase class names (``VocalinuxEngine``, ...);
- the package name as a quoted tooling or test identifier (package lists,
mock module names, fixture trees), only in tooling and test paths;
- the one-time legacy import sources (ticket #18), only in the files that
implement or test the import, and only on lines that say so;
- pre-rename guards that name the old files they reject (ticket #28);
- generated or historical files (``uv.lock``, ``requirements/*.txt``,
``CHANGELOG.md``) and the git worktree pointer file ``.git``;
- web upstream surfaces (ticket #24): the nix flake snippets on the nixos page.
``tests/test_rename_sweep.py`` also checks the allowlist itself, so a loosened
rule fails here before it lets an old name onto a user-visible surface.
"""
import os
import re
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
SRC_PACKAGE = os.path.join(REPO_ROOT, "src", "vocalinux")
OLD_NAME = re.compile(r"vocalinux", re.IGNORECASE)
SKIP_DIRS = {
".git",
"__pycache__",
".venv",
"venv",
"node_modules",
".next",
"build",
"dist",
"out",
}
SKIP_SUFFIXES = (".egg-info",)
#: ``.git`` is a file in linked worktrees (``gitdir: .../worktrees/...``), so it
#: is skipped by name in addition to being a skipped directory.
SKIP_FILES = {"uv.lock", "CHANGELOG.md", ".git"}
SKIP_PREFIXES = ("requirements/",)
BINARY_EXTS = (
".wav",
".mp3",
".ogg",
".png",
".svg",
".ico",
".icns",
".bin",
".onnx",
".pt",
".zip",
".tar",
".gz",
".whl",
".AppImage",
".zsync",
".pyc",
)
#: web/ was fully rebranded in ticket #24; only upstream-provenance lines
#: (matched in _allowed) may still spell the old name there.
WEB_OWNED = {"web/src/app/page.tsx", "web/package.json"}
#: Top-level modules and packages of the retained package, read from the tree
#: so a dotted reference to a module that does not exist is not allowlisted.
RETAINED_MODULE_HEADS = frozenset(
[name[:-3] for name in os.listdir(SRC_PACKAGE) if name.endswith(".py")]
+ [
name
for name in os.listdir(SRC_PACKAGE)
if os.path.isdir(os.path.join(SRC_PACKAGE, name)) and not name.startswith("__")
]
) - {"__init__"}
_UPSTREAM_SLUG = re.compile(r"(?:VocaHQ|jatinkrmalik)/vocalinux", re.IGNORECASE)
_UPSTREAM_LABEL = re.compile(
r"upstream[^\n]{0,40}vocalinux|vocalinux[^\n]{0,40}upstream|fork of vocalinux",
re.IGNORECASE,
)
_DOTTED_MODULE = re.compile(
r"\bvocalinux\.(?:__\w+__|"
+ "|".join(sorted(RETAINED_MODULE_HEADS, key=len, reverse=True))
+ r")\b"
)
_IMPORT_FORM = re.compile(r"(?:^|[\s\"'])(?:from|import)\s+vocalinux\b|-m\s+vocalinux\b")
#: Prose that names the retained package as such (ticket #17's inventory).
_RETAINED_PROSE = re.compile(
r"retained[^\n]{0,60}vocalinux|vocalinux[^\n]{0,60}(?:retained|import package|module name)"
r"|(?:import package|module name)[^\n]{0,60}vocalinux",
re.IGNORECASE,
)
#: Prose that names the old install as the source of the one-time import.
_LEGACY_PROSE = re.compile(r"\b(?:legacy|previous|old) Vocalinux\b", re.IGNORECASE)
_MODULE_PATH = re.compile(
r"src/vocalinux\b"
r"|site-packages/vocalinux\b"
r"|\"src\"\s*/\s*\"vocalinux\""
r"|\bvocalinux/[\w./-]*\.py\b"
r"|\"vocalinux/"
r"|\bsite\s*/\s*\"vocalinux\""
r"|\"platform/vocalinux\""
)
_CAMEL_CLASS = re.compile(r"Vocalinux[A-Z]\w*")
#: Quoted package names in tooling and tests (package lists, mock targets,
#: ``"vocalinux."`` prefixes), and scanner regex literals in web tests.
_PACKAGE_SEGMENT = re.compile(r"[\"']vocalinux(?:[\"']|\.)|/vocalinux/[a-z]*")
_TOOLING_PREFIXES = (
"tests/",
"web/src/lib/__tests__/",
"scripts/",
"snap/local/",
".flake8",
"MANIFEST.in",
"install.sh",
"install.d/",
"uninstall.sh",
"pyproject.toml",
"justfile",
)
#: Files that implement or test the one-time legacy import (ticket #18), and the
#: pre-XDG compatibility path the custom dictionary keeps for stored settings.
_LEGACY_SOURCE_FILES = frozenset(
{
"src/vocalinux/legacy_import.py",
"src/vocalinux/app_identity.py",
"src/vocalinux/custom_dictionary.py",
"tests/test_legacy_import.py",
"tests/test_app_identity.py",
"tests/test_custom_dictionary.py",
"tests/test_rename_sweep.py",
}
)
_LEGACY_LINE = re.compile(
r"legacy|previous|pre-XDG|\.config/vocalinux|\.config\"?\s*/\s*\"vocalinux\""
r"|\.local/share/vocalinux",
re.IGNORECASE,
)
#: Pre-rename file names a packaging guard rejects (ticket #28).
_PRE_RENAME_GUARD = re.compile(r"usr/share/(?:applications|icons/[^\s\"]*)/vocalinux")
_NIX_FLAKE_IDENTIFIER = re.compile(
r"packages\.<system>\.|apps\.<system>\.|nixosModules\.|homeManagerModules\.|programs\."
r"|\bflake\b|\binputs?\b|outputs = \{|nixpkgs, |pkgs\.vocalinux|nix run|nix profile"
r"|github:VocaHQ",
re.IGNORECASE,
)
def _allowed(path: str, line: str) -> bool:
"""Return True when an old-name match is an allowlisted retained surface."""
if path == "tests/test_rename_sweep.py":
# This file defines the allowlist and quotes the old name to do so.
return True
if _UPSTREAM_SLUG.search(line) or _UPSTREAM_LABEL.search(line):
return True
if _MODULE_PATH.search(line) or _DOTTED_MODULE.search(line):
return True
if _IMPORT_FORM.search(line) or _CAMEL_CLASS.search(line) or _RETAINED_PROSE.search(line):
return True
if _LEGACY_PROSE.search(line):
return True
if path == "web/src/app/install/nixos/page.tsx":
# The Nix flake is upstream's: flake URL, input and output names and
# programs.* options spell the retained upstream API. The page carries
# a fork-status note saying those snippets install Vocalinux, not
# Voculinux, so only flake-shaped lines are allowed here.
return bool(_NIX_FLAKE_IDENTIFIER.search(line))
if path in _LEGACY_SOURCE_FILES and _LEGACY_LINE.search(line):
return True
if path.startswith(_TOOLING_PREFIXES) and _PACKAGE_SEGMENT.search(line):
return True
if path == "packaging/void/container-release.sh" and _PRE_RENAME_GUARD.search(line):
return True
if path == "src/vocalinux/utils/update_checker.py" and (
'casefold() == "vocalinux"' in line or '"/{name}/vocalinux"' in line
):
# Upstream repository identity the update checker recognises.
return True
return False
def _iter_files():
for root, dirs, files in os.walk(REPO_ROOT):
dirs[:] = [d for d in sorted(dirs) if d not in SKIP_DIRS]
for name in sorted(files):
if name in SKIP_FILES or name.endswith(BINARY_EXTS):
continue
full = os.path.join(root, name)
rel = os.path.relpath(full, REPO_ROOT)
if rel.startswith(SKIP_PREFIXES):
continue
yield rel, full
def test_naming_sweep_no_old_name_outside_allowlist() -> None:
"""Every old-name hit must be an allowlisted retained surface."""
offenders: list[str] = []
for rel, full in _iter_files():
if any(part.endswith(SKIP_SUFFIXES) for part in rel.split("/")):
continue
try:
with open(full, encoding="utf-8") as handle:
text = handle.read()
except (UnicodeDecodeError, ValueError):
continue
for number, line in enumerate(text.splitlines(), 1):
if OLD_NAME.search(line) and not _allowed(rel, line):
offenders.append(f"{rel}:{number}:{line.strip()[:160]}")
assert (
offenders == []
), f"{len(offenders)} old-name string(s) outside the allowlist:\n" + "\n".join(offenders[:60])
def test_owned_web_files_carry_new_name() -> None:
"""The two web rename surfaces name the fork (URLs aside)."""
for rel in sorted(WEB_OWNED):
with open(os.path.join(REPO_ROOT, rel), encoding="utf-8") as handle:
text = handle.read()
assert "Voculinux" in text or "voculinux" in text, rel
def test_allowlist_denies_user_facing_old_names() -> None:
"""A user-facing old name is never allowlisted, wherever it sits."""
denied = [
("web/src/app/page.tsx", "<h1>Install vocalinux today</h1>"),
("README.md", "Launch vocalinux from your app menu."),
("docs/UPDATE.md", "Your settings live in ~/.local/share/vocalinux."),
("src/vocalinux/ui/tray_indicator.py", 'TRAY_TITLE = "vocalinux"'),
("src/vocalinux/ui/tray_indicator.py", 'ICON = "vocalinux-microphone"'),
("packaging/void/srcpkgs/voculinux/template", "pkgname=vocalinux"),
("docs/INSTALL.md", "Open vocalinux.desktop from the menu."),
]
for path, line in denied:
assert not _allowed(path, line), f"{path}: {line!r} must not be allowlisted"
def test_allowlist_keeps_the_retained_surfaces() -> None:
"""Upstream provenance, the retained package and the legacy import stay allowed."""
kept = [
("README.md", "Upstream: https://github.com/VocaHQ/vocalinux/issues"),
("web/src/app/page.tsx", "Voculinux is a hard fork of Vocalinux for Void Linux."),
("web/src/app/install/[distro]/page.tsx", "installs Vocalinux (upstream), not Voculinux."),
("src/vocalinux/ui/tray_indicator.py", "from vocalinux.utils import paths"),
("src/vocalinux/app_identity.py", 'LEGACY_BUS_NAME: str = "com.vocalinux.Vocalinux"'),
("tests/test_app_identity.py", "from vocalinux import app_identity"),
("packaging/void/container-release.sh", 'grep -E "usr/share/applications/vocalinux"'),
("install.sh", 'local PKG_NAME="vocalinux"'),
]
for path, line in kept:
assert _allowed(path, line), f"{path}: {line!r} is a retained surface"