"""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", "knulli", "amberelec", "emuelec", ) # 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()