diff --git a/scripts/cross_reference.py b/scripts/cross_reference.py index 488ad8e4..e9ff726c 100644 --- a/scripts/cross_reference.py +++ b/scripts/cross_reference.py @@ -242,10 +242,17 @@ class FileConsumers: self.platforms: dict[str, set[str]] = {} self.emulators: dict[str, set[str]] = {} - def sha1_of(self, entry: dict, dest: str = "") -> str | None: - """The collected file an entry resolves to, or None.""" + def sha1_of(self, entry: dict, dest: str = "", owner: str = "") -> str | None: + """The collected file an entry resolves to, or None. + + A profile entry names its owner, as the builder does: without it the + name step chose another tree's copy, and the page credited the file + the pack does not ship. + """ if not entry.get("name"): return None + if owner: + entry = {**entry, "source_profile": owner} path, status = resolve_local_file( entry, self._db, self._zip_contents, dest_hint=dest ) @@ -266,7 +273,7 @@ class FileConsumers: if profile.get("type") == "alias": continue for entry in profile.get("files", []): - sha1 = self.sha1_of(entry, entry.get("path") or "") + sha1 = self.sha1_of(entry, entry.get("path") or "", owner=name) if sha1: self.emulators.setdefault(sha1, set()).add(name) diff --git a/scripts/packextras.py b/scripts/packextras.py index a74b3058..0405d608 100644 --- a/scripts/packextras.py +++ b/scripts/packextras.py @@ -213,7 +213,9 @@ def _agnostic_scan_extras( if not isinstance(candidate, dict): continue local, status = resolve_local_file( - candidate, db, dest_hint=candidate.get("path", "") + {**candidate, "source_profile": emu_name}, + db, + dest_hint=candidate.get("path", ""), ) if not local or "/" not in local: continue diff --git a/scripts/slots.py b/scripts/slots.py index 02c99db1..6c76e40b 100644 --- a/scripts/slots.py +++ b/scripts/slots.py @@ -175,8 +175,9 @@ def profile_claims( if not dest: continue full = f"{base_dest}/{dest}" if base_dest else dest + # The owner's own copy is the one the builder ships. local, status = resolve_local_file( - entry, + {**entry, "source_profile": emu_name}, db, zip_contents, dest_hint=dest, diff --git a/tests/test_owner_resolution.py b/tests/test_owner_resolution.py new file mode 100644 index 00000000..bf0e4cc9 --- /dev/null +++ b/tests/test_owner_resolution.py @@ -0,0 +1,65 @@ +"""Every reader of a profile entry names its owner, as the builder does. + +The resolver prefers the owning profile's copy when a name has several. The +builder passes `source_profile`; the site's consumer index, the slot claims +and the agnostic scan did not, and resolved 95 entries to other files: the +site credited aethersx2's game_controller_db.txt to armsx2 and left the copy +armsx2's pack ships without a reader, and MAME's neogeo.zip without MAME. +""" + +from __future__ import annotations + +import sys +import unittest +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parent.parent +sys.path.insert(0, str(REPO_ROOT / "scripts")) + +import cross_reference # noqa: E402 +import slots # noqa: E402 +from common import ( # noqa: E402 + build_zip_contents_index, + load_database, + load_emulator_profiles, +) + + +class ReadersNameTheOwner(unittest.TestCase): + @classmethod + def setUpClass(cls): + db_path = REPO_ROOT / "database.json" + if not db_path.exists(): + raise unittest.SkipTest("database.json is not built") + cls.db = load_database(str(db_path)) + cls.profiles = load_emulator_profiles(str(REPO_ROOT / "emulators")) + cls.sha1_by_path = {r["path"]: s for s, r in cls.db["files"].items()} + + def test_the_site_credits_the_copy_the_pack_ships(self): + consumers = cross_reference.FileConsumers( + self.db, build_zip_contents_index(self.db) + ) + consumers.add_emulators(self.profiles) + for path, owner in ( + ("bios/Other/armsx2/game_controller_db.txt", "armsx2"), + ("bios/Arcade/MAME/neogeo.zip", "mame"), + ): + with self.subTest(path=path): + sha1 = self.sha1_by_path.get(path) + if sha1 is None: + self.skipTest(f"{path} is not collected") + self.assertIn(owner, consumers.emulators.get(sha1, set())) + + def test_slot_claims_resolve_the_owners_tree(self): + claims = slots.profile_claims({"pcem": self.profiles["pcem"]}, self.db) + held = { + claim.destination.rsplit("/", 1)[-1]: claim.local_path for claim in claims + } + local = held.get("ide_xt.bin") + if local is None: + self.skipTest("ide_xt.bin is not claimed") + self.assertTrue(local.startswith("bios/Other/pcem/"), local) + + +if __name__ == "__main__": + unittest.main()