diff --git a/scripts/exporter/recalbox_exporter.py b/scripts/exporter/recalbox_exporter.py index 0647bcb8..490ae90b 100644 --- a/scripts/exporter/recalbox_exporter.py +++ b/scripts/exporter/recalbox_exporter.py @@ -222,6 +222,19 @@ class Exporter(BaseExporter): return False return super().states(fe, field_name) + def lands(self, fe: NativeFile) -> bool: + """An element of theirs the render cannot rewrite is still in the + file, written back as Recalbox wrote it.""" + return self.writable(fe) or id(fe) in getattr(self, "_held_whole", {}) + + def withheld(self) -> list[str]: + """Each element kept as written because the truth names no md5.""" + return [ + f"{label} keeps its md5: the truth accepts another dump and " + "names no md5 for it, which es_bios.xsd requires" + for label in sorted(getattr(self, "_held_whole", {}).values()) + ] + @classmethod def writable(cls, fe: NativeFile, require: str = "") -> bool: """es_bios.xsd makes md5 and core required: without them, no element. @@ -272,6 +285,21 @@ class Exporter(BaseExporter): for s in root.iter("system") ] + # Elements of theirs written back as they stand, by (platform, path). + held = { + (platform, " ".join(child.get("path", "").split())) + for platform, _, children in theirs + for child in children + if isinstance(child, dict) + } + self._held_whole = { + id(fe): f"{system.native_id}/{self._path(fe)}" + for system in systems.values() + for fe in system.files + if fe.platform is not None + and not self.writable(fe) + and (system.native_id, " ".join(self._path(fe).split())) in held + } ours: dict[str, tuple[NativeSystem, dict[str, tuple[NativeFile, str]]]] = {} for system in systems.values(): writable = [fe for fe in system.files if self.writable(fe)] diff --git a/tests/test_exporters.py b/tests/test_exporters.py index 3c7f4dfc..72025477 100644 --- a/tests/test_exporters.py +++ b/tests/test_exporters.py @@ -1169,6 +1169,32 @@ class RecalboxExport(unittest.TestCase): [("cdimono1.zip", False, True)], ) + def test_an_element_kept_as_written_is_not_counted_lost(self): + """The truth takes the entry on sha1 and size and names no md5: the + element is written back whole, and the run counted it a platform + entry the format cannot keep.""" + scraped = {"systems": {"x": {"native_id": "x", "files": [{ + "name": "a.bin", "destination": "x/a.bin", "md5": "a" * 32, "size": 100, + "native_system": "x", "core": "libretro/foo", "required": True, + }]}}} + truth = {"systems": {"x": {"files": [{ + "name": "a.bin", "path": "x/a.bin", "sha1": "b" * 40, "size": 200, + "required": True, "_cores": ["foo"], + }]}}} + element = f' ' + original = ( + '\n\n' + f' \n{element}\n \n\n' + ) + systems, report = build_native_model(truth, scraped) + exporter = discover_exporters()["recalbox"]() + produced = exporter.render(systems, report, {"es_bios.xml": original}, None) + self.assertIn(element, produced["es_bios.xml"]) + fe = next(fe for s in systems.values() for fe in s.files) + self.assertTrue(exporter.lands(fe)) + self.assertEqual(landed_changes(exporter, systems).records, []) + self.assertEqual(len(exporter.withheld()), 1) + def test_a_truncated_export_is_reported(self): exporter, systems, _ = self._render() empty = {