From 673e70d791b1992704845d1503ea409a5595d45f Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Thu, 5 Nov 2026 12:01:17 +0100 Subject: [PATCH] feat: refuse an arcade index off its profile pin --- scripts/validate_schemas.py | 29 +++++++++++++++++++++++++++++ tests/test_arcade_parents.py | 24 ++++++++++++++++++++++++ wiki/tools.md | 4 ++++ 3 files changed, 57 insertions(+) diff --git a/scripts/validate_schemas.py b/scripts/validate_schemas.py index bb377a2a6..5380f64c6 100644 --- a/scripts/validate_schemas.py +++ b/scripts/validate_schemas.py @@ -701,6 +701,34 @@ def _games_source_errors() -> list[str]: return out +def _arcade_pin_errors() -> list[str]: + """games/arcade/.json read at the revision and version its + profile pins. A bump of the profile left the game-to-BIOS map of the old + code in place, and the installer kept serving it.""" + out: list[str] = [] + for path in sorted((ROOT / "games" / "arcade").glob("*.json")): + doc = _load_json(path) + profile_path = ROOT / "emulators" / f"{doc.get('profile')}.yml" + label = str(path.relative_to(ROOT)) + if not profile_path.is_file(): + out.append(f"{label}: no profile emulators/{profile_path.name}") + continue + profile = _load_source(profile_path) or {} + pinned = profile.get("source_commit") + pins = set(pinned.values()) if isinstance(pinned, dict) else {pinned} + commit = (doc.get("source") or {}).get("commit") + drifted = [] + if commit and commit not in pins: + drifted.append(f"read at {commit[:12]}, the profile pins {pinned}") + if doc.get("core_version") != profile.get("core_version"): + drifted.append(f"core_version {doc.get('core_version')!r}, " + f"the profile says {profile.get('core_version')!r}") + if drifted: + out.append(f"{label}: {'; '.join(drifted)} " + f"(python -m scripts.scraper.arcade_parent_importer --profile {doc.get('profile')})") + return out + + def _games_index_errors() -> list[str]: """install/games/: each document against its kind, and the index against the files it lists (size and sha256).""" @@ -765,6 +793,7 @@ def main() -> int: errors.extend(_measurement_errors()) errors.extend(_romfolder_errors()) errors.extend(_games_source_errors()) + errors.extend(_arcade_pin_errors()) if not args.source_only: database_path = ROOT / "database.json" diff --git a/tests/test_arcade_parents.py b/tests/test_arcade_parents.py index dc3ac2160..c03b4caa1 100644 --- a/tests/test_arcade_parents.py +++ b/tests/test_arcade_parents.py @@ -401,6 +401,30 @@ class CommittedIndex(unittest.TestCase): errors = list(validator.iter_errors(json.loads(path.read_text(encoding="utf-8")))) self.assertEqual(errors, []) + @unittest.skipIf(Draft202012Validator is None, "jsonschema not installed") + def test_each_file_is_read_at_its_profile_pin(self) -> None: + """A bumped profile kept the game-to-BIOS map of the old code.""" + import sys + from unittest import mock + + sys.path.insert(0, str(ROOT / "scripts")) + import validate_schemas + + self.assertEqual(validate_schemas._arcade_pin_errors(), []) + with tempfile.TemporaryDirectory(dir=TMP_ROOT) as tmp: + root = Path(tmp) + (root / "games" / "arcade").mkdir(parents=True) + (root / "emulators").mkdir() + (root / "emulators" / "core.yml").write_text( + f"source_commit: {'b' * 40}\ncore_version: '0.2'\n") + (root / "games" / "arcade" / "core.json").write_text(json.dumps( + {"profile": "core", "core_version": "0.1", "source": {"commit": COMMIT}})) + with mock.patch.object(validate_schemas, "ROOT", root): + (error,) = validate_schemas._arcade_pin_errors() + self.assertIn("read at aaaaaaaaaaaa", error) + self.assertIn("core_version '0.1'", error) + self.assertIn("arcade_parent_importer --profile core", error) + if __name__ == "__main__": unittest.main() diff --git a/wiki/tools.md b/wiki/tools.md index 1acea4c99..31a0b756d 100644 --- a/wiki/tools.md +++ b/wiki/tools.md @@ -1644,6 +1644,10 @@ keeps, is computed from `install/.json` with the installer's own core narrowing. Every published file is listed in `install/games/index.json` with its size, its gzip size and its sha256. +`validate_schemas.py` refuses a `games/arcade/.json` whose source +commit or `core_version` is not the one its profile pins: after a profile's pin +moves, rerun `arcade_parent_importer --profile NAME`. + ## Target scrapers Located in `scripts/scraper/targets/`. Each inherits `BaseTargetScraper` and implements `fetch_targets()`.