From 89ac31780bbcc447549b2678b7c8d8214a556e46 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Sun, 18 Oct 2026 20:40:23 +0200 Subject: [PATCH] fix: stop when a profile does not parse generate_db wrote a database without the aliases of a profile it could not read, and the MAME and FBNeo refreshes passed without it. Each now names the file and exits non-zero. --- scripts/generate_db.py | 27 ++++++++++++++++++++++++--- scripts/scraper/fbneo_hash_scraper.py | 6 ++++-- scripts/scraper/mame_hash_scraper.py | 6 ++++-- tests/test_db_aliases.py | 25 +++++++++++++++++++++++++ tests/test_hash_merge.py | 22 ++++++++++++++++++++++ 5 files changed, 79 insertions(+), 7 deletions(-) diff --git a/scripts/generate_db.py b/scripts/generate_db.py index fc3864c9..803cc494 100644 --- a/scripts/generate_db.py +++ b/scripts/generate_db.py @@ -375,7 +375,13 @@ def main() -> int: if preserved: print(f" Preserved {preserved} large file entries from existing database") - platform_aliases = _collect_all_aliases(files) + try: + platform_aliases = _collect_all_aliases(files) + except UnreadableSource as exc: + for source in exc.sources: + print(f"Error: cannot read {source}", file=sys.stderr) + print(f"Error: {args.output} not written", file=sys.stderr) + return 1 for sha1, name_list in platform_aliases.items(): for alias_entry in name_list: if sha1 not in aliases: @@ -440,6 +446,14 @@ def _own_aliases(file_entry: dict, primary_names: set[str]) -> list[str]: ] +class UnreadableSource(Exception): + """Platform or profile files that do not parse, named one per line.""" + + def __init__(self, sources: list[str]) -> None: + super().__init__("\n".join(sources)) + self.sources = sources + + def _collect_all_aliases(files: dict) -> dict: """Collect alternate filenames from platform YAMLs, core-info, and known aliases. @@ -464,6 +478,10 @@ def _collect_all_aliases(files: dict) -> dict: if name not in existing: aliases[matched_sha1].append({"name": name, "path": ""}) + # A source that does not parse is reported and the run refused: written + # without it, the database lost every alias the file proves and the + # command exited 0 (dosbox-x's wqy fonts answered not_found). + unreadable: list[str] = [] platforms_dir = Path("platforms") if platforms_dir.is_dir(): for platform_name in list_registered_platforms( @@ -474,7 +492,7 @@ def _collect_all_aliases(files: dict) -> dict: with open(config_file) as f: config = yaml_load(f) or {} except (yaml.YAMLError, OSError) as e: - print(f"Warning: {config_file.name}: {e}", file=sys.stderr) + unreadable.append(f"{config_file}: {e}") continue for sys_id, system in config.get("systems", {}).items(): @@ -513,7 +531,8 @@ def _collect_all_aliases(files: dict) -> dict: try: with open(emu_file) as f: emu_config = yaml_load(f) or {} - except (yaml.YAMLError, OSError): + except (yaml.YAMLError, OSError) as e: + unreadable.append(f"{emu_file}: {e}") continue # A profile whose entries name each other (dosbox-x opens # MT32_CONTROL.ROM and CM32L_CONTROL.ROM alike, so each entry @@ -602,6 +621,8 @@ def _collect_all_aliases(files: dict) -> dict: for name in group: _add_alias(name, matched_sha1) + if unreadable: + raise UnreadableSource(unreadable) return aliases diff --git a/scripts/scraper/fbneo_hash_scraper.py b/scripts/scraper/fbneo_hash_scraper.py index 4b6099ef..f21fdd75 100644 --- a/scripts/scraper/fbneo_hash_scraper.py +++ b/scripts/scraper/fbneo_hash_scraper.py @@ -204,8 +204,10 @@ def _find_fbneo_profiles() -> list[Path]: for path in source_yaml_paths(EMULATORS_DIR): try: data = yaml_load(path.read_text(encoding="utf-8")) - except (yaml.YAMLError, OSError): - continue + except (yaml.YAMLError, OSError) as exc: + # Skipped, a broken fbneo.yml left the list and the refresh + # reported "no matching emulator profiles found" with exit 0. + raise RuntimeError(f"cannot read {path}: {exc}") from exc if not data or not isinstance(data, dict): continue upstream = data.get("upstream", "") diff --git a/scripts/scraper/mame_hash_scraper.py b/scripts/scraper/mame_hash_scraper.py index f530f302..076e01ef 100644 --- a/scripts/scraper/mame_hash_scraper.py +++ b/scripts/scraper/mame_hash_scraper.py @@ -216,8 +216,10 @@ def _find_mame_profiles() -> list[Path]: and source.rstrip("/") in _MAME_TREES ): profiles.append(path) - except (yaml.YAMLError, OSError): - continue + except (yaml.YAMLError, OSError) as exc: + # Skipped, a broken mame.yml left the list and the refresh + # reported success without touching it. + raise RuntimeError(f"cannot read {path}: {exc}") from exc return profiles diff --git a/tests/test_db_aliases.py b/tests/test_db_aliases.py index f8136433..3d2af69f 100644 --- a/tests/test_db_aliases.py +++ b/tests/test_db_aliases.py @@ -128,5 +128,30 @@ class CrossNamingStaysInItsProfile(unittest.TestCase): self.assertEqual([a["name"] for a in aliases["mt"]], ["MT32_CONTROL.ROM"]) self.assertEqual([a["name"] for a in aliases["cm"]], ["CM32L_CONTROL.ROM"]) +class AnUnreadableSourceStopsTheRun(unittest.TestCase): + """A profile with a YAML error was skipped in silence: the database was + written without its aliases and the command exited 0.""" + + def test_the_file_is_named_and_nothing_collected(self): + import os + import shutil + import tempfile + + import generate_db + + tmp_root = Path(__file__).resolve().parents[1] / "tmp" + tmp_root.mkdir(exist_ok=True) + root = Path(tempfile.mkdtemp(dir=tmp_root)) + self.addCleanup(shutil.rmtree, root, True) + previous = os.getcwd() + os.chdir(root) + self.addCleanup(os.chdir, previous) + Path("emulators").mkdir() + Path("emulators/broken.yml").write_text("files: [\n - name: a\n") + with self.assertRaises(generate_db.UnreadableSource) as caught: + generate_db._collect_all_aliases({}) + self.assertIn("broken.yml", str(caught.exception)) + + if __name__ == "__main__": unittest.main() diff --git a/tests/test_hash_merge.py b/tests/test_hash_merge.py index d4dbd69c..c7a79a62 100644 --- a/tests/test_hash_merge.py +++ b/tests/test_hash_merge.py @@ -774,5 +774,27 @@ class OneCloneDirectoryPerRun(unittest.TestCase): self.assertTrue(clone.name.endswith(f"-{os.getpid()}")) +class ABrokenProfileStopsTheRefresh(unittest.TestCase): + """A profile with a YAML error left the list in silence: the refresh + printed 'no matching emulator profiles found' and exited 0.""" + + def test_both_scrapers_name_it(self): + from unittest import mock + + from scripts.scraper import fbneo_hash_scraper, mame_hash_scraper + + with tempfile.TemporaryDirectory() as directory: + Path(directory, "fbneo.yml").write_text("files: [\n - name: a\n") + for module, attr, finder in ( + (fbneo_hash_scraper, "EMULATORS_DIR", "_find_fbneo_profiles"), + (mame_hash_scraper, "_EMULATORS_DIR", "_find_mame_profiles"), + ): + with self.subTest(scraper=module.__name__), \ + mock.patch.object(module, attr, Path(directory)), \ + self.assertRaises(RuntimeError) as caught: + getattr(module, finder)() + self.assertIn("fbneo.yml", str(caught.exception)) + + if __name__ == "__main__": unittest.main()