The allowlist accepted any line that mentioned upstream, legacy, cmdline, a dotted name or a path segment, so a user-facing old name could pass by sharing a line with one of those words. Each rule is now scoped to a retained surface: upstream repository slugs and labelled upstream prose, dotted references to modules that exist under src/vocalinux, import and -m forms, retained class names, legacy import sources in the files that implement or test the import, quoted package names in tooling and test paths only, and the pre-rename guard in the Void container harness. The sweep skips the .git worktree pointer file, and the allowlist itself has behaviour tests: user-facing names are denied and retained surfaces are kept.
261 lines
10 KiB
Python
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-main.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-main.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"
|