diff --git a/scripts/truth.py b/scripts/truth.py index fd409653..13116bb5 100644 --- a/scripts/truth.py +++ b/scripts/truth.py @@ -200,6 +200,36 @@ def _same_file(files: list[dict], file_entry: dict, emu_name: str) -> dict | Non ) +def _require_by_slot(files: list[dict]) -> None: + """A core that requires a name an entry answers for requires the entry. + + Merged by name, one core's alternatives land in several entries. Ymir + reads sega_101.bin, mpr-17933.bin or saturn_bios.bin, so the sega_101.bin + entry carries those aliases. kronos, yabause and yabasanshiro open + saturn_bios.bin first and sega_101.bin as a fallback: they sat in that + entry's _cores, out of its _required_by, while their saturn_bios.bin entry + had them requiring it, and a platform line paired through the alias read + them as optional. A core already among the entry's readers that requires + one of its aliases, as another entry of the system, is one of its + requirers. No reader is added. + """ + required_by_name: dict[str, set[str]] = {} + for entry in files: + required_by_name.setdefault(entry["name"].lower(), set()).update( + entry.get("_required_by", ()) + ) + for entry in files: + # Only the aliases: entries sharing the entry's own name are other + # files that name already kept apart (pak0.pak under each game). + names = {str(alias).lower() for alias in entry.get("aliases") or ()} + names.discard(entry["name"].lower()) + requirers = set().union(*(required_by_name.get(n, set()) for n in names)) + added = (requirers & entry.get("_cores", set())) - entry.get("_required_by", set()) + if added: + entry["_required_by"] = entry.get("_required_by", set()) | added + entry["required"] = True + + def _brought(file_entry: dict, emu_name: str) -> set[str]: """The core and build that need the file brought, empty when the emulator installs it. An entry read without layer (a test, a caller of @@ -666,6 +696,9 @@ def generate_platform_truth( ) sys_cov["unprofiled"].add(emu_name) + for sys_data in systems.values(): + _require_by_slot(sys_data.get("files", [])) + # Drop files with no exploitable data AFTER all cores have contributed. # A file declared by one core without hash/size/path may be enriched by # another core that has the same entry with data, so the filter must run diff --git a/tests/test_truth_diff.py b/tests/test_truth_diff.py index 75d559c5..fa48edf6 100644 --- a/tests/test_truth_diff.py +++ b/tests/test_truth_diff.py @@ -246,6 +246,51 @@ class ACompositeZipMd5IsNoContainerMd5(unittest.TestCase): self.assertEqual([m["name"] for m in result["hash_mismatch"]], ["neocdz.zip"]) +class OneSlotUnderSeveralNames(unittest.TestCase): + """kronos opens saturn_bios.bin, then sega_101.bin as a fallback. Ymir + reads either name, so the sega_101.bin entry answers for saturn_bios.bin + too. kronos sat in that entry's _cores and out of its _required_by while + its saturn_bios.bin entry had it requiring the file.""" + + PROFILES = { + "kronos": { + "emulator": "Kronos", "type": "libretro", "systems": ["sega-saturn"], + "files": [ + {"name": "saturn_bios.bin", "path": "kronos/saturn_bios.bin", + "required": True, "size": 524288}, + {"name": "sega_101.bin", "required": False, "size": 524288}, + ], + }, + "ymir": { + "emulator": "Ymir", "type": "libretro", "systems": ["sega-saturn"], + "files": [ + {"name": "sega_101.bin", "required": True, "size": 524288, + "aliases": ["saturn_bios.bin"]}, + ], + }, + "other": { + "emulator": "Other", "type": "libretro", "systems": ["sega-saturn"], + "files": [{"name": "saturn_bios.bin", "required": True, "size": 524288}], + }, + } + + def _files(self) -> dict[str, dict]: + cores = ["kronos", "ymir", "other"] + model = generate_platform_truth("p", {"cores": cores}, {}, self.PROFILES) + return {f["name"]: f for f in model["systems"]["sega-saturn"]["files"]} + + def test_a_reader_requiring_an_alias_requires_the_entry(self): + sega = self._files()["sega_101.bin"] + self.assertEqual(sega["_cores"], ["kronos", "ymir"]) + self.assertEqual(sega["_required_by"], ["kronos", "ymir"]) + + def test_no_reader_is_added(self): + """other requires saturn_bios.bin but never opens sega_101.bin.""" + sega = self._files()["sega_101.bin"] + self.assertNotIn("other", sega["_required_by"]) + self.assertTrue(set(sega["_required_by"]) <= set(sega["_cores"])) + + class OnlySystemDirectoryEntries(unittest.TestCase): """The truth kept files read from the save, content or working directory: diff_truth listed Cemu's mlc01 certificates, panda3ds's seeddb.bin and