diff --git a/tests/test_profile_paths.py b/tests/test_profile_paths.py new file mode 100644 index 00000000..3df44b9c --- /dev/null +++ b/tests/test_profile_paths.py @@ -0,0 +1,77 @@ +"""A standalone app's paths are the folders its code opens below data_root. + +The site prints data_root joined to each entry's standalone destination, so a +frontend's layout written there (Batocera's bios/switch/) sends the reader to a +folder the app never reads. A frontend folder name is allowed only where the +app's own code joins that folder to its data root. +""" + +from __future__ import annotations + +import unittest +from pathlib import Path + +import yaml + +REPO_ROOT = Path(__file__).resolve().parents[1] + +FRONTEND_FOLDERS = ("bios/", "switch/", "system/") + +# Profiles whose code reads one of those folders below its data root. +CODE_READS = { + # BIOS folder of the data directory, the PCSX2 layout it carries + # (aethersx2 profile notes, AetherSX2-release MacOS/AetherSX2). + "aethersx2": {"bios/"}, + # BIOS_FOLDER under gui_data_folder() (src/core/fds.c:305, src/core/cheat.c:105). + "punes": {"bios/"}, + # KeysDir "system" under BaseDirPath + # (src/Ryujinx.Common/Configuration/AppDataManager.cs:17, 229). + "ryujinx": {"system/"}, + "kenji-nx": {"system/"}, + # PathSystem is the system folder beside DOSBoxPure.cfg (main.cpp:2032-2039). + "dosbox-pure-unleashed": {"system/"}, +} + + +def _standalone_profiles() -> dict[str, dict]: + profiles = {} + for path in sorted((REPO_ROOT / "emulators").glob("*.yml")): + with path.open(encoding="utf-8") as fh: + profile = yaml.safe_load(fh) + if isinstance(profile, dict) and profile.get("data_root") \ + and profile.get("type") == "standalone": + profiles[path.stem] = profile + return profiles + + +def _destination(entry: dict) -> str: + return str(entry.get("standalone_path") or entry.get("path") or "") + + +class StandaloneDestinations(unittest.TestCase): + def test_no_frontend_folder_below_data_root(self): + wrong = [] + for name, profile in _standalone_profiles().items(): + allowed = CODE_READS.get(name, set()) + for entry in profile.get("files") or []: + if not isinstance(entry, dict): + continue + dest = _destination(entry) + for folder in FRONTEND_FOLDERS: + if dest.startswith(folder) and folder not in allowed: + wrong.append(f"{name}: {entry.get('name')} at {dest}") + self.assertEqual(wrong, []) + + def test_every_allowed_folder_is_read(self): + profiles = _standalone_profiles() + unused = [] + for name, folders in CODE_READS.items(): + dests = [_destination(e) for e in profiles.get(name, {}).get("files") or [] + if isinstance(e, dict)] + unused += [f"{name}: {folder}" for folder in sorted(folders) + if not any(d.startswith(folder) for d in dests)] + self.assertEqual(unused, []) + + +if __name__ == "__main__": + unittest.main()