Files
voculinux/tests/test_rename_sweep.py
T
xavierk 4171567fd7
CI / security (push) Successful in 1m12s
CI / lint (push) Successful in 21s
CI / ai-review (push) Skipped
docs: publish Void xbps as the only install channel, drop upstream references (#38)
User-facing docs, About page and website now point at the fork and the Void xbps repository, with one credit line to upstream Vocalinux. Channels this fork does not publish are removed from navigation.
2026-11-06 16:17:04 +00:00

364 lines
15 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.
The Void release cleanup adds the fork surfaces (``FORK_SURFACES``): README,
the support and security docs, the install docs, the About page and the Void
install message. On those, upstream references are not allowlisted by pattern
at all. An upstream URL, handle or host is a regression unless the line is
exactly the one credit line recorded for that file.
``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__"}
#: The one upstream credit per fork surface, matched as whole stripped lines.
CREDIT_LINE = "Voculinux is a hard fork of Vocalinux by VocaHQ: https://github.com/VocaHQ/vocalinux"
FORK_SURFACE_CREDITS: dict[str, frozenset[str]] = {
"README.md": frozenset({CREDIT_LINE}),
"CONTRIBUTING.md": frozenset({CREDIT_LINE}),
"src/vocalinux/ui/settings_about.py": frozenset(
{
'UPSTREAM_REPO_URL = "https://github.com/VocaHQ/vocalinux"',
'UPSTREAM_CREDIT_LINE = f"Voculinux is a hard fork of Vocalinux by VocaHQ: {UPSTREAM_REPO_URL}"',
}
),
}
FORK_SURFACES = frozenset(
{
"README.md",
"SECURITY.md",
"SUPPORT.md",
"CONTRIBUTING.md",
"docs/UPDATE.md",
"docs/INSTALL.md",
"docs/TROUBLESHOOTING.md",
"docs/DISTRO_COMPATIBILITY.md",
"src/vocalinux/ui/settings_about.py",
"src/vocalinux/ui/settings_dialog.py",
"packaging/void/srcpkgs/voculinux/INSTALL.msg",
}
)
#: Upstream hosts, handles and URL forms. Plain words such as "Flathub" are not
#: listed, so a sentence that says a channel is not published stays legal.
UPSTREAM_REFERENCE = re.compile(
r"VocaHQ|vocahq|jatinkrmalik|raw\.githubusercontent|discord\.gg|(?<![\w.])x\.com/"
r"|mailto:|codeberg|vocamac\.com|vocawin\.com|vocaphone\.|vocagateway\.|flathub\.org"
r"|dl\.flathub"
r"|snapcraft\.io|aur\.archlinux|pypi\.org/project/voculinux|github:|star-history"
r"|contrib\.rocks"
r"|shields\.io/(?:aur|pypi|discord|github)",
re.IGNORECASE,
)
_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")
WEB_REBRAND_TEST = "web/src/lib/__tests__/voculinux-rebrand.test.ts"
WEB_REBRAND_SCANNER_LINES = frozenset(
{
"const OLD_NAME = /Vocalinux/;",
'expect(home).not.toContain("Then launch <code>vocalinux</code>");',
}
)
def _fork_surface_allowed(path: str, line: str) -> bool:
"""Fork surfaces allow the exact credit, retained module references and legacy prose."""
if line.strip() in FORK_SURFACE_CREDITS.get(path, frozenset()):
return True
if UPSTREAM_REFERENCE.search(line):
return False
return bool(
_MODULE_PATH.search(line)
or _DOTTED_MODULE.search(line)
or _IMPORT_FORM.search(line)
or _CAMEL_CLASS.search(line)
or _RETAINED_PROSE.search(line)
or _LEGACY_PROSE.search(line)
)
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 path in FORK_SURFACES:
return _fork_surface_allowed(path, line)
if path == WEB_REBRAND_TEST and line.strip() in WEB_REBRAND_SCANNER_LINES:
# The web rebrand test names the old word only to scan for it.
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 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_fork_surfaces_name_no_upstream_but_the_one_credit() -> None:
"""A fork surface may name upstream only on its recorded credit line."""
offenders: list[str] = []
for rel in sorted(FORK_SURFACES):
with open(os.path.join(REPO_ROOT, rel), encoding="utf-8") as handle:
text = handle.read()
credits = FORK_SURFACE_CREDITS.get(rel, frozenset())
for number, line in enumerate(text.splitlines(), 1):
if UPSTREAM_REFERENCE.search(line) and line.strip() not in credits:
offenders.append(f"{rel}:{number}:{line.strip()[:160]}")
assert offenders == [], "upstream reference outside the credit:\n" + "\n".join(offenders)
def test_fork_surfaces_carry_their_credit_exactly_once() -> None:
"""The credit is present, once, on every file that records one."""
for rel, credits in sorted(FORK_SURFACE_CREDITS.items()):
with open(os.path.join(REPO_ROOT, rel), encoding="utf-8") as handle:
lines = [line.strip() for line in handle.read().splitlines()]
for credit in credits:
assert lines.count(credit) == 1, f"{rel}: credit line must appear exactly once"
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 = [
("README.md", "Upstream: https://github.com/VocaHQ/vocalinux/issues"),
("README.md", "Join us on [Discord](https://discord.gg/t6muquAJbm)."),
("docs/INSTALL.md", "flatpak install vocahq com.voculinux.Voculinux"),
("SUPPORT.md", "| [Discussions](https://github.com/VocaHQ/vocalinux/discussions) |"),
("SECURITY.md", "Email hello@vocahq.com for security reports."),
("src/vocalinux/ui/settings_about.py", 'VOCAHQ_X_URL = "https://x.com/vocahq"'),
(
"src/vocalinux/ui/settings_dialog.py",
'MODEL_SELECTION_DOC_URL = "https://github.com/VocaHQ/vocalinux/x"',
),
(
"packaging/void/srcpkgs/voculinux/INSTALL.msg",
"Report bugs at github.com/VocaHQ/vocalinux",
),
("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", CREDIT_LINE),
("CONTRIBUTING.md", CREDIT_LINE),
(
"src/vocalinux/ui/settings_about.py",
'UPSTREAM_REPO_URL = "https://github.com/VocaHQ/vocalinux"',
),
("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"