diff --git a/scripts/exporter/romm_exporter.py b/scripts/exporter/romm_exporter.py index 700029bb..01cf11ec 100644 --- a/scripts/exporter/romm_exporter.py +++ b/scripts/exporter/romm_exporter.py @@ -29,6 +29,9 @@ SOURCE_URL = ( class Exporter(BaseExporter): """Write RomM's known_bios_files.json, corrected.""" + # (system, filename) keys the platform itself declares, set by render. + _platform_keys: frozenset[tuple[str, str]] = frozenset() + @staticmethod def platform_name() -> str: return "romm" @@ -61,16 +64,20 @@ class Exporter(BaseExporter): """ return native_id in SLUG_MAP - @classmethod - def writable(cls, fe: NativeFile, require: str = "") -> bool: + def writable(self, fe: NativeFile, require: str = "") -> bool: """What RomM already ships stays; the conditions gate additions. An entry of theirs that could never verify is still theirs, and the - round trip is not the place to decide otherwise. + round trip is not the place to decide otherwise. An addition whose + slug:filename key the platform already holds cannot be written: the + fixture is a dict, and the second write replaced RomM's Dreamcast + boot.bin with fbneo's 480-byte file of the same name. """ if fe.platform is not None: return True - return cls._verifiable(fe) and cls._known_platform(fe.native_system) + if (fe.native_system, fe.name) in self._platform_keys: + return False + return self._verifiable(fe) and self._known_platform(fe.native_system) def render( self, @@ -80,6 +87,12 @@ class Exporter(BaseExporter): scraped: dict | None = None, ) -> dict[str, str]: output: OrderedDict[str, dict] = OrderedDict() + self._platform_keys = { + (system.native_id, fe.name) + for system in systems.values() + for fe in system.files + if fe.platform is not None + } for system in sorted(systems.values(), key=lambda s: s.native_id): for fe in sorted(system.files, key=lambda f: f.name): diff --git a/tests/test_export_counts.py b/tests/test_export_counts.py index 75792d1e..aabdca1f 100644 --- a/tests/test_export_counts.py +++ b/tests/test_export_counts.py @@ -183,6 +183,21 @@ class OneFileOneIdentity(unittest.TestCase): declared = next(f for f in systems["dc"].files if f.platform is not None) self.assertIsNone(declared.truth) +class RommKeepsItsOwnKeys(unittest.TestCase): + def test_a_same_named_addition_does_not_replace_their_entry(self): + """fbneo's 480-byte boot.bin overwrote RomM's Dreamcast boot.bin.""" + from exporter.romm_exporter import Exporter as Romm + + theirs = NativeFile("boot.bin", "dc/boot.bin", "dc", + platform={"size": 2097152, "md5": A}) + ours = NativeFile("boot.bin", "fbneo/boot.bin", "dc", + truth={"size": 480, "crc32": "f0774fc2"}) + exporter = Romm() + produced = exporter.render({"dc": NativeSystem("dc", files=[theirs, ours])}, None, {}) + written = json.loads(produced[exporter.native_filename()]) + self.assertEqual(written["dc:boot.bin"]["size"], "2097152") + self.assertFalse(exporter.writable(ours)) + class RecalboxKeepsItsOwnNotes(unittest.TestCase): def test_no_profile_prose_reaches_a_note(self): fe = NativeFile("bios.bin", "bios.bin", "psx", platform={"md5": A}, diff --git a/tests/test_exporters.py b/tests/test_exporters.py index 82a6c09f..50e4fcaf 100644 --- a/tests/test_exporters.py +++ b/tests/test_exporters.py @@ -206,7 +206,7 @@ class Reconciliation(unittest.TestCase): if entry.platform is None: continue with self.subTest(platform=name, file=entry.name): - self.assertTrue(cls.writable(entry)) + self.assertTrue(cls().writable(entry)) def test_an_addition_the_format_cannot_express_is_refused(self): truth = {