diff --git a/scripts/exporter/retrobat_exporter.py b/scripts/exporter/retrobat_exporter.py index 04d4c133..1f014a91 100644 --- a/scripts/exporter/retrobat_exporter.py +++ b/scripts/exporter/retrobat_exporter.py @@ -66,33 +66,42 @@ class Exporter(BaseExporter): originals: dict[str, str], scraped: dict | None = None, ) -> dict[str, str]: - # Keep the platform's own key order when we have its file, so the - # diff a maintainer reads is the corrections and nothing else. - order: list[str] = [] + # The platform's own file is the base: its key and entry order stay, + # so the diff a maintainer reads is the corrections and nothing else, + # and a system or file our transcription does not carry (RetroBat + # renamed ic10_g24lc64 after a scrape) is kept as RetroBat wrote it. + base: dict = {} original = originals.get(self.native_filename(), "") if original: try: - order = list(json.loads(original)) + base = json.loads(original, object_pairs_hook=OrderedDict) except json.JSONDecodeError: - order = [] + base = {} exportable = { system.native_id: (system, files) for system, files in self.exportable(systems, require="md5") } - keys = [k for k in order if k in exportable] - keys.extend(sorted(k for k in exportable if k not in keys)) + keys = list(base) + keys.extend(sorted(k for k in exportable if k not in base)) output: OrderedDict[str, object] = OrderedDict() for key in keys: + if key not in exportable: + output[key] = base[key] + continue system, files = exportable[key] - bios_files = [] + ours: OrderedDict[str, OrderedDict[str, str]] = OrderedDict() for fe in files: entry: OrderedDict[str, str] = OrderedDict() entry["md5"] = self._md5(fe) declared = fe.native("native_path", "") entry["file"] = str(declared) if declared else f"bios/{fe.destination}" - bios_files.append(entry) + ours.setdefault(entry["file"], entry) + bios_files = [] + for theirs in (base.get(key) or {}).get("biosFiles", []): + bios_files.append(ours.pop(theirs.get("file"), theirs)) + bios_files.extend(ours.values()) system_entry: OrderedDict[str, object] = OrderedDict() system_entry["name"] = self.display_name(system) system_entry["biosFiles"] = bios_files diff --git a/tests/test_export_counts.py b/tests/test_export_counts.py index e4df63c9..6ed3a51a 100644 --- a/tests/test_export_counts.py +++ b/tests/test_export_counts.py @@ -148,6 +148,39 @@ class HashesTheFormatCannotHold(unittest.TestCase): self.assertFalse(EmuDeck().states(unhashed, "md5")) +class RetroBatKeepsWhatWeDoNotModel(unittest.TestCase): + """RetroBat renamed chihiro/ic10_g24lc64 to ic10_g24lc64.bin after our + scrape. The export was rebuilt from our model and read the original only + for its key order, so the renamed entry vanished from RetroBat's file, + and a system we do not carry would have vanished whole.""" + + def test_an_unmodelled_file_and_system_stay_as_written(self): + modelled = NativeFile( + "ic11_24lc024.bin", "chihiro/ic11_24lc024.bin", "chihiro", + platform={"md5": "", "native_path": "bios/chihiro/ic11_24lc024.bin"}, + truth={"md5": A}, filled=["md5"], + ) + systems = {"chihiro": NativeSystem("chihiro", files=[modelled])} + original = { + "chihiro": {"name": "Sega Chihiro", "biosFiles": [ + {"md5": "", "file": "bios/chihiro/ic10_g24lc64.bin"}, + {"md5": "", "file": "bios/chihiro/ic11_24lc024.bin"}, + ]}, + "newsys": {"name": "New", "biosFiles": [{"md5": B, "file": "bios/new.bin"}]}, + } + from exporter.baseline import Report + + produced = json.loads(RetroBat().render( + systems, Report(), {"batocera-systems.json": json.dumps(original)}, + )["batocera-systems.json"]) + self.assertEqual(list(produced), ["chihiro", "newsys"]) + self.assertEqual(produced["newsys"], original["newsys"]) + self.assertEqual(produced["chihiro"]["biosFiles"], [ + {"md5": "", "file": "bios/chihiro/ic10_g24lc64.bin"}, + {"md5": A, "file": "bios/chihiro/ic11_24lc024.bin"}, + ]) + + class EmuDeckWritesItsCorrections(unittest.TestCase): """A correction replaced nothing: every withdrawal was refused, the run counted the correction anyway and validate failed on it."""