Files
libretro/tests/test_romfolders_card.py
T
Abdessamad Derraz 057fd2d0f6 fix: isolate every test module however it runs
tests/__init__.py keeps a run offline, its scratch under tmp/ and its
installer cache its own, but only discover -s tests -t . imported it.
discover tests ran with RETROBIOS_OFFLINE unset, scratch on the /tmp
tmpfs and the user's real installer cache, the setting in which e2e
fixtures had written into the shared .cache/large. Every test module now
imports the package first, and a test fails on a module that does not.
test_verify_listing.py took its line with the previous commit.
2026-11-06 15:14:05 +01:00

294 lines
13 KiB
Python

"""Rom folder parsers of the card OSes and the menu-less frontends, offline.
The parsers run on fixture text through a fake Fetcher, so nothing here
reaches the network. The committed setup files are checked against the
schema and their own declared sources.
"""
from __future__ import annotations
import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache
import json
import sys
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT / "scripts"))
from romfolders import split_refs # noqa: E402
from safeparse import yaml_load # noqa: E402
from scraper.romfolders import SourceError, load_sources, scrape_setup # noqa: E402
from scraper.romfolders.__main__ import dump_setup # noqa: E402
from scraper.romfolders.card import ( # noqa: E402
libretro_core,
parse_allium,
parse_bizhawk,
parse_crossmix,
parse_gmenu2x,
parse_koriki,
parse_minui,
parse_mister,
parse_muos,
parse_onion,
parse_plumos,
parse_romm,
parse_spruce,
parse_toml_subset,
ref,
)
FIXTURES = json.loads(
(ROOT / "tests" / "fixtures" / "romfolders_card" / "files.json").read_text(encoding="utf-8")
)
SETUPS = (
"onion", "allium", "spruceos", "miyoocfw", "koriki", "crossmix-os", "minui",
"myminui", "nextui", "plumos-pixel2", "plumos-mf", "muos", "misterfpga", "romm",
"bizhawk", "lakka", "retroarch",
)
class FakeFetcher:
"""Serves fixture files by source key, the Fetcher interface parsers use."""
def __init__(self, files: dict[str, dict[str, str]]) -> None:
self.files = files
def text(self, key: str, path: str) -> str:
try:
return self.files[key][path]
except KeyError as exc:
raise SourceError(f"{key}:{path} does not exist") from exc
def optional_text(self, key: str, path: str) -> str | None:
return self.files.get(key, {}).get(path)
def tree(self, key: str, prefix: str = "") -> list[str]:
return sorted(p for p in self.files.get(key, {}) if p.startswith(prefix))
def spec(source: str, **params: object) -> dict:
return {"params": {"source": source, **params}}
def by_folder(rows: list[dict]) -> dict[str, dict]:
return {row["folder"]: row for row in rows}
class LaunchScriptTests(unittest.TestCase):
def test_core_named_through_a_shell_variable(self) -> None:
script = "default_core=gpsp\nretroarch -L cores/${default_core}_libretro.so\n"
self.assertEqual(libretro_core(script), "gpsp")
def test_core_named_by_emu_exe(self) -> None:
self.assertEqual(libretro_core("EMU_EXE=fceumm\nminarch.elf x\n"), "fceumm")
def test_standalone_script_names_no_core(self) -> None:
self.assertIsNone(libretro_core("./drastic \"$1\"\n"))
def test_space_in_a_cited_path_is_escaped(self) -> None:
part = ref("os", "Emu/Commodore - Amiga/config.json", 3)
self.assertEqual(split_refs(part), [("os", "Emu/Commodore%20-%20Amiga/config.json", "3")])
class ParserTests(unittest.TestCase):
def setUp(self) -> None:
self.fetcher = FakeFetcher(FIXTURES)
def test_onion_emu_package_is_the_default_over_retroarch_section(self) -> None:
rows = by_folder(parse_onion({"params": {"source": "onion"}}, self.fetcher))
self.assertEqual(sorted(rows), ["GBA", "PCNINETYEIGHT"])
self.assertEqual(rows["GBA"]["core"], "gpsp")
self.assertEqual(rows["GBA"]["extensions"], [".gba", ".zip", ".7z"])
self.assertIn("Nintendo - GBA (VBA-M)", rows["GBA"]["note"])
self.assertNotIn("core", rows["PCNINETYEIGHT"])
def test_spruce_reads_default_and_alternative_folders(self) -> None:
rows = by_folder(parse_spruce(spec("spruce", skip=["-CUSTOM-SYSTEM-"]), self.fetcher))
self.assertEqual(list(rows), ["PS"])
self.assertEqual(rows["PS"]["aliases"], ["psx", "PS1"])
self.assertEqual(rows["PS"]["core"], "pcsx_rearmed")
self.assertEqual(rows["PS"]["extensions"], [".bin", ".cue", ".chd"])
self.assertIn("neogeo.zip", rows["PS"]["note"])
def test_crossmix_skips_separators_and_underscore_folders(self) -> None:
rows = by_folder(parse_crossmix(spec("crossmix"), self.fetcher))
self.assertEqual(list(rows), ["PS"])
self.assertEqual(rows["PS"]["core"], "pcsx_rearmed")
def test_koriki_first_launcher_wins(self) -> None:
params = {"source": "koriki", "sections": "base/.simplemenu/section_groups/apps and games.ini",
"launchers": "base/.simplemenu/launchers", "roms_prefix": "/mnt/SDCARD/Roms/",
"skip": ["CONSOLES"]}
rows = by_folder(parse_koriki({"params": params}, self.fetcher))
self.assertEqual(rows["PS"]["core"], "pcsx_rearmed")
self.assertEqual(rows["PS"]["extensions"], [".cue", ".chd", ".pbp"])
self.assertEqual(rows["PS"]["source_ref"],
"koriki base/.simplemenu/section_groups/apps%20and%20games.ini:4")
def test_allium_folder_is_the_first_pattern(self) -> None:
rows = by_folder(parse_allium(spec("allium", consoles="consoles.toml", cores="cores.toml"),
self.fetcher))
self.assertEqual(sorted(rows), ["ARCADE", "GBA"])
self.assertEqual(rows["ARCADE"]["core"], "mame2003_plus")
self.assertEqual(rows["ARCADE"]["aliases"], ["mame"])
self.assertEqual(rows["ARCADE"]["source_ref"], "allium consoles.toml:1")
def test_toml_subset_matches_tomllib(self) -> None:
try:
import tomllib
except ImportError:
self.skipTest("tomllib needs Python 3.11")
def clean(node: object) -> object:
if isinstance(node, dict):
return {k: clean(v) for k, v in node.items() if k != "__line__"}
if isinstance(node, list):
return [clean(v) for v in node]
return node
for name in ("consoles.toml", "cores.toml"):
text = FIXTURES["allium"][name]
self.assertEqual(clean(parse_toml_subset(text)), tomllib.loads(text))
def test_toml_outside_the_subset_is_refused(self) -> None:
with self.assertRaises(SourceError):
parse_toml_subset("when = 1979-05-27T07:32:00Z\n")
def test_minui_takes_the_core_most_devices_run(self) -> None:
rows = by_folder(parse_minui(spec("minui"), self.fetcher))
ps = rows["Sony PlayStation (PS)"]
self.assertEqual(ps["name"], "PS")
self.assertEqual(ps["core"], "pcsx_rearmed")
self.assertEqual(ps["board_defaults"]["tg5040"]["core"], "swanstation")
self.assertNotIn("rg35xx", ps["board_defaults"])
self.assertEqual(rows["A2600"]["core"], "stella2014")
self.assertNotIn("extensions", ps)
def test_plumos_shared_folder_goes_to_the_first_system(self) -> None:
rows = by_folder(parse_plumos(spec("plumos", path="systems.json"), self.fetcher))
self.assertEqual(rows["FC"]["name"], "nes")
self.assertEqual(rows["FC"]["core"], "quicknes")
self.assertEqual(rows["fds"]["emulator"], "standalone")
self.assertIn("FC", rows["fds"]["aliases"])
self.assertIn("disabled by default", rows["fds"]["note"])
def test_muos_folder_is_the_friendly_name_of_the_catalogue(self) -> None:
rows = by_folder(parse_muos(spec("muos", path="core.json"), self.fetcher))
self.assertEqual(rows["sonyplaystation"]["core"], "pcsx_rearmed")
self.assertEqual(rows["sonyplaystation"]["emulator"], "retroarch")
self.assertEqual(rows["nintendods"]["core"], "drastic")
self.assertEqual(rows["nintendods"]["emulator"], "external")
def test_gmenu2x_link_and_shipped_folders(self) -> None:
params = {"source": "gmenu2x", "shipped_roms": "board/miyoo/roms",
"link_roms_prefix": "/mnt/roms",
"links": [{"source": "gpsp", "path": "miyoo/opkg_assets/gpsp.lnk", "core": "gpsp"}]}
rows = by_folder(parse_gmenu2x({"params": params}, self.fetcher))
self.assertEqual(sorted(rows), ["GB", "GBA"])
self.assertEqual(rows["GBA"]["extensions"], [".gba", ".zip"])
self.assertEqual(rows["GBA"]["source_ref"], "gpsp miyoo/opkg_assets/gpsp.lnk:3")
self.assertNotIn("core", rows["GB"])
def test_mister_rows_are_the_shipped_core_folders(self) -> None:
rows = by_folder(parse_mister(spec("dist", arcade=["mame"], arcade_note="mra"), self.fetcher))
self.assertEqual(sorted(rows), ["SNES", "mame"])
self.assertTrue(rows["mame"]["arcade"])
self.assertIsNone(rows["SNES"]["arcade"])
def test_romm_reads_multiline_arrays_and_bios_only_slugs(self) -> None:
rows = by_folder(parse_romm(spec("romm", cores_map="index.ts",
bios_fixture="known_bios_files.json"), self.fetcher))
self.assertEqual(sorted(rows), ["3do", "3ds", "64dd", "arcade"])
self.assertEqual(rows["arcade"]["core"], "mame2003_plus")
self.assertIn("nightly", rows["3ds"]["note"])
self.assertNotIn("core", rows["64dd"])
def test_bizhawk_combined_systems_and_extension_lists(self) -> None:
rows = by_folder(parse_bizhawk(spec("bizhawk", paths="PathEntryCollection.cs",
rom_loader="RomLoader.cs", config="Config.cs"),
self.fetcher))
self.assertEqual(rows["Gameboy"]["name"], "GB")
self.assertEqual(rows["Gameboy"]["aliases"], ["GBC", "SGB"])
self.assertEqual(rows["Gameboy"]["extensions"], [".gb", ".gbc", ".sgb"])
self.assertEqual(rows["NES"]["extensions"], [".nes", ".fds", ".unf"])
self.assertIn("QuickNes", rows["NES"]["note"])
self.assertEqual(rows["32X"]["extensions"], [])
self.assertNotIn("core", rows["NES"])
class ScrapeTests(unittest.TestCase):
def test_two_scrapes_write_the_same_bytes(self) -> None:
setup = {
"name": "Fixture", "platform": "retroarch", "format": "plumos",
"params": {"source": "plumos", "path": "systems.json"},
"sources": {"plumos": {"repo": "https://github.com/a/b", "commit": "0" * 40}},
"roms_root": {"path": "/r", "from_bios": "../r", "source_ref": "plumos systems.json:1"},
"case_sensitive": None,
}
first = dump_setup(scrape_setup("fixture", setup, FakeFetcher(FIXTURES)))
second = dump_setup(scrape_setup("fixture", setup, FakeFetcher(FIXTURES)))
self.assertEqual(first, second)
self.assertEqual(yaml_load(first)["folders"][0]["folder"], "FC")
def test_an_empty_read_fails_unless_allowed(self) -> None:
setup = {"format": "mister", "params": {"source": "none", "arcade_note": "x"}}
with self.assertRaises(SourceError):
scrape_setup("empty", setup, FakeFetcher({}))
class CommittedSetupTests(unittest.TestCase):
def setUp(self) -> None:
self.sources = load_sources(ROOT / "platforms" / "romfolders" / "_sources")
def documents(self) -> dict[str, dict]:
out = {}
for setup in SETUPS:
path = ROOT / "platforms" / "romfolders" / f"{setup}.yml"
with path.open(encoding="utf-8") as handle:
out[setup] = yaml_load(handle)
return out
def test_every_owned_setup_has_sources_and_output(self) -> None:
for setup in SETUPS:
self.assertIn(setup, self.sources, setup)
for setup, doc in self.documents().items():
self.assertEqual(doc["setup"], setup)
self.assertEqual(doc["platform"], self.sources[setup]["platform"])
def test_every_ref_names_a_declared_source(self) -> None:
for setup, doc in self.documents().items():
declared = set(doc["sources"])
refs = [doc["roms_root"]["source_ref"]]
refs += [doc["case_ref"]] if doc.get("case_ref") else []
for block in ("skip", "hidden", "media"):
refs += [r["source_ref"] for r in doc.get(block, [])]
refs += [row["source_ref"] for row in doc["folders"]]
refs += [b["source_ref"] for row in doc["folders"]
for b in (row.get("board_defaults") or {}).values()]
for source_ref in refs:
for key, _, _ in split_refs(source_ref):
self.assertIn(key, declared, f"{setup}: {source_ref}")
def test_committed_setups_validate_against_the_schema(self) -> None:
try:
from jsonschema import Draft202012Validator
except ImportError:
self.skipTest("jsonschema not installed")
schema = json.loads((ROOT / "schemas" / "romfolders.schema.json").read_text(encoding="utf-8"))
validator = Draft202012Validator(schema)
for setup, doc in self.documents().items():
errors = [e.message for e in validator.iter_errors(doc)]
self.assertEqual(errors, [], setup)
def test_rows_with_folders_are_not_empty(self) -> None:
for setup, doc in self.documents().items():
if self.sources[setup].get("allow_empty"):
self.assertEqual(doc["folders"], [], setup)
else:
self.assertGreater(len(doc["folders"]), 0, setup)
if __name__ == "__main__":
unittest.main()