From 659e9b591f4c8aa411ea75743b2e1f621a3c2690 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Tue, 20 Oct 2026 19:07:46 +0200 Subject: [PATCH] fix: keep RetroBat's blank line between systems RetroBat separates every system after the first with a blank line. json.dumps dropped all 100 of them: 100 hunks of noise in a diff meant to show corrections. The separator is written again where the original has it, and an added system takes it too where the original separates them all. An empty truth gives back the cached file byte for byte. --- scripts/exporter/retrobat_exporter.py | 31 ++++++++++++++++++++++++++- tests/test_exporters.py | 25 +++++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/scripts/exporter/retrobat_exporter.py b/scripts/exporter/retrobat_exporter.py index 22ce255c..c14d064f 100644 --- a/scripts/exporter/retrobat_exporter.py +++ b/scripts/exporter/retrobat_exporter.py @@ -7,6 +7,7 @@ state md5 then file, in that order. from __future__ import annotations import json +import re from collections import OrderedDict from .base_exporter import BaseExporter @@ -18,6 +19,33 @@ SOURCE_URL = ( ) +_TOP_KEY = re.compile(r'^ "((?:[^"\\]|\\.)*)":', re.MULTILINE) + + +def keep_blank_lines(original: str, produced: str) -> str: + """The blank line RetroBat writes before each system, written again. + + json.dumps has no blank lines, and the original separates every system + after the first with one: dropped, they were 100 hunks of a diff meant + to show corrections. A system the original separates is separated, and + where it separates them all, an added one is too. + """ + keys = _TOP_KEY.findall(original) + separated = set(re.findall(r'\n\n "((?:[^"\\]|\\.)*)":', original)) + every = bool(keys) and separated == set(keys[1:]) + first = True + + def separate(match: re.Match[str]) -> str: + nonlocal first + key, was_first = match.group(1), first + first = False + if not was_first and (key in separated or (every and key not in keys)): + return "\n" + match.group(0) + return match.group(0) + + return _TOP_KEY.sub(separate, produced) + + class Exporter(BaseExporter): """Write RetroBat's batocera-systems.json, corrected.""" @@ -105,7 +133,8 @@ class Exporter(BaseExporter): output[key] = system_entry text = json.dumps(output, indent=2, ensure_ascii=False) + "\n" - return {self.native_filename(): text} + original = (originals or {}).get(self.native_filename(), "") + return {self.native_filename(): keep_blank_lines(original, text) if original else text} def validate( self, diff --git a/tests/test_exporters.py b/tests/test_exporters.py index 67351c8f..840898f1 100644 --- a/tests/test_exporters.py +++ b/tests/test_exporters.py @@ -1420,6 +1420,31 @@ class BizhawkExport(unittest.TestCase): self.assertFalse(exporter.states(entry, "sha1")) +class RetrobatListComesBackAsWritten(unittest.TestCase): + """RetroBat separates each system with a blank line. json.dumps dropped + all 100 of them, 100 hunks of noise in a diff meant to show + corrections.""" + + def test_an_empty_truth_hands_back_the_list(self): + original_path = UPSTREAM_CACHE / "retrobat" / "batocera-systems.json" + if not original_path.exists(): + self.skipTest("upstream cache not populated") + original = original_path.read_text(encoding="utf-8") + exporter = discover_exporters()["retrobat"]() + config = native_layer(load_platform_config("retrobat", str(REPO_ROOT / "platforms"))) + systems, report = build_native_model({"systems": {}}, config) + produced = exporter.render(systems, report, {"batocera-systems.json": original}, config) + self.assertEqual(produced["batocera-systems.json"], original) + + def test_an_added_system_takes_the_same_separator(self): + from exporter.retrobat_exporter import keep_blank_lines # noqa: PLC0415 + + original = '{\n "a": {},\n\n "b": {}\n}\n' + produced = '{\n "a": {},\n "b": {},\n "c": {}\n}\n' + self.assertEqual(keep_blank_lines(original, produced), + '{\n "a": {},\n\n "b": {},\n\n "c": {}\n}\n') + + class MisterDatabaseComesBackAsWritten(unittest.TestCase): """The BiosDB is one line of JSON. Written with indent=2 it came out as 1499 changed lines for a database whose content had not changed."""