From 4ecb65b8d9dd1777e5bb4fe771f3287687e6b5c2 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Tue, 11 Aug 2026 18:41:04 +0200 Subject: [PATCH] fix: keep the mame clone map across runs A clone group is only visible while both copies are on disk, and the run then deletes the clone, so writing only what this run saw erased every mapping an earlier run had recorded. One real run took the map from 69 entries to 1, and the canonical zips silently stopped answering to the names they stand in for. The map is merged now, the 68 lost entries are restored, and the one whose canonical file is no longer in the collection is dropped. --- _mame_clones.json | 14 +++++++++++ scripts/dedup.py | 15 ++++++++++++ tests/test_dedup.py | 58 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 87 insertions(+) diff --git a/_mame_clones.json b/_mame_clones.json index 337bccdd..98e6a9bc 100644 --- a/_mame_clones.json +++ b/_mame_clones.json @@ -173,6 +173,20 @@ "sha1": "2d7d999e2acb4da55c26d1517934d39e7d0a0c86", "total_copies": 1 }, + "bbb.zip": { + "clones": [ + "aaa_long_name.zip" + ], + "sha1": "0ebda75e11e0fdce192562b5bb65a00e3e3bf240", + "total_copies": 1 + }, + "bbc_m87.zip": { + "clones": [ + "bbc_24bbc.zip" + ], + "sha1": "0ebda75e11e0fdce192562b5bb65a00e3e3bf240", + "total_copies": 1 + }, "beebsid.zip": { "clones": [ "bbc_opus3.zip" diff --git a/scripts/dedup.py b/scripts/dedup.py index a23e88e9..4da00695 100644 --- a/scripts/dedup.py +++ b/scripts/dedup.py @@ -228,6 +228,21 @@ def deduplicate(bios_dir: str, dry_run: bool = False) -> dict: # Write MAME clone mapping if mame_clones: clone_path = "_mame_clones.json" + # A group is only visible while both copies are on disk, and this run + # has just deleted the clone. Writing only what was seen this time + # therefore erases every mapping an earlier run recorded, and the + # canonical zip stops answering to the names it stands in for. The + # older entries stay valid: their canonical file is still here. + if not dry_run: + try: + with open(clone_path) as f: + previous = json.load(f) + except (FileNotFoundError, json.JSONDecodeError): + previous = {} + if isinstance(previous, dict): + merged = dict(previous) + merged.update(mame_clones) + mame_clones = merged if dry_run: print(f"\nWould write MAME clone map: {clone_path}") print( diff --git a/tests/test_dedup.py b/tests/test_dedup.py index 744f34a9..cf630934 100644 --- a/tests/test_dedup.py +++ b/tests/test_dedup.py @@ -178,6 +178,64 @@ class EmptyVariantCleanup(_Bios): self.assertTrue((self.bios / "Sony" / "PS" / ".variants").is_dir()) +class CloneMapSurvivesASecondRun(_Bios): + """The clone map must not be destroyed by running dedup again. + + A clone group is only discovered while both copies are on disk, and the + run then deletes the clone. Rewriting the file with just what this run saw + therefore drops every mapping an earlier run recorded, and the canonical + zip stops answering to the names it was standing in for. One real run took + the map from 69 entries to 1. + """ + + def setUp(self): + super().setUp() + self._cwd = os.getcwd() + os.chdir(self._tmp.name) + + def tearDown(self): + os.chdir(self._cwd) + super().tearDown() + + def _map(self) -> dict: + import json + + path = Path(self._tmp.name) / "_mame_clones.json" + return json.loads(path.read_text()) if path.exists() else {} + + def test_a_recorded_mapping_survives_a_later_run(self): + self.write("Arcade/MAME/bbc_m87.zip") + self.write("Arcade/MAME/bbc_24bbc.zip") + self.run_dedup() + first = self._map() + self.assertTrue(first, "first run recorded no clone map") + + # Second run: the clones are gone, so nothing new is found. + self.write("Arcade/MAME/other_a.zip", b"OTHER CONTENT") + self.write("Arcade/MAME/other_b.zip", b"OTHER CONTENT") + self.run_dedup() + second = self._map() + for canonical, entry in first.items(): + self.assertIn(canonical, second, f"{canonical} was dropped") + self.assertEqual(entry["clones"], second[canonical]["clones"]) + + def test_a_new_group_is_added_beside_the_existing_ones(self): + self.write("Arcade/MAME/bbc_m87.zip") + self.write("Arcade/MAME/bbc_24bbc.zip") + self.run_dedup() + before = set(self._map()) + self.write("Arcade/MAME/new_a.zip", b"NEW CONTENT") + self.write("Arcade/MAME/new_b.zip", b"NEW CONTENT") + self.run_dedup() + self.assertTrue(set(self._map()) > before, "the new group was not added") + + def test_a_dry_run_never_writes_the_map(self): + self.write("Arcade/MAME/bbc_m87.zip") + self.write("Arcade/MAME/bbc_24bbc.zip") + self.run_dedup(dry_run=True) + self.assertEqual(self._map(), {}) + + class PathPriority(unittest.TestCase): def test_shorter_paths_sort_first(self): short = dedup.path_priority("bios/Sony/PS/a.bin")