chore: merge audit fixes (profile sources)

The flycast standalone paths (eb04fb50) stay out until the packs read the
platforms' launcher links. Two rosalies-mupen-gui aliases that only
RetroBat's launcher writes leave the profile, now that its layer
records them.
This commit is contained in:
Abdessamad Derraz committed 2026-10-21 11:55:22 +02:00
commit d49fcfd59e
36 files changed
+382 -157

No files matched your search

+153
View File
@@ -0,0 +1,153 @@
"""A profile alias is a name the emulator's own code opens.
Platform launchers pick names of their own: Batocera passes cerbios.bin to
xemu, ROCKNIX and EmuDeck write Complex_4627v1.03.bin into xemu.toml. Those
names live in the platform's ``launchers:`` layer. Copied into a profile as
aliases, they claimed that xemu opens them, and xemu-libretro offers
Complex_4627_v1.03.bin, not Complex_4627v1.03.bin (ui/xemu-libretro.c:115-118).
Two checks read the committed data:
- an alias equal to a name a launcher of the same emulator chooses is kept
only when the entry's own note or description names it in a sentence that
speaks of no platform, next to a ``source_ref`` into the emulator's code;
- no entry cites a platform's launcher script as its source.
"""
from __future__ import annotations
import re
import unittest
from collections import defaultdict
from pathlib import Path
import yaml
REPO_ROOT = Path(__file__).resolve().parent.parent
PLATFORMS = REPO_ROOT / "platforms"
EMULATORS = REPO_ROOT / "emulators"
# Words that name a platform in prose. MiSTer's registry key carries its
# hardware suffix, the prose says MiSTer.
PLATFORM_WORDS = (
"retroarch", "batocera", "recalbox", "retrobat", "emudeck", "lakka",
"retrodeck", "romm", "rocknix", "retropie", "bizhawk", "mister",
)
# Launcher sources: configgen and emulatorLauncher generators, EmuDeck and
# RetroPie scripts, a ref annotated as a launcher.
LAUNCHER_REF = re.compile(
r"Generator\.(?:py|cs)\b|\bconfiggen\b|\bemulatorLauncher\b|"
r"\bemuDeck\w*\.sh\b|\bscriptmodules/|\(launcher\)",
)
# Profiles whose entries still rest on a launcher, with the fact that blocks
# reading the emulator itself.
LAUNCHER_SOURCED = {
"lexaloffle": "PICO-8 and Voxatron are sold binaries, and no copy of either is held to read",
}
_SENTENCE = re.compile(r"(?<=[.!?])\s+(?=[A-Z0-9(\"'])")
def _profiles() -> dict[str, dict]:
return {
path.stem: yaml.safe_load(path.read_text(encoding="utf-8")) or {}
for path in sorted(EMULATORS.glob("*.yml"))
}
def _launcher_names() -> dict[str, set[tuple[str, str]]]:
"""Folded file name -> (platform, emulator) of each launcher choosing it."""
names: dict[str, set[tuple[str, str]]] = defaultdict(set)
for path in sorted(PLATFORMS.glob("*.yml")):
config = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
for record in config.get("launchers") or []:
for name in record.get("names") or []:
base = str(name).rsplit("/", 1)[-1].casefold()
names[base].add((path.stem, str(record.get("emulator", ""))))
return names
def _text(value: object) -> str:
if isinstance(value, dict):
return " ".join(_text(v) for v in value.values())
if isinstance(value, list):
return " ".join(_text(v) for v in value)
return str(value or "")
def _named_without_platform(alias: str, prose: str) -> bool:
for sentence in _SENTENCE.split(" ".join(prose.split())):
if alias not in sentence:
continue
folded = sentence.casefold()
if not any(re.search(rf"\b{word}", folded) for word in PLATFORM_WORDS):
return True
return False
class ProfileAliasesAreCodeNames(unittest.TestCase):
def test_alias_a_launcher_chooses_is_named_by_the_code(self) -> None:
launchers = _launcher_names()
offenders = []
for key, profile in _profiles().items():
emulator_names = {key} | {str(c) for c in profile.get("cores") or []}
for entry in profile.get("files") or []:
prose = _text(entry.get("note")) + " " + _text(entry.get("description"))
for alias in entry.get("aliases") or []:
alias = str(alias)
chosen_by = {
platform
for platform, emulator in launchers.get(
alias.rsplit("/", 1)[-1].casefold(), ()
)
if emulator in emulator_names
}
if not chosen_by:
continue
if entry.get("source_ref") and _named_without_platform(alias, prose):
continue
offenders.append(
f"{key}: {entry.get('name')} alias {alias} "
f"(launchers: {', '.join(sorted(chosen_by))})"
)
self.assertEqual(
offenders, [],
"an alias a platform launcher chooses must be named by the emulator's "
"code, stated in the entry's note with its source_ref, or removed",
)
def test_check_catches_a_launcher_name(self) -> None:
self.assertFalse(_named_without_platform(
"cerbios.bin", "Batocera writes bios/cerbios.bin into flashrom_path.",
))
self.assertTrue(_named_without_platform(
"cerbios.bin", "The core option offers cerbios.bin. Batocera writes it.",
))
class EntriesCiteTheEmulator(unittest.TestCase):
def test_no_source_ref_is_a_launcher_script(self) -> None:
offenders = []
for key, profile in _profiles().items():
if key in LAUNCHER_SOURCED:
continue
for entry in profile.get("files") or []:
ref = _text(entry.get("source_ref"))
if LAUNCHER_REF.search(ref):
offenders.append(f"{key}: {entry.get('name')}: {ref}")
self.assertEqual(offenders, [])
def test_known_exceptions_still_need_it(self) -> None:
profiles = _profiles()
for key in LAUNCHER_SOURCED:
refs = [_text(e.get("source_ref")) for e in profiles[key].get("files") or []]
self.assertTrue(
any(LAUNCHER_REF.search(r) for r in refs),
f"{key} no longer cites a launcher: drop it from LAUNCHER_SOURCED",
)
if __name__ == "__main__":
unittest.main()
+55
View File
@@ -0,0 +1,55 @@
"""A profile names each repository under the name its forge answers today.
GitHub follows a renamed or transferred repository's old name in silence:
flycast2021 declared libretro/flycast, which answers as libretro/parallel-dc,
and reicast declared reicast/reicast-emulator, which answers as
skmp/reicast-emulator. profile_sync counts such a move as review work. This
test reads the forge answers profile_sync has cached and fails on any
profile still declaring an old name. Without a cache it has nothing to read
and skips.
"""
from __future__ import annotations
import sys
import unittest
from pathlib import Path
import yaml
REPO_ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO_ROOT / "scripts"))
import upstream # noqa: E402
from profile_sync import declared_repositories # noqa: E402
CACHE = Path(upstream.CACHE_DIR)
if not CACHE.is_absolute():
CACHE = REPO_ROOT / CACHE
class DeclaredRepositoriesAreCurrent(unittest.TestCase):
def test_no_profile_declares_a_moved_repository(self) -> None:
if not (CACHE / "_api").is_dir():
self.skipTest("no forge answers cached")
checked = 0
moved = []
for path in sorted((REPO_ROOT / "emulators").glob("*.yml")):
profile = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
for field, _key, url in declared_repositories(profile):
repo = upstream.parse_repo(url)
if repo is None:
continue
current = upstream.current_slug(repo, str(CACHE), offline=True)
if current is None:
continue
checked += 1
if current.lower() != repo.slug.lower():
moved.append(f"{path.stem} {field}: {repo.slug} answers as {current}")
if not checked:
self.skipTest("no declared repository has a cached forge answer")
self.assertEqual(moved, [])
if __name__ == "__main__":
unittest.main()