From 12fcdd52a6c4667a18d6d3fe18084f8041de6bac Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Fri, 16 Oct 2026 18:14:03 +0200 Subject: [PATCH] fix: keep every revision a core accepts in truth --- scripts/truth.py | 28 +++++++++++++++++++++---- tests/test_truth_layout.py | 42 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 4 deletions(-) diff --git a/scripts/truth.py b/scripts/truth.py index 197b6c5a..65dc9d96 100644 --- a/scripts/truth.py +++ b/scripts/truth.py @@ -198,6 +198,7 @@ def _contents_disagree(a: dict, b: dict) -> bool: def _fold_into(existing: dict, file_entry: dict, emu_name: str) -> None: """Add one core's declaration to the entry that already stands for it.""" + revision = emu_name in existing.get("_cores", ()) existing["_cores"] = existing.get("_cores", set()) | {emu_name} sr = file_entry.get("source_ref") if sr is not None: @@ -210,7 +211,7 @@ def _fold_into(existing: dict, file_entry: dict, emu_name: str) -> None: # Which cores need it: `required` above is the union, true when # any core needs it, and a package of one core must not read it. existing["_required_by"] = existing.get("_required_by", set()) | {emu_name} - _merge_hashes(existing, file_entry, emu_name) + _merge_hashes(existing, file_entry, emu_name, revision) # Merge non-hash data fields if existing lacks them. # A core that creates an entry without size/path/validation may be # enriched by a sibling core that has those fields. @@ -220,7 +221,15 @@ def _fold_into(existing: dict, file_entry: dict, emu_name: str) -> None: _merge_priority(existing, file_entry.get("priority")) -def _merge_hashes(existing: dict, file_entry: dict, emu_name: str) -> None: +def _merge_hashes( + existing: dict, file_entry: dict, emu_name: str, revision: bool = False +) -> None: + """Fold one declaration's hashes into the entry. + + A core that lists several revisions under one name accepts each of them + (dosbox's MT-32 control ROMs, raze's GRP versions): ``revision`` keeps + the union. Two cores that disagree keep the first value and are reported. + """ for h in _HASH_FIELDS: theirs = file_entry.get(h, "") ours = existing.get(h, "") @@ -229,9 +238,14 @@ def _merge_hashes(existing: dict, file_entry: dict, emu_name: str) -> None: if not ours: existing[h] = theirs continue - # Normalize to sets for multi-hash comparison t_list = theirs if isinstance(theirs, list) else [theirs] o_list = ours if isinstance(ours, list) else [ours] + if revision: + known = {str(v).lower() for v in o_list} + added = [v for v in t_list if str(v).lower() not in known] + if added: + existing[h] = [*o_list, *added] + continue if not {str(v).lower() for v in t_list} & {str(v).lower() for v in o_list}: print( f"WARNING: hash conflict for {file_entry['name']} " @@ -618,7 +632,13 @@ def _pair_rank(truth_entry: dict, scraped_entry: dict) -> tuple[bool, bool, bool def _hash_mismatch(t_entry: dict, s_entry: dict) -> dict | None: - """The first hash both sides declare without a value in common.""" + """The first hash both sides declare without a value in common. + + A value shared on any field settles it: the truth holds every revision a + core accepts, and a profile may give one revision's sha1 without its md5. + """ + if _hash_set(t_entry) & _hash_set(s_entry): + return None for h in ("sha1", "md5", "crc32"): t_hash = t_entry.get(h, "") s_hash = s_entry.get(h, "") diff --git a/tests/test_truth_layout.py b/tests/test_truth_layout.py index 031f673c..db71b93f 100644 --- a/tests/test_truth_layout.py +++ b/tests/test_truth_layout.py @@ -131,5 +131,47 @@ class AnUnattributedEntryIsNotGuessed(unittest.TestCase): ["apple2gs.rom", "zx81.rom"], ) + +class EveryRevisionACoreAcceptsStays(unittest.TestCase): + """dosbox lists each MT-32 control ROM it accepts under one name. The + truth kept the first sha1 and dropped the others, so a frontend holding + 1.07 read as a mismatch against a core that loads it.""" + + def truth(self, profiles): + config = {"cores": list(profiles), "systems": {}} + return generate_platform_truth("p", config, {}, profiles) + + @staticmethod + def entry(name, sha1): + return {"name": name, "system": "dos", "sha1": sha1} + + def test_one_core_keeps_the_union(self): + profile = {"emulator": "DOSBox", "type": "libretro", "systems": ["dos"], + "files": [self.entry("MT32_CONTROL.ROM", "a" * 40), + self.entry("MT32_CONTROL.ROM", "b" * 40)]} + files = self.truth({"dosbox": profile})["systems"]["dos"]["files"] + self.assertEqual([f["sha1"] for f in files], [["a" * 40, "b" * 40]]) + + def test_two_cores_that_disagree_keep_the_first(self): + first = {"emulator": "A", "type": "libretro", "systems": ["dos"], + "files": [self.entry("x.rom", "a" * 40)]} + second = {"emulator": "B", "type": "libretro", "systems": ["dos"], + "files": [{**self.entry("x.rom", "b" * 40), "path": "x.rom"}]} + files = self.truth({"a": first, "b": second})["systems"]["dos"]["files"] + self.assertEqual([f["sha1"] for f in files], ["a" * 40]) + + def test_a_revision_known_by_one_hash_is_no_mismatch(self): + """1.04 carries md5 and sha1, 1.07 only its sha1: a platform entry + giving 1.07's sha1 and md5 is that revision, not a mismatch.""" + from truth import _hash_mismatch + + truth = {"name": "MT32_CONTROL.ROM", "md5": "c" * 32, "sha1": ["a" * 40, "b" * 40]} + scraped = {"name": "MT32_CONTROL.ROM", "md5": "d" * 32, "sha1": "b" * 40} + self.assertIsNone(_hash_mismatch(truth, scraped)) + self.assertEqual( + _hash_mismatch(truth, {**scraped, "sha1": "e" * 40})["hash_type"], "sha1" + ) + + if __name__ == "__main__": unittest.main()