test(rename): tighten the naming-sweep allowlist to retained surfaces
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.
This commit is contained in:
+156
-92
@@ -1,24 +1,34 @@
|
||||
"""Ticket #18 + #24: offline naming-sweep — no old-name strings on surfaces.
|
||||
"""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 be on the allowlist below:
|
||||
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`` URLs),
|
||||
- the retained internal Python module name (``src/vocalinux`` paths,
|
||||
``vocalinux.*`` imports/attributes, ``-m vocalinux.main``),
|
||||
- retained internal class names (``VocalinuxEngine*``, ``VocalinuxDBusService``),
|
||||
- one-time-legacy import sources (old config/dictionary/bus literals),
|
||||
- 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``),
|
||||
- web upstream surfaces (ticket #24): upstream GitHub/raw/nix flake URLs,
|
||||
nix flake API identifiers, and the nixos-page fork-status note that names
|
||||
the upstream flake as installing Vocalinux rather than Voculinux.
|
||||
``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)
|
||||
|
||||
@@ -34,7 +44,9 @@ SKIP_DIRS = {
|
||||
"out",
|
||||
}
|
||||
SKIP_SUFFIXES = (".egg-info",)
|
||||
SKIP_FILES = {"uv.lock", "CHANGELOG.md"}
|
||||
#: ``.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",
|
||||
@@ -60,98 +72,119 @@ BINARY_EXTS = (
|
||||
#: (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 surface."""
|
||||
low = line.lower()
|
||||
if "vocahq/" in low or "jatinkrmalik/vocalinux" in low:
|
||||
"""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 "vocalinux" in low and "voculinux" in low:
|
||||
# Migration-compat shims that name both installs at once.
|
||||
if _UPSTREAM_SLUG.search(line) or _UPSTREAM_LABEL.search(line):
|
||||
return True
|
||||
if "upstream" in low:
|
||||
# Upstream-provenance mentions that explicitly label the old name
|
||||
# as upstream (fork-status notes, provenance cards).
|
||||
if _MODULE_PATH.search(line) or _DOTTED_MODULE.search(line):
|
||||
return True
|
||||
if "/{name}/vocalinux" in line:
|
||||
# Upstream repo-name template (retained provenance).
|
||||
if _IMPORT_FORM.search(line) or _CAMEL_CLASS.search(line) or _RETAINED_PROSE.search(line):
|
||||
return True
|
||||
if path == "snap/local/check_runtime.py" and '"vocalinux"' in line:
|
||||
# Smoke imports resolve the retained import package.
|
||||
return True
|
||||
if re.search(r'vocalinux\.["a-z_]', line):
|
||||
return True
|
||||
if line.strip().rstrip(",").lstrip("/ ") == '"vocalinux"':
|
||||
# Retained module-dir path segment element in tests and tooling
|
||||
# (single-line and os.path.join element forms).
|
||||
return True
|
||||
if 'site / "vocalinux"' in line or '"platform/vocalinux"' in line:
|
||||
# Fixture module trees named after the retained import package.
|
||||
return True
|
||||
if '"vocalinux/' in line:
|
||||
# Fixture module file paths under the retained package name.
|
||||
return True
|
||||
if "/vocalinux/" in line or "src/vocalinux" in line:
|
||||
return True
|
||||
if "-m vocalinux" in line or "import vocalinux" in line:
|
||||
return True
|
||||
if "VocalinuxEngine" in line or "VocalinuxDBusService" in line:
|
||||
return True
|
||||
if "cmdline" in line:
|
||||
return True
|
||||
if " - vocalinux" in line:
|
||||
# Log logger names keep the retained module path.
|
||||
return True
|
||||
if path in (
|
||||
"src/vocalinux/legacy_import.py",
|
||||
"tests/test_legacy_import.py",
|
||||
):
|
||||
return True
|
||||
if "LEGACY" in line or "legacy" in line:
|
||||
return True
|
||||
if "~/.config/vocalinux" in line or '".config" / "vocalinux"' in line:
|
||||
# Documented one-time-import source (ticket #18).
|
||||
return True
|
||||
if "from vocalinux" in line:
|
||||
return True
|
||||
if '"src"' in line and '"vocalinux"' in line:
|
||||
# Retained module-dir path segments in tests and tooling.
|
||||
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/output names, module
|
||||
# paths, and programs.* options spell the retained upstream API.
|
||||
# The page carries a fork-status note stating those snippets install
|
||||
# Vocalinux (upstream), not Voculinux.
|
||||
# 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 == "tests/test_rename_sweep.py":
|
||||
if path.startswith(_TOOLING_PREFIXES) and _PACKAGE_SEGMENT.search(line):
|
||||
return True
|
||||
if path == "pyproject.toml" and re.search(r'"vocalinux[."\'\]]', line):
|
||||
if path == "packaging/void/container-main.sh" and _PRE_RENAME_GUARD.search(line):
|
||||
return True
|
||||
if path == "pyproject.toml" and "vocalinux.version" in line:
|
||||
return True
|
||||
if 'PKG_NAME="vocalinux"' in line:
|
||||
# Installer probes the retained import package, not the distribution.
|
||||
return True
|
||||
if path == "packaging/void/srcpkgs/voculinux/template" and "vocalinux-${version}" in line:
|
||||
# Pre-rename release asset root (ticket #27); dropped at the first
|
||||
# post-rename release bump.
|
||||
return True
|
||||
if path == "tests/test_void_app_template.py" and '"vocalinux"' in line:
|
||||
# The app-template policy test asserts the old name is gone.
|
||||
return True
|
||||
if path == "tests/test_void_app_template.py" and "usr/bin/vocalinux" in line:
|
||||
# Pre-rename sdist launchers the template promotes at install time
|
||||
# (ticket #28); dropped at the first post-rename release bump.
|
||||
return True
|
||||
if path == "packaging/void/container-main.sh" and (
|
||||
"usr/share/applications/vocalinux" in line or 'version("vocalinux")' in line
|
||||
if path == "src/vocalinux/utils/update_checker.py" and (
|
||||
'casefold() == "vocalinux"' in line or '"/{name}/vocalinux"' in line
|
||||
):
|
||||
# Pre-rename sdist compat (ticket #28): the payload guard names the
|
||||
# old file names it rejects, and the import smoke queries the old
|
||||
# distribution name. Dropped at the first post-rename release bump.
|
||||
return True
|
||||
if '== "vocalinux"' in line or '.startswith("vocalinux' in line:
|
||||
# Retained module identity comparisons in test isolation helpers.
|
||||
# Upstream repository identity the update checker recognises.
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -194,3 +227,34 @@ def test_owned_web_files_carry_new_name() -> None:
|
||||
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"
|
||||
|
||||
Reference in New Issue
Block a user