diff --git a/scripts/generate_site.py b/scripts/generate_site.py index bdc8f794..031221d7 100644 --- a/scripts/generate_site.py +++ b/scripts/generate_site.py @@ -39,6 +39,8 @@ from common import ( load_emulator_profiles, load_provenance_snapshots, require_yaml, + resolution_is_hash_exact, + resolve_local_file, resolve_platform_cores, unique_emulator_profiles, write_if_changed as _write_artifact, @@ -121,6 +123,26 @@ CLS_LABELS = { } +def _system_folder( + files: list[dict], db: dict, name_consoles: dict[str, set[tuple[str, str]]] +) -> tuple[str, str] | None: + """The bios// folder a system's files live in.""" + votes: dict[tuple[str, str], int] = {} + for fe in files: + local, status = resolve_local_file(fe, db) + parts = (local or "").split("/") + if len(parts) >= 4 and parts[0] == "bios" and resolution_is_hash_exact(status): + place = (parts[1], parts[2]) + votes[place] = votes.get(place, 0) + 1 + if votes: + return max(sorted(votes), key=lambda place: votes[place]) + for fe in files: + places = name_consoles.get(fe.get("name", ""), set()) + if len(places) == 1: + return next(iter(places)) + return None + + def _build_system_page_map_from_data( manufacturers: dict, coverages: dict, @@ -130,15 +152,14 @@ def _build_system_page_map_from_data( Uses platform file paths to trace system_id -> bios directory -> manufacturer page. """ - db.get("files", {}) - db.get("indexes", {}).get("by_name", {}) - - # Build reverse index: filename -> (manufacturer, console) from bios/ structure - file_to_console: dict[str, tuple[str, str]] = {} + # Every console folder a filename sits in. A name in one folder only can + # place a system; a name in several says nothing (MiSTer's boot.rom lives + # under a dozen consoles, and the last one written used to win). + name_consoles: dict[str, set[tuple[str, str]]] = {} for mfr, consoles in manufacturers.items(): for console, entries in consoles.items(): for entry in entries: - file_to_console[entry["name"]] = (mfr, console) + name_consoles.setdefault(entry["name"], set()).add((mfr, console)) # Build normalized console name index for fuzzy matching console_norm: dict[str, tuple[str, str]] = {} @@ -170,15 +191,13 @@ def _build_system_page_map_from_data( for sys_id, system in config.get("systems", {}).items(): if sys_id in _system_page_map: continue - # Strategy 1: trace via file paths in DB - for fe in system.get("files", []): - fname = fe.get("name", "") - if fname in file_to_console: - mfr, console = file_to_console[fname] - slug = mfr.lower().replace(" ", "-") - _system_page_map[sys_id] = (slug, console) - break - if sys_id in _system_page_map: + # Strategy 1: the folder holding the files the system declares, + # found by content; most votes win. A bare name stands in only + # where the collection files it in a single console. + place = _system_folder(system.get("files", []), db, name_consoles) + if place is not None: + mfr, console = place + _system_page_map[sys_id] = (mfr.lower().replace(" ", "-"), console) continue # Strategy 2: fuzzy match system_id against console directory names if sys_id in console_norm: diff --git a/tests/test_site_system_links.py b/tests/test_site_system_links.py new file mode 100644 index 00000000..b2d9bc0b --- /dev/null +++ b/tests/test_site_system_links.py @@ -0,0 +1,60 @@ +"""A platform system links to the page of the folder its files live in. + +The site mapped each system by the bare name of its first file, the last +console written winning: MiSTer's boot.rom sits under a dozen consoles, so +wonderswan and tiger-game-com linked to VideoBrain, ngpc and tandy-coco to +TI-99. Reads the repository: skips when the database is absent. +""" + +from __future__ import annotations + +import os +import sys +import unittest +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(REPO_ROOT / "scripts")) + +import generate_site as gs # noqa: E402 + + +class LinksFollowContent(unittest.TestCase): + def test_an_ambiguous_name_places_nothing(self): + names = {"boot.rom": {("VideoBrain", "VideoBrain"), ("Bandai", "WonderSwan")}} + files = [{"name": "boot.rom", "sha1": "0" * 40}] + self.assertIsNone(gs._system_folder(files, {"files": {}, "indexes": {}}, names)) + names["only.rom"] = {("Bandai", "WonderSwan")} + files.append({"name": "only.rom"}) + self.assertEqual( + gs._system_folder(files, {"files": {}, "indexes": {}}, names), + ("Bandai", "WonderSwan"), + ) + + def test_mister_systems(self): + if not (REPO_ROOT / "database.json").exists(): + self.skipTest("database.json is not built") + previous = os.getcwd() + os.chdir(REPO_ROOT) + self.addCleanup(os.chdir, previous) + from common import load_database, load_platform_config + + db = load_database("database.json") + config = load_platform_config("misterfpga") + names: dict = {} + for mfr, consoles in gs._group_by_manufacturer(db).items(): + for console, entries in consoles.items(): + for entry in entries: + names.setdefault(entry["name"], set()).add((mfr, console)) + expected = {"wonderswan": "WonderSwan", "ngpc": "Neo Geo Pocket"} + for sys_id, console in expected.items(): + system = config["systems"].get(sys_id) + if system is None: + continue + with self.subTest(system=sys_id): + place = gs._system_folder(system.get("files", []), db, names) + self.assertEqual(place and place[1], console) + + +if __name__ == "__main__": + unittest.main()