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")